From 5978b0eccb473a1e83e5d0f143714ce00d9cce86 Mon Sep 17 00:00:00 2001 From: Ox Cart Date: Thu, 6 Nov 2014 06:03:42 -0600 Subject: [PATCH 01/10] Made ByteExec use static filename --- byteexec.go | 107 +++++++++++++++++++---------- byteexec_test.go | 72 +++++++++++++++++-- rename_linux.go | 5 -- rename_darwin.go => rename_stub.go | 2 + 4 files changed, 139 insertions(+), 47 deletions(-) delete mode 100644 rename_linux.go rename rename_darwin.go => rename_stub.go (79%) diff --git a/byteexec.go b/byteexec.go index 7dc2211..bbcdb9e 100644 --- a/byteexec.go +++ b/byteexec.go @@ -2,8 +2,7 @@ // supplied as byte arrays, which is handy when used with // github.com/jteeuwen/go-bindata. // -// ByteExec works by storing the provided command in a temp file. A ByteExec -// should always be closed using its Close() method to clean up the temp file. +// ByteExec works by storing the provided command in a file. // // Example Usage: // @@ -19,60 +18,92 @@ package byteexec import ( + "bytes" + "fmt" "io/ioutil" "os" "os/exec" + "path/filepath" + "sync" + + "github.com/getlantern/golog" +) + +const ( + fileMode = 0755 +) + +var ( + log = golog.LoggerFor("byteexec") + + initMutex sync.Mutex ) type ByteExec struct { - fileName string + filename string } -// NewByteExec creates a new ByteExec using the program stored in the provided -// bytes. -func NewByteExec(bytes []byte) (be *ByteExec, err error) { - return NewNamedByteExec(bytes, "byteexec") -} +// New creates a new ByteExec using the program stored in the provided data, at +// the provided filename (relative or absolute path allowed). +// +// WARNING - if a file already exists at this location and its contents differ +// from data, byteexec will attempt to overwrite it. +func New(data []byte, filename string) (*ByteExec, error) { + // Use initMutex to synchronize file operations by this process + initMutex.Lock() + defer initMutex.Unlock() -// NewNamedByteExec creates a new ByteExec using the program stored in the -// provided bytes and uses the given prefix to name the temporary file that gets -// executed. -func NewNamedByteExec(bytes []byte, prefix string) (be *ByteExec, err error) { - var tmpFile *os.File - tmpFile, err = ioutil.TempFile("", prefix+"_") - if err != nil { - return - } - _, err = tmpFile.Write(bytes) - if err != nil { - return - } - tmpFile.Sync() - tmpFile.Chmod(0755) - tmpFile.Close() + filename = renameExecutable(filename) + log.Tracef("Renamed executable to %s for this platform", filename) - orig := tmpFile.Name() - renamed := renameExecutable(orig) - if renamed != orig { - err = os.Rename(orig, renamed) + file, err := os.OpenFile(filename, os.O_RDWR|os.O_CREATE|os.O_EXCL, fileMode) + if err == nil { + log.Tracef("Creating new file at %s", filename) + } else { + if !os.IsExist(err) { + return nil, fmt.Errorf("Unexpected error opening %s: %s", filename, err) + } + + log.Tracef("%s already exists, check to make sure contents is the same", filename) + dataOnDisk, err := ioutil.ReadFile(filename) + if err == nil && bytes.Equal(dataOnDisk, data) { + log.Tracef("Data in %s matches expected, using existing", filename) + fi, err := os.Stat(filename) + if err != nil || fi.Mode() != fileMode { + log.Tracef("Chmodding %s", filename) + err = os.Chmod(filename, fileMode) + if err != nil { + return nil, fmt.Errorf("Unable to chmod file %s: %s", filename, err) + } + } + return newByteExec(filename) + } + log.Tracef("Data in %s doesn't match expected, truncating file", filename) + file, err = os.OpenFile(filename, os.O_RDWR|os.O_CREATE|os.O_TRUNC, fileMode) if err != nil { - return nil, err + return nil, fmt.Errorf("Unable to truncate %s: %s", err) } } - be = &ByteExec{fileName: renamed} - return + + _, err = file.Write(data) + if err != nil { + os.Remove(filename) + return nil, fmt.Errorf("Unable to write to file at %s: %s", filename, err) + } + file.Sync() + file.Close() + return newByteExec(filename) } // Command creates an exec.Cmd using the supplied args. func (be *ByteExec) Command(args ...string) *exec.Cmd { - return exec.Command(be.fileName, args...) + return exec.Command(be.filename, args...) } -// Close() closes the ByteExec, cleaning up the associated temp file. -func (be *ByteExec) Close() error { - if be.fileName == "" { - return nil - } else { - return os.Remove(be.fileName) +func newByteExec(filename string) (*ByteExec, error) { + absolutePath, err := filepath.Abs(filename) + if err != nil { + return nil, err } + return &ByteExec{filename: absolutePath}, nil } diff --git a/byteexec_test.go b/byteexec_test.go index c558b5a..bf523be 100644 --- a/byteexec_test.go +++ b/byteexec_test.go @@ -1,26 +1,90 @@ package byteexec import ( + "io/ioutil" + "os" + "sync" "testing" + "time" "github.com/getlantern/testify/assert" ) +const ( + program = "helloworld" + + concurrency = 10 +) + func TestExec(t *testing.T) { - bytes, err := Asset("helloworld") + tempDir, err := ioutil.TempDir("", "") + if err != nil { + t.Fatalf("Unable to create temp dir: %s", err) + } + defer os.RemoveAll(tempDir) + filename := tempDir + "/" + program + + data, err := Asset(program) if err != nil { t.Fatalf("Unable to read helloworld program: %s", err) } - be, err := NewByteExec(bytes) + be := createByteExec(t, data, filename) + + // Concurrently create some other BEs and make sure they don't get errors + var wg sync.WaitGroup + wg.Add(concurrency) + for i := 0; i < concurrency; i++ { + _, err := New(data, filename) + assert.NoError(t, err, "Concurrent New should have succeeded") + wg.Done() + } + wg.Wait() + + originalInfo := testByteExec(t, be) + + // Recreate be and make sure file is reused + be = createByteExec(t, data, filename) + updatedInfo := testByteExec(t, be) + assert.Equal(t, originalInfo.ModTime(), updatedInfo.ModTime(), "File modification time should be unchanged after creating new ByteExec") + + // Now mess with the file permissions and make sure that we can still run + err = os.Chmod(filename, 0655) + if err != nil { + t.Fatalf("Unable to chmod test executable %s: %s", filename, err) + } + be = createByteExec(t, data, filename) + updatedInfo = testByteExec(t, be) + assert.Equal(t, fileMode, updatedInfo.Mode(), "File mode is changed back to %v", fileMode) + + // Now mess with the file contents and make sure it gets overwritten on next + // ByteExec + ioutil.WriteFile(filename, []byte("Junk"), 0755) + be = createByteExec(t, data, filename) + updatedInfo = testByteExec(t, be) + assert.NotEqual(t, originalInfo.ModTime(), updatedInfo.ModTime(), "File modification time should be changed after creating new ByteExec on bad data") +} + +func createByteExec(t *testing.T, data []byte, filename string) *ByteExec { + // Sleep 1 second to give file timestamp a chance to increase + time.Sleep(1 * time.Second) + be, err := New(data, filename) if err != nil { t.Fatalf("Unable to create new ByteExec: %s", err) } - defer be.Close() + return be +} + +func testByteExec(t *testing.T, be *ByteExec) os.FileInfo { cmd := be.Command() out, err := cmd.CombinedOutput() if err != nil { t.Errorf("Unable to run helloworld program: %s", err) } + assert.Equal(t, "Hello world\n", string(out), "Should receive expected output from helloworld program") - assert.Equal(t, "Hello world\n", string(out), "Did not receive expected output from helloworld program") + fileInfo, err := os.Stat(be.filename) + if err != nil { + t.Fatalf("Unable to re-stat file %s: %s", be.filename, err) + } + return fileInfo } diff --git a/rename_linux.go b/rename_linux.go deleted file mode 100644 index 0053a8f..0000000 --- a/rename_linux.go +++ /dev/null @@ -1,5 +0,0 @@ -package byteexec - -func renameExecutable(orig string) string { - return orig -} diff --git a/rename_darwin.go b/rename_stub.go similarity index 79% rename from rename_darwin.go rename to rename_stub.go index 0053a8f..4de189a 100644 --- a/rename_darwin.go +++ b/rename_stub.go @@ -1,3 +1,5 @@ +// +build !windows + package byteexec func renameExecutable(orig string) string { From 3be53962d035a36c0ba4ec59c4c4f8e0acf8fd04 Mon Sep 17 00:00:00 2001 From: Ox Cart Date: Thu, 6 Nov 2014 06:38:25 -0600 Subject: [PATCH 02/10] Using checksums to compare contents rather than reading whole file from disk into memory --- byteexec.go | 25 ++++++++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/byteexec.go b/byteexec.go index bbcdb9e..de4d048 100644 --- a/byteexec.go +++ b/byteexec.go @@ -19,8 +19,9 @@ package byteexec import ( "bytes" + "crypto/sha256" "fmt" - "io/ioutil" + "io" "os" "os/exec" "path/filepath" @@ -65,8 +66,8 @@ func New(data []byte, filename string) (*ByteExec, error) { } log.Tracef("%s already exists, check to make sure contents is the same", filename) - dataOnDisk, err := ioutil.ReadFile(filename) - if err == nil && bytes.Equal(dataOnDisk, data) { + checksumErr := checksumsMatch(filename, data) + if checksumErr == nil { log.Tracef("Data in %s matches expected, using existing", filename) fi, err := os.Stat(filename) if err != nil || fi.Mode() != fileMode { @@ -100,6 +101,24 @@ func (be *ByteExec) Command(args ...string) *exec.Cmd { return exec.Command(be.filename, args...) } +func checksumsMatch(filename string, data []byte) error { + shasum := sha256.New() + file, err := os.OpenFile(filename, os.O_RDONLY, 0) + if err != nil { + return fmt.Errorf("Unable to open existing file at %s for reading: %s", filename, err) + } + _, err = io.Copy(shasum, file) + if err != nil { + return fmt.Errorf("Unable to read bytes to calculate sha sum: %s", err) + } + checksumOnDisk := shasum.Sum(nil) + expectedChecksum := sha256.Sum256(data) + if !bytes.Equal(checksumOnDisk, expectedChecksum[:]) { + return fmt.Errorf("Checksums don't match") + } + return nil +} + func newByteExec(filename string) (*ByteExec, error) { absolutePath, err := filepath.Abs(filename) if err != nil { From 7b1b9bc8b756dd693be73b1ad85dc4a35125c92a Mon Sep 17 00:00:00 2001 From: Ox Cart Date: Thu, 6 Nov 2014 06:40:56 -0600 Subject: [PATCH 03/10] Factored for better readability --- byteexec.go | 39 +++++++++++++++++++++------------------ 1 file changed, 21 insertions(+), 18 deletions(-) diff --git a/byteexec.go b/byteexec.go index de4d048..4dbf449 100644 --- a/byteexec.go +++ b/byteexec.go @@ -66,19 +66,10 @@ func New(data []byte, filename string) (*ByteExec, error) { } log.Tracef("%s already exists, check to make sure contents is the same", filename) - checksumErr := checksumsMatch(filename, data) - if checksumErr == nil { - log.Tracef("Data in %s matches expected, using existing", filename) - fi, err := os.Stat(filename) - if err != nil || fi.Mode() != fileMode { - log.Tracef("Chmodding %s", filename) - err = os.Chmod(filename, fileMode) - if err != nil { - return nil, fmt.Errorf("Unable to chmod file %s: %s", filename, err) - } - } - return newByteExec(filename) + if checksumsMatch(filename, data) { + return newByteExecFromExisting(filename) } + log.Tracef("Data in %s doesn't match expected, truncating file", filename) file, err = os.OpenFile(filename, os.O_RDWR|os.O_CREATE|os.O_TRUNC, fileMode) if err != nil { @@ -101,22 +92,34 @@ func (be *ByteExec) Command(args ...string) *exec.Cmd { return exec.Command(be.filename, args...) } -func checksumsMatch(filename string, data []byte) error { +func checksumsMatch(filename string, data []byte) bool { shasum := sha256.New() file, err := os.OpenFile(filename, os.O_RDONLY, 0) if err != nil { - return fmt.Errorf("Unable to open existing file at %s for reading: %s", filename, err) + log.Tracef("Unable to open existing file at %s for reading: %s", filename, err) + return false } _, err = io.Copy(shasum, file) if err != nil { - return fmt.Errorf("Unable to read bytes to calculate sha sum: %s", err) + log.Tracef("Unable to read bytes to calculate sha sum: %s", err) + return false } checksumOnDisk := shasum.Sum(nil) expectedChecksum := sha256.Sum256(data) - if !bytes.Equal(checksumOnDisk, expectedChecksum[:]) { - return fmt.Errorf("Checksums don't match") + return bytes.Equal(checksumOnDisk, expectedChecksum[:]) +} + +func newByteExecFromExisting(filename string) (*ByteExec, error) { + log.Tracef("Data in %s matches expected, using existing", filename) + fi, err := os.Stat(filename) + if err != nil || fi.Mode() != fileMode { + log.Tracef("Chmodding %s", filename) + err = os.Chmod(filename, fileMode) + if err != nil { + return nil, fmt.Errorf("Unable to chmod file %s: %s", filename, err) + } } - return nil + return newByteExec(filename) } func newByteExec(filename string) (*ByteExec, error) { From 039dd2b7ba51c43478071681945cd64caaf4ff34 Mon Sep 17 00:00:00 2001 From: Ox Cart Date: Thu, 6 Nov 2014 06:42:13 -0600 Subject: [PATCH 04/10] Factored for better readability --- byteexec.go | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/byteexec.go b/byteexec.go index 4dbf449..dc189d1 100644 --- a/byteexec.go +++ b/byteexec.go @@ -58,9 +58,7 @@ func New(data []byte, filename string) (*ByteExec, error) { log.Tracef("Renamed executable to %s for this platform", filename) file, err := os.OpenFile(filename, os.O_RDWR|os.O_CREATE|os.O_EXCL, fileMode) - if err == nil { - log.Tracef("Creating new file at %s", filename) - } else { + if err != nil { if !os.IsExist(err) { return nil, fmt.Errorf("Unexpected error opening %s: %s", filename, err) } @@ -77,6 +75,7 @@ func New(data []byte, filename string) (*ByteExec, error) { } } + log.Tracef("Creating new file at %s", filename) _, err = file.Write(data) if err != nil { os.Remove(filename) From b380550e135da6187d006774d7424c9721050938 Mon Sep 17 00:00:00 2001 From: Ox Cart Date: Thu, 6 Nov 2014 07:42:34 -0600 Subject: [PATCH 05/10] Updated comment --- byteexec.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/byteexec.go b/byteexec.go index dc189d1..4b040d6 100644 --- a/byteexec.go +++ b/byteexec.go @@ -45,7 +45,9 @@ type ByteExec struct { } // New creates a new ByteExec using the program stored in the provided data, at -// the provided filename (relative or absolute path allowed). +// the provided filename (relative or absolute path allowed). This can be a +// somewhat expensive, so it's best to create only one ByteExec per executable +// and reuse that. // // WARNING - if a file already exists at this location and its contents differ // from data, byteexec will attempt to overwrite it. From f7146dc6400e33931f279303b8326de1c72ff552 Mon Sep 17 00:00:00 2001 From: Ox Cart Date: Thu, 6 Nov 2014 08:25:04 -0600 Subject: [PATCH 06/10] Renamed ByteExec to just Exec --- byteexec.go | 32 ++++++++++++++++---------------- byteexec_test.go | 4 ++-- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/byteexec.go b/byteexec.go index 4b040d6..8c18c3a 100644 --- a/byteexec.go +++ b/byteexec.go @@ -1,13 +1,13 @@ -// Package byteexec provides a very basic facility for running executables +// Package Exec provides a very basic facility for running executables // supplied as byte arrays, which is handy when used with // github.com/jteeuwen/go-bindata. // -// ByteExec works by storing the provided command in a file. +// Exec works by storing the provided command in a file. // // Example Usage: // // programBytes := // read bytes from somewhere -// be, err := NewByteExec(programBytes) +// be, err := NewExec(programBytes) // if err != nil { // log.Fatalf("Uh oh: %s", err) // } @@ -35,23 +35,23 @@ const ( ) var ( - log = golog.LoggerFor("byteexec") + log = golog.LoggerFor("Exec") initMutex sync.Mutex ) -type ByteExec struct { +type Exec struct { filename string } -// New creates a new ByteExec using the program stored in the provided data, at +// New creates a new Exec using the program stored in the provided data, at // the provided filename (relative or absolute path allowed). This can be a -// somewhat expensive, so it's best to create only one ByteExec per executable +// somewhat expensive, so it's best to create only one Exec per executable // and reuse that. // // WARNING - if a file already exists at this location and its contents differ -// from data, byteexec will attempt to overwrite it. -func New(data []byte, filename string) (*ByteExec, error) { +// from data, Exec will attempt to overwrite it. +func New(data []byte, filename string) (*Exec, error) { // Use initMutex to synchronize file operations by this process initMutex.Lock() defer initMutex.Unlock() @@ -67,7 +67,7 @@ func New(data []byte, filename string) (*ByteExec, error) { log.Tracef("%s already exists, check to make sure contents is the same", filename) if checksumsMatch(filename, data) { - return newByteExecFromExisting(filename) + return newExecFromExisting(filename) } log.Tracef("Data in %s doesn't match expected, truncating file", filename) @@ -85,11 +85,11 @@ func New(data []byte, filename string) (*ByteExec, error) { } file.Sync() file.Close() - return newByteExec(filename) + return newExec(filename) } // Command creates an exec.Cmd using the supplied args. -func (be *ByteExec) Command(args ...string) *exec.Cmd { +func (be *Exec) Command(args ...string) *exec.Cmd { return exec.Command(be.filename, args...) } @@ -110,7 +110,7 @@ func checksumsMatch(filename string, data []byte) bool { return bytes.Equal(checksumOnDisk, expectedChecksum[:]) } -func newByteExecFromExisting(filename string) (*ByteExec, error) { +func newExecFromExisting(filename string) (*Exec, error) { log.Tracef("Data in %s matches expected, using existing", filename) fi, err := os.Stat(filename) if err != nil || fi.Mode() != fileMode { @@ -120,13 +120,13 @@ func newByteExecFromExisting(filename string) (*ByteExec, error) { return nil, fmt.Errorf("Unable to chmod file %s: %s", filename, err) } } - return newByteExec(filename) + return newExec(filename) } -func newByteExec(filename string) (*ByteExec, error) { +func newExec(filename string) (*Exec, error) { absolutePath, err := filepath.Abs(filename) if err != nil { return nil, err } - return &ByteExec{filename: absolutePath}, nil + return &Exec{filename: absolutePath}, nil } diff --git a/byteexec_test.go b/byteexec_test.go index bf523be..e89fb59 100644 --- a/byteexec_test.go +++ b/byteexec_test.go @@ -64,7 +64,7 @@ func TestExec(t *testing.T) { assert.NotEqual(t, originalInfo.ModTime(), updatedInfo.ModTime(), "File modification time should be changed after creating new ByteExec on bad data") } -func createByteExec(t *testing.T, data []byte, filename string) *ByteExec { +func createByteExec(t *testing.T, data []byte, filename string) *Exec { // Sleep 1 second to give file timestamp a chance to increase time.Sleep(1 * time.Second) be, err := New(data, filename) @@ -74,7 +74,7 @@ func createByteExec(t *testing.T, data []byte, filename string) *ByteExec { return be } -func testByteExec(t *testing.T, be *ByteExec) os.FileInfo { +func testByteExec(t *testing.T, be *Exec) os.FileInfo { cmd := be.Command() out, err := cmd.CombinedOutput() if err != nil { From 7675b205a4db7120e32cb9b32bbc4c0bfa01294a Mon Sep 17 00:00:00 2001 From: Ox Cart Date: Thu, 6 Nov 2014 08:30:12 -0600 Subject: [PATCH 07/10] Comment updates --- byteexec.go | 19 +++++++++++-------- byteexec_test.go | 1 + 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/byteexec.go b/byteexec.go index 8c18c3a..ce3072f 100644 --- a/byteexec.go +++ b/byteexec.go @@ -1,20 +1,21 @@ -// Package Exec provides a very basic facility for running executables +// Package byteexec provides a very basic facility for running executables // supplied as byte arrays, which is handy when used with // github.com/jteeuwen/go-bindata. // -// Exec works by storing the provided command in a file. +// byteexec works by storing the provided command in a file. // // Example Usage: // // programBytes := // read bytes from somewhere -// be, err := NewExec(programBytes) +// be, err := byteexec.New(programBytes) // if err != nil { // log.Fatalf("Uh oh: %s", err) // } -// defer be.Close() // cmd := be.Command("arg1", "arg2") // // cmd is an os/exec.Cmd // err = cmd.Run() +// +// Note - byteexec.New is somewhat expensive, package byteexec import ( @@ -40,14 +41,16 @@ var ( initMutex sync.Mutex ) +// Exec is a handle to an executable that can be used to create an exec.Cmd +// using the Command method. Exec is safe for concurrent use. type Exec struct { filename string } -// New creates a new Exec using the program stored in the provided data, at -// the provided filename (relative or absolute path allowed). This can be a -// somewhat expensive, so it's best to create only one Exec per executable -// and reuse that. +// New creates a new Exec using the program stored in the provided data, at the +// provided filename (relative or absolute path allowed). This can be a somewhat +// expensive, so it's best to create only one Exec per executable and reuse +// that. // // WARNING - if a file already exists at this location and its contents differ // from data, Exec will attempt to overwrite it. diff --git a/byteexec_test.go b/byteexec_test.go index e89fb59..3b19a36 100644 --- a/byteexec_test.go +++ b/byteexec_test.go @@ -67,6 +67,7 @@ func TestExec(t *testing.T) { func createByteExec(t *testing.T, data []byte, filename string) *Exec { // Sleep 1 second to give file timestamp a chance to increase time.Sleep(1 * time.Second) + be, err := New(data, filename) if err != nil { t.Fatalf("Unable to create new ByteExec: %s", err) From a9cc0820c61690c0ead79630be08cddfc89b93a7 Mon Sep 17 00:00:00 2001 From: Ox Cart Date: Thu, 6 Nov 2014 08:31:04 -0600 Subject: [PATCH 08/10] Comment updates --- byteexec.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/byteexec.go b/byteexec.go index ce3072f..6f3a371 100644 --- a/byteexec.go +++ b/byteexec.go @@ -15,7 +15,8 @@ // // cmd is an os/exec.Cmd // err = cmd.Run() // -// Note - byteexec.New is somewhat expensive, +// Note - byteexec.New is somewhat expensive, and Exec is safe for concurrent +// use, so it's advisable to create only one Exec for each executable. package byteexec import ( From 7285503c3af57bb67033661253d2ac0017b8ab00 Mon Sep 17 00:00:00 2001 From: Ox Cart Date: Thu, 6 Nov 2014 08:33:27 -0600 Subject: [PATCH 09/10] Comment updates --- byteexec.go | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/byteexec.go b/byteexec.go index 6f3a371..ad738b9 100644 --- a/byteexec.go +++ b/byteexec.go @@ -63,6 +63,7 @@ func New(data []byte, filename string) (*Exec, error) { filename = renameExecutable(filename) log.Tracef("Renamed executable to %s for this platform", filename) + log.Trace("Attempting to open file for creating, but only if it doesn't already exist") file, err := os.OpenFile(filename, os.O_RDWR|os.O_CREATE|os.O_EXCL, fileMode) if err != nil { if !os.IsExist(err) { @@ -71,6 +72,7 @@ func New(data []byte, filename string) (*Exec, error) { log.Tracef("%s already exists, check to make sure contents is the same", filename) if checksumsMatch(filename, data) { + log.Tracef("Data in %s matches expected, using existing", filename) return newExecFromExisting(filename) } @@ -81,7 +83,7 @@ func New(data []byte, filename string) (*Exec, error) { } } - log.Tracef("Creating new file at %s", filename) + log.Tracef("Created new file at %s, saving executable", filename) _, err = file.Write(data) if err != nil { os.Remove(filename) @@ -89,6 +91,8 @@ func New(data []byte, filename string) (*Exec, error) { } file.Sync() file.Close() + + log.Trace("File saved, returning new Exec") return newExec(filename) } @@ -115,7 +119,6 @@ func checksumsMatch(filename string, data []byte) bool { } func newExecFromExisting(filename string) (*Exec, error) { - log.Tracef("Data in %s matches expected, using existing", filename) fi, err := os.Stat(filename) if err != nil || fi.Mode() != fileMode { log.Tracef("Chmodding %s", filename) From 72d1d5f38bc7b6241d7bd7ab6c6b5f6b85d36bc6 Mon Sep 17 00:00:00 2001 From: Ox Cart Date: Thu, 6 Nov 2014 12:15:59 -0600 Subject: [PATCH 10/10] Placing relative executables in ~/.byteexec --- byteexec.go | 36 +++++++++++++++++++++++++++++++----- byteexec_test.go | 27 ++++++++++----------------- 2 files changed, 41 insertions(+), 22 deletions(-) diff --git a/byteexec.go b/byteexec.go index ad738b9..64ac7b0 100644 --- a/byteexec.go +++ b/byteexec.go @@ -26,6 +26,8 @@ import ( "io" "os" "os/exec" + "os/user" + "path" "path/filepath" "sync" @@ -33,7 +35,7 @@ import ( ) const ( - fileMode = 0755 + fileMode = 0744 ) var ( @@ -49,9 +51,12 @@ type Exec struct { } // New creates a new Exec using the program stored in the provided data, at the -// provided filename (relative or absolute path allowed). This can be a somewhat -// expensive, so it's best to create only one Exec per executable and reuse -// that. +// provided filename (relative or absolute path allowed). If the path given is +// a relative path, the executable will be placed in the user's home directory +// in a subfolder named ".byteexec". +// +// Creating a new Exec can be somewhat expensive, so it's best to create only +// one Exec per executable and reuse that. // // WARNING - if a file already exists at this location and its contents differ // from data, Exec will attempt to overwrite it. @@ -60,8 +65,15 @@ func New(data []byte, filename string) (*Exec, error) { initMutex.Lock() defer initMutex.Unlock() + var err error + if !path.IsAbs(filename) { + filename, err = inUserDir(filename) + if err != nil { + return nil, err + } + } filename = renameExecutable(filename) - log.Tracef("Renamed executable to %s for this platform", filename) + log.Tracef("Placing executable in %s", filename) log.Trace("Attempting to open file for creating, but only if it doesn't already exist") file, err := os.OpenFile(filename, os.O_RDWR|os.O_CREATE|os.O_EXCL, fileMode) @@ -137,3 +149,17 @@ func newExec(filename string) (*Exec, error) { } return &Exec{filename: absolutePath}, nil } + +func inUserDir(filename string) (string, error) { + log.Tracef("Determining user's home directory") + usr, err := user.Current() + if err != nil { + return filename, fmt.Errorf("Unable to determine user's home directory: %s", err) + } + folder := path.Join(usr.HomeDir, ".byteexec") + err = os.MkdirAll(folder, fileMode) + if err != nil { + return filename, fmt.Errorf("Unable to make folder %s: %s", folder, err) + } + return path.Join(folder, filename), nil +} diff --git a/byteexec_test.go b/byteexec_test.go index 3b19a36..a535fdf 100644 --- a/byteexec_test.go +++ b/byteexec_test.go @@ -17,24 +17,17 @@ const ( ) func TestExec(t *testing.T) { - tempDir, err := ioutil.TempDir("", "") - if err != nil { - t.Fatalf("Unable to create temp dir: %s", err) - } - defer os.RemoveAll(tempDir) - filename := tempDir + "/" + program - data, err := Asset(program) if err != nil { t.Fatalf("Unable to read helloworld program: %s", err) } - be := createByteExec(t, data, filename) + be := createByteExec(t, data) // Concurrently create some other BEs and make sure they don't get errors var wg sync.WaitGroup wg.Add(concurrency) for i := 0; i < concurrency; i++ { - _, err := New(data, filename) + _, err := New(data, program) assert.NoError(t, err, "Concurrent New should have succeeded") wg.Done() } @@ -43,32 +36,32 @@ func TestExec(t *testing.T) { originalInfo := testByteExec(t, be) // Recreate be and make sure file is reused - be = createByteExec(t, data, filename) + be = createByteExec(t, data) updatedInfo := testByteExec(t, be) assert.Equal(t, originalInfo.ModTime(), updatedInfo.ModTime(), "File modification time should be unchanged after creating new ByteExec") // Now mess with the file permissions and make sure that we can still run - err = os.Chmod(filename, 0655) + err = os.Chmod(be.filename, 0655) if err != nil { - t.Fatalf("Unable to chmod test executable %s: %s", filename, err) + t.Fatalf("Unable to chmod test executable %s: %s", be.filename, err) } - be = createByteExec(t, data, filename) + be = createByteExec(t, data) updatedInfo = testByteExec(t, be) assert.Equal(t, fileMode, updatedInfo.Mode(), "File mode is changed back to %v", fileMode) // Now mess with the file contents and make sure it gets overwritten on next // ByteExec - ioutil.WriteFile(filename, []byte("Junk"), 0755) - be = createByteExec(t, data, filename) + ioutil.WriteFile(be.filename, []byte("Junk"), 0755) + be = createByteExec(t, data) updatedInfo = testByteExec(t, be) assert.NotEqual(t, originalInfo.ModTime(), updatedInfo.ModTime(), "File modification time should be changed after creating new ByteExec on bad data") } -func createByteExec(t *testing.T, data []byte, filename string) *Exec { +func createByteExec(t *testing.T, data []byte) *Exec { // Sleep 1 second to give file timestamp a chance to increase time.Sleep(1 * time.Second) - be, err := New(data, filename) + be, err := New(data, program) if err != nil { t.Fatalf("Unable to create new ByteExec: %s", err) }