From 0ba0af889e3a71d6d5aeb4dd10279d5027d3d8cd Mon Sep 17 00:00:00 2001 From: Jesse Hallam Date: Wed, 6 Jun 2018 10:18:24 -0400 Subject: [PATCH] MM-6839: searching for paths relative to executable (#8915) * MM-6839: search relative to executable (#8853) * MM-6839: searching for paths relative to executable In addition to searching relative to the current working directory, also search relative to the location of the binary. This helps locate config and i18n files when invoking an absolute path to the mattermost binary. * MM-6839: find mattermost/ binary using utils.FindFile * add unit tests for utils.FindFile to exclude directories * fix filtering out directories in FindFile * fix platform invoking ./bin/mattermost --- cmd/platform/main.go | 25 ++-- utils/config.go | 102 ++++++++++++---- utils/config_test.go | 284 +++++++++++++++++++++++++++++++++++++++++-- 3 files changed, 366 insertions(+), 45 deletions(-) diff --git a/cmd/platform/main.go b/cmd/platform/main.go index b5ea51920d..25e091a84f 100644 --- a/cmd/platform/main.go +++ b/cmd/platform/main.go @@ -6,19 +6,10 @@ package main import ( "fmt" "os" - "path/filepath" "syscall" -) -func findMattermostBinary() string { - for _, file := range []string{"./mattermost", "../mattermost", "./bin/mattermost"} { - path, _ := filepath.Abs(file) - if stat, err := os.Stat(path); err == nil && !stat.IsDir() { - return path - } - } - return "./mattermost" -} + "github.com/mattermost/mattermost-server/utils" +) func main() { // Print angry message to use mattermost command directly @@ -33,7 +24,15 @@ The platform binary will be removed in a future version. args := os.Args args[0] = "mattermost" args = append(args, "--platform") - if err := syscall.Exec(findMattermostBinary(), args, nil); err != nil { - fmt.Println("Could not start Mattermost, use the mattermost command directly.") + + realMattermost := utils.FindFile("mattermost") + if realMattermost == "" { + realMattermost = utils.FindFile("bin/mattermost") + } + + if realMattermost == "" { + fmt.Println("Could not start Mattermost, use the mattermost command directly: failed to find mattermost") + } else if err := syscall.Exec(realMattermost, args, nil); err != nil { + fmt.Printf("Could not start Mattermost, use the mattermost command directly: %s\n", err.Error()) } } diff --git a/utils/config.go b/utils/config.go index 64085fcffe..2fb6e689f8 100644 --- a/utils/config.go +++ b/utils/config.go @@ -32,36 +32,96 @@ const ( LOG_FILENAME = "mattermost.log" ) +var ( + commonBaseSearchPaths = []string{ + ".", + "..", + "../..", + "../../..", + } +) + +func FindPath(path string, baseSearchPaths []string, filter func(os.FileInfo) bool) string { + if filepath.IsAbs(path) { + if _, err := os.Stat(path); err == nil { + return path + } + + return "" + } + + searchPaths := []string{} + for _, baseSearchPath := range baseSearchPaths { + searchPaths = append(searchPaths, baseSearchPath) + } + + // Additionally attempt to search relative to the location of the running binary. + var binaryDir string + if exe, err := os.Executable(); err == nil { + if exe, err = filepath.EvalSymlinks(exe); err == nil { + if exe, err = filepath.Abs(exe); err == nil { + binaryDir = filepath.Dir(exe) + } + } + } + if binaryDir != "" { + for _, baseSearchPath := range baseSearchPaths { + searchPaths = append( + searchPaths, + filepath.Join(binaryDir, baseSearchPath), + ) + } + } + + for _, parent := range searchPaths { + found, err := filepath.Abs(filepath.Join(parent, path)) + if err != nil { + continue + } else if fileInfo, err := os.Stat(found); err == nil { + if filter != nil { + if filter(fileInfo) { + return found + } + } else { + return found + } + } + } + + return "" +} + // FindConfigFile attempts to find an existing configuration file. fileName can be an absolute or // relative path or name such as "/opt/mattermost/config.json" or simply "config.json". An empty // string is returned if no configuration is found. func FindConfigFile(fileName string) (path string) { - if filepath.IsAbs(fileName) { - if _, err := os.Stat(fileName); err == nil { - return fileName - } - } else { - for _, dir := range []string{"./config", "../config", "../../config", "../../../config", "."} { - path, _ := filepath.Abs(filepath.Join(dir, fileName)) - if _, err := os.Stat(path); err == nil { - return path - } - } + found := FindFile(filepath.Join("config", fileName)) + if found == "" { + found = FindPath(fileName, []string{"."}, nil) } - return "" + + return found } -// FindDir looks for the given directory in nearby ancestors, falling back to `./` if not found. +// FindFile looks for the given file in nearby ancestors relative to the current working +// directory as well as the directory of the executable. +func FindFile(path string) string { + return FindPath(path, commonBaseSearchPaths, func(fileInfo os.FileInfo) bool { + return !fileInfo.IsDir() + }) +} + +// FindDir looks for the given directory in nearby ancestors relative to the current working +// directory as well as the directory of the executable, falling back to `./` if not found. func FindDir(dir string) (string, bool) { - for _, parent := range []string{".", "..", "../..", "../../.."} { - foundDir, err := filepath.Abs(filepath.Join(parent, dir)) - if err != nil { - continue - } else if _, err := os.Stat(foundDir); err == nil { - return foundDir, true - } + found := FindPath(dir, commonBaseSearchPaths, func(fileInfo os.FileInfo) bool { + return fileInfo.IsDir() + }) + if found == "" { + return "./", false } - return "./", false + + return found, true } func MloggerConfigFromLoggerConfig(s *model.LogSettings) *mlog.LoggerConfiguration { diff --git a/utils/config_test.go b/utils/config_test.go index 75bbc420f5..63b2835847 100644 --- a/utils/config_test.go +++ b/utils/config_test.go @@ -5,6 +5,7 @@ package utils import ( "bytes" + "fmt" "io/ioutil" "os" "path/filepath" @@ -46,20 +47,281 @@ func TestTimezoneConfig(t *testing.T) { } func TestFindConfigFile(t *testing.T) { - dir, err := ioutil.TempDir("", "") - require.NoError(t, err) - defer os.RemoveAll(dir) + t.Run("config.json in current working directory, not inside config/", func(t *testing.T) { + // Force a unique working directory + cwd, err := ioutil.TempDir("", "") + require.NoError(t, err) + defer os.RemoveAll(cwd) - path := filepath.Join(dir, "config.json") - require.NoError(t, ioutil.WriteFile(path, []byte("{}"), 0600)) + prevDir, err := os.Getwd() + require.NoError(t, err) + defer os.Chdir(prevDir) + os.Chdir(cwd) - assert.Equal(t, path, FindConfigFile(path)) + configJson, err := filepath.Abs("config.json") + require.NoError(t, err) + require.NoError(t, ioutil.WriteFile(configJson, []byte("{}"), 0600)) - prevDir, err := os.Getwd() - require.NoError(t, err) - defer os.Chdir(prevDir) - os.Chdir(dir) - assert.Equal(t, path, FindConfigFile(path)) + // Relative paths end up getting symlinks fully resolved. + configJsonResolved, err := filepath.EvalSymlinks(configJson) + require.NoError(t, err) + + assert.Equal(t, configJsonResolved, FindConfigFile("config.json")) + }) + + t.Run("config/config.json from various paths", func(t *testing.T) { + // Create the following directory structure: + // tmpDir1/ + // config/ + // config.json + // tmpDir2/ + // tmpDir3/ + // tmpDir4/ + // tmpDir5/ + tmpDir1, err := ioutil.TempDir("", "") + require.NoError(t, err) + defer os.RemoveAll(tmpDir1) + + err = os.Mkdir(filepath.Join(tmpDir1, "config"), 0700) + require.NoError(t, err) + + tmpDir2, err := ioutil.TempDir(tmpDir1, "") + require.NoError(t, err) + + tmpDir3, err := ioutil.TempDir(tmpDir2, "") + require.NoError(t, err) + + tmpDir4, err := ioutil.TempDir(tmpDir3, "") + require.NoError(t, err) + + tmpDir5, err := ioutil.TempDir(tmpDir4, "") + require.NoError(t, err) + + configJson := filepath.Join(tmpDir1, "config", "config.json") + require.NoError(t, ioutil.WriteFile(configJson, []byte("{}"), 0600)) + + // Relative paths end up getting symlinks fully resolved, so use this below as necessary. + configJsonResolved, err := filepath.EvalSymlinks(configJson) + require.NoError(t, err) + + testCases := []struct { + Description string + Cwd *string + FileName string + Expected string + }{ + { + "absolute path to config.json", + nil, + configJson, + configJson, + }, + { + "absolute path to config.json from directory containing config.json", + &tmpDir1, + configJson, + configJson, + }, + { + "relative path to config.json from directory containing config.json", + &tmpDir1, + "config.json", + configJsonResolved, + }, + { + "subdirectory of directory containing config.json", + &tmpDir2, + "config.json", + configJsonResolved, + }, + { + "twice-nested subdirectory of directory containing config.json", + &tmpDir3, + "config.json", + configJsonResolved, + }, + { + "thrice-nested subdirectory of directory containing config.json", + &tmpDir4, + "config.json", + configJsonResolved, + }, + { + "can't find from four nesting levels deep", + &tmpDir5, + "config.json", + "", + }, + } + + for _, testCase := range testCases { + t.Run(testCase.Description, func(t *testing.T) { + if testCase.Cwd != nil { + prevDir, err := os.Getwd() + require.NoError(t, err) + defer os.Chdir(prevDir) + os.Chdir(*testCase.Cwd) + } + + assert.Equal(t, testCase.Expected, FindConfigFile(testCase.FileName)) + }) + } + }) + + t.Run("config/config.json relative to executable", func(t *testing.T) { + osExecutable, err := os.Executable() + require.NoError(t, err) + osExecutableDir := filepath.Dir(osExecutable) + + // Force a working directory different than the executable. + cwd, err := ioutil.TempDir("", "") + require.NoError(t, err) + defer os.RemoveAll(cwd) + + prevDir, err := os.Getwd() + require.NoError(t, err) + defer os.Chdir(prevDir) + os.Chdir(cwd) + + testCases := []struct { + Description string + RelativePath string + }{ + { + "config/config.json", + ".", + }, + { + "../config/config.json", + "../", + }, + } + + for _, testCase := range testCases { + t.Run(testCase.Description, func(t *testing.T) { + // Install the config in config/config.json relative to the executable + configJson := filepath.Join(osExecutableDir, testCase.RelativePath, "config", "config.json") + require.NoError(t, os.Mkdir(filepath.Dir(configJson), 0700)) + require.NoError(t, ioutil.WriteFile(configJson, []byte("{}"), 0600)) + defer os.RemoveAll(filepath.Dir(configJson)) + + // Relative paths end up getting symlinks fully resolved. + configJsonResolved, err := filepath.EvalSymlinks(configJson) + require.NoError(t, err) + + assert.Equal(t, configJsonResolved, FindConfigFile("config.json")) + }) + } + }) +} + +func TestFindFile(t *testing.T) { + t.Run("files from various paths", func(t *testing.T) { + // Create the following directory structure: + // tmpDir1/ + // file1.json + // file2.xml + // other.txt + // tmpDir2/ + // other.txt/ [directory] + // tmpDir3/ + // tmpDir4/ + // tmpDir5/ + tmpDir1, err := ioutil.TempDir("", "") + require.NoError(t, err) + defer os.RemoveAll(tmpDir1) + + tmpDir2, err := ioutil.TempDir(tmpDir1, "") + require.NoError(t, err) + + err = os.Mkdir(filepath.Join(tmpDir2, "other.txt"), 0700) + require.NoError(t, err) + + tmpDir3, err := ioutil.TempDir(tmpDir2, "") + require.NoError(t, err) + + tmpDir4, err := ioutil.TempDir(tmpDir3, "") + require.NoError(t, err) + + tmpDir5, err := ioutil.TempDir(tmpDir4, "") + require.NoError(t, err) + + type testCase struct { + Description string + Cwd *string + FileName string + Expected string + } + + testCases := []testCase{} + + for _, fileName := range []string{"file1.json", "file2.xml", "other.txt"} { + filePath := filepath.Join(tmpDir1, fileName) + require.NoError(t, ioutil.WriteFile(filePath, []byte("{}"), 0600)) + + // Relative paths end up getting symlinks fully resolved, so use this below as necessary. + filePathResolved, err := filepath.EvalSymlinks(filePath) + require.NoError(t, err) + + testCases = append(testCases, []testCase{ + { + fmt.Sprintf("absolute path to %s", fileName), + nil, + filePath, + filePath, + }, + { + fmt.Sprintf("absolute path to %s from containing directory", fileName), + &tmpDir1, + filePath, + filePath, + }, + { + fmt.Sprintf("relative path to %s from containing directory", fileName), + &tmpDir1, + fileName, + filePathResolved, + }, + { + fmt.Sprintf("%s: subdirectory of containing directory", fileName), + &tmpDir2, + fileName, + filePathResolved, + }, + { + fmt.Sprintf("%s: twice-nested subdirectory of containing directory", fileName), + &tmpDir3, + fileName, + filePathResolved, + }, + { + fmt.Sprintf("%s: thrice-nested subdirectory of containing directory", fileName), + &tmpDir4, + fileName, + filePathResolved, + }, + { + fmt.Sprintf("%s: can't find from four nesting levels deep", fileName), + &tmpDir5, + fileName, + "", + }, + }...) + } + + for _, testCase := range testCases { + t.Run(testCase.Description, func(t *testing.T) { + if testCase.Cwd != nil { + prevDir, err := os.Getwd() + require.NoError(t, err) + defer os.Chdir(prevDir) + os.Chdir(*testCase.Cwd) + } + + assert.Equal(t, testCase.Expected, FindFile(testCase.FileName)) + }) + } + }) } func TestConfigFromEnviroVars(t *testing.T) {