Commit d3cf1106 authored by Jesse Hallam's avatar Jesse Hallam Committed by Harrison Healey

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
parent 4f5fdb6f
......@@ -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,14 @@ 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 {
realMattermost := utils.FindFile("mattermost")
if realMattermost == "" {
// This will still fail, of course.
realMattermost = "./mattermost"
}
if err := syscall.Exec(utils.FindFile("mattermost"), args, nil); err != nil {
fmt.Println("Could not start Mattermost, use the mattermost command directly.")
}
}
......@@ -32,36 +32,94 @@ const (
LOG_FILENAME = "mattermost.log"
)
// 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
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
}
} 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
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)
}
}
}
return ""
}
if binaryDir != "" {
for _, baseSearchPath := range baseSearchPaths {
searchPaths = append(
searchPaths,
filepath.Join(binaryDir, baseSearchPath),
)
}
}
// FindDir looks for the given directory in nearby ancestors, falling back to `./` if not found.
func FindDir(dir string) (string, bool) {
for _, parent := range []string{".", "..", "../..", "../../.."} {
foundDir, err := filepath.Abs(filepath.Join(parent, dir))
for _, parent := range searchPaths {
found, err := filepath.Abs(filepath.Join(parent, path))
if err != nil {
continue
} else if _, err := os.Stat(foundDir); err == nil {
return foundDir, true
} else if fileInfo, err := os.Stat(found); err == nil {
if filter != nil && filter(fileInfo) {
return found
} else {
return found
}
}
}
return "./", false
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) {
found := FindFile(filepath.Join("config", fileName))
if found == "" {
found = FindPath(fileName, []string{"."}, nil)
}
return 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) {
found := FindPath(dir, commonBaseSearchPaths, func(fileInfo os.FileInfo) bool {
return fileInfo.IsDir()
})
if found == "" {
return "./", false
}
return found, true
}
func MloggerConfigFromLoggerConfig(s *model.LogSettings) *mlog.LoggerConfiguration {
......
......@@ -46,20 +46,172 @@ 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)
prevDir, err := os.Getwd()
require.NoError(t, err)
defer os.Chdir(prevDir)
os.Chdir(cwd)
configJson, err := filepath.Abs("config.json")
require.NoError(t, err)
require.NoError(t, ioutil.WriteFile(configJson, []byte("{}"), 0600))
// Relative paths end up getting symlinks fully resolved.
configJsonResolved, err := filepath.EvalSymlinks(configJson)
require.NoError(t, err)
assert.Equal(t, configJsonResolved, FindConfigFile("config.json"))
})
path := filepath.Join(dir, "config.json")
require.NoError(t, ioutil.WriteFile(path, []byte("{}"), 0600))
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",
"",
},
}
assert.Equal(t, path, FindConfigFile(path))
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",
"../",
},
}
prevDir, err := os.Getwd()
require.NoError(t, err)
defer os.Chdir(prevDir)
os.Chdir(dir)
assert.Equal(t, path, FindConfigFile(path))
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 TestConfigFromEnviroVars(t *testing.T) {
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment