From e6d89f7a79774b6e36c79f13dc735d7af9216dbd Mon Sep 17 00:00:00 2001 From: Nikhil Saraf <1028334+nikhilsaraf@users.noreply.github.com> Date: Thu, 9 Apr 2020 03:02:19 +0530 Subject: [PATCH] Kelp UI: Fix filepaths for windows by introducing the kelpos.OsPath struct (#400) * 1 - add OSPath struct with test * 2 - cleaner interface and factory structure for ospath * 3a - partial replacement of paths to use ospath * 3b - more partial replacement of paths to use ospath * 3c - some more partial replacement of paths to use ospath * 3d - fix compile errors in ospath refactoring * 4 - update configsDir usage in gui/backend * 5 - fix start_bot.go * 6 - remove support for OSPath.String() because of ambiguity * 7 - change panic in ospath.String() to log.Fatal call * 8 - fix log calls to ospath by replacing Stringer function with AsString() * 9 - added support for relative paths to ospath + test * 10 - start_bot.go uses relative paths to start off bot in the gui * 11 - added comment on choice to use relative paths when starting bots from the GUI --- cmd/server_amd64.go | 169 +++++++++++---------------- gui/backend/api_server.go | 34 +++--- gui/backend/autogenerate_bot.go | 12 +- gui/backend/delete_bot.go | 3 +- gui/backend/generate_bot_name.go | 2 +- gui/backend/get_bot_config.go | 8 +- gui/backend/get_bot_info.go | 4 +- gui/backend/list_bots.go | 2 +- gui/backend/start_bot.go | 28 ++++- gui/backend/upsert_bot_config.go | 12 +- scripts/ccxt_bin_gen/ccxt_bin_gen.go | 4 +- support/kelpos/ospath.go | 131 +++++++++++++++++++++ support/kelpos/ospath_test.go | 68 +++++++++++ support/kelpos/process.go | 4 +- 14 files changed, 334 insertions(+), 147 deletions(-) create mode 100644 support/kelpos/ospath.go create mode 100644 support/kelpos/ospath_test.go diff --git a/cmd/server_amd64.go b/cmd/server_amd64.go index 0fd89a091..e3b4a6fc1 100644 --- a/cmd/server_amd64.go +++ b/cmd/server_amd64.go @@ -35,7 +35,6 @@ import ( "github.com/stellar/kelp/support/networking" "github.com/stellar/kelp/support/prefs" "github.com/stellar/kelp/support/sdk" - "github.com/stellar/kelp/support/utils" ) const kelpPrefsDirectory = ".kelp" @@ -80,44 +79,33 @@ func init() { options.noElectron = serverCmd.Flags().Bool("no-electron", false, "open in browser instead of using electron") serverCmd.Run = func(ccmd *cobra.Command, args []string) { - currentDirUnslashed, e := getCurrentDirUnix() + basepath, e := kelpos.MakeOsPathBase() if e != nil { - panic(errors.Wrap(e, "could not get current directory")) - } - currentDirUnix := toUnixFilepath(currentDirUnslashed) - log.Printf("currentDirUnix: %s", currentDirUnix) - - binaryDirectoryNative, e := getBinaryDirectoryNative() - if e != nil { - panic(errors.Wrap(e, "could not get binary directory")) - } - log.Printf("binaryDirectoryNative: %s", binaryDirectoryNative) - - if filepath.Base(currentDirUnix) != filepath.Base(binaryDirectoryNative) { - utils.PrintErrorHintf("need to run from the same directory as the location of the binary, cd over to the binary directory : %s", binaryDirectoryNative) - return + panic(errors.Wrap(e, "could not make ospath")) } + log.Printf("ospath initialized with native path: %s", basepath.Native()) + log.Printf("ospath initialized with unix path: %s", basepath.Unix()) isLocalMode := env == envDev isLocalDevMode := isLocalMode && *options.dev kos := kelpos.GetKelpOS() - logFilepathNative := "" + var logFilepath *kelpos.OSPath if !isLocalDevMode { l := logger.MakeBasicLogger() t := time.Now().Format("20060102T150405MST") logFilename := fmt.Sprintf("kelp-ui_%s.log", t) - logsDirPathUnix := toUnixFilepath(filepath.Join(currentDirUnix, kelpPrefsDirectory, logsDir)) - log.Printf("making logsDirPathUnix: %s ...", logsDirPathUnix) - e = kos.Mkdir(logsDirPathUnix) + logsDirPath := basepath.Join(kelpPrefsDirectory, logsDir) + log.Printf("calling mkdir on logsDirPath: %s ...", logsDirPath.AsString()) + e = kos.Mkdir(logsDirPath) if e != nil { - panic(errors.Wrap(e, "could not make directories for logsDirPathUnix: "+logsDirPathUnix)) + panic(errors.Wrap(e, "could not mkdir on logsDirPath: "+logsDirPath.AsString())) } // don't use explicit unix filepath here since it uses os.Open directly and won't work on windows - logFilepathNative = filepath.Join(binaryDirectoryNative, kelpPrefsDirectory, logsDir, logFilename) - setLogFile(l, logFilepathNative) + logFilepath = basepath.Join(kelpPrefsDirectory, logsDir, logFilename) + setLogFile(l, logFilepath.Native()) if *options.verbose { astilog.SetDefaultLogger() @@ -129,9 +117,9 @@ func init() { openBrowserWg.Add(1) if !isLocalDevMode { // don't use explicit unix filepath here since it uses os.Create directly and won't work on windows - trayIconPathNative := filepath.Join(binaryDirectoryNative, kelpPrefsDirectory, kelpAssetsPath, trayIconName) - log.Printf("trayIconPathNative: %s", trayIconPathNative) - e = writeTrayIcon(kos, trayIconPathNative, currentDirUnix, binaryDirectoryNative) + trayIconPath := basepath.Join(kelpPrefsDirectory, kelpAssetsPath, trayIconName) + log.Printf("trayIconPath: %s", trayIconPath.AsString()) + e = writeTrayIcon(kos, trayIconPath, basepath) if e != nil { log.Fatal(errors.Wrap(e, "could not write tray icon")) } @@ -144,7 +132,7 @@ func init() { appURL := fmt.Sprintf("http://localhost:%d", *options.port) pingURL := fmt.Sprintf("http://localhost:%d/ping", *options.port) // write out tail.html after setting the file to be tailed - tailFileCompiled1 := strings.Replace(htmlContent, stringPlaceholder, logFilepathNative, -1) + tailFileCompiled1 := strings.Replace(htmlContent, stringPlaceholder, logFilepath.Native(), -1) tailFileCompiled2 := strings.Replace(tailFileCompiled1, redirectPlaceholder, appURL, -1) tailFileCompiled3 := strings.Replace(tailFileCompiled2, readyPlaceholder, readyStringIndicator, -1) version := strings.TrimSpace(fmt.Sprintf("%s (%s)", guiVersion, version)) @@ -159,14 +147,14 @@ func init() { tailFilePort := startTailFileServer(tailFileCompiled) electronURL = fmt.Sprintf("http://localhost:%d", tailFilePort) } else { - tailFilepathUnix := toUnixFilepath(filepath.Join(currentDirUnix, kelpPrefsDirectory, "tail.html")) + tailFilepath := basepath.Join(kelpPrefsDirectory, "tail.html") fileContents := []byte(tailFileCompiled) - e := ioutil.WriteFile(tailFilepathUnix, fileContents, 0644) + e := ioutil.WriteFile(tailFilepath.Native(), fileContents, 0644) if e != nil { - panic(fmt.Sprintf("could not write tailfile to path '%s': %s", tailFilepathUnix, e)) + panic(fmt.Sprintf("could not write tailfile to path '%s': %s", tailFilepath, e)) } - electronURL = tailFilepathUnix + electronURL = tailFilepath.Native() } // kick off the desktop window for UI feedback to the user @@ -175,7 +163,7 @@ func init() { if *options.noElectron { openBrowser(appURL, openBrowserWg) } else { - openElectron(trayIconPathNative, electronURL) + openElectron(trayIconPath, electronURL) } }() } @@ -244,24 +232,21 @@ func init() { ccxtGoos = "linux" } - ccxtDirPathNative := filepath.Join(binaryDirectoryNative, kelpPrefsDirectory, kelpCcxtPath) - ccxtDirPathUnix := toUnixFilepath(filepath.Join(currentDirUnix, kelpPrefsDirectory, kelpCcxtPath)) + ccxtDirPath := basepath.Join(kelpPrefsDirectory, kelpCcxtPath) ccxtFilenameNoExt := fmt.Sprintf("ccxt-rest_%s-x64", ccxtGoos) filenameWithExt := fmt.Sprintf("%s.zip", ccxtFilenameNoExt) // don't use explicit unix filepath here since it uses os.Stat and os.Create directly and won't work on windows - ccxtZipDownloadPathNative := filepath.Join(ccxtDirPathNative, filenameWithExt) - e = downloadCcxtBinary(kos, ccxtDirPathUnix, ccxtZipDownloadPathNative, filenameWithExt) + ccxtZipDownloadPath := ccxtDirPath.Join(filenameWithExt) + e = downloadCcxtBinary(kos, ccxtDirPath, ccxtZipDownloadPath, filenameWithExt) if e != nil { panic(e) } - ccxtUnzippedFolderNative := filepath.Join(ccxtDirPathNative, ccxtFilenameNoExt) - ccxtBinPathNative := filepath.Join(ccxtUnzippedFolderNative, ccxtBinaryName) - unzipCcxtFile(kos, ccxtDirPathNative, ccxtBinPathNative, ccxtDirPathUnix, filenameWithExt, currentDirUnix) + ccxtBinPath := ccxtDirPath.Join(ccxtFilenameNoExt, ccxtBinaryName) + unzipCcxtFile(kos, ccxtDirPath, ccxtBinPath, filenameWithExt, basepath) - ccxtBinPathUnix := toUnixFilepath(filepath.Join(ccxtDirPathUnix, ccxtFilenameNoExt, ccxtBinaryName)) - e = runCcxtBinary(kos, ccxtBinPathNative, ccxtBinPathUnix) + e = runCcxtBinary(kos, ccxtBinPath) if e != nil { panic(e) } @@ -270,7 +255,7 @@ func init() { s, e := backend.MakeAPIServer( kos, - currentDirUnix, + basepath, *options.horizonTestnetURI, apiTestNet, *options.horizonPubnetURI, @@ -283,12 +268,12 @@ func init() { panic(e) } - guiWebPathUnix := toUnixFilepath(filepath.Join(currentDirUnix, "../gui/web")) + guiWebPath := basepath.Join("../gui/web") if isLocalDevMode { // the frontend app checks the REACT_APP_API_PORT variable to be set when serving os.Setenv("REACT_APP_API_PORT", fmt.Sprintf("%d", *options.devAPIPort)) go runAPIServerDevBlocking(s, *options.port, *options.devAPIPort) - runWithYarn(kos, options, guiWebPathUnix) + runWithYarn(kos, options, guiWebPath) log.Printf("should not have reached here after running yarn") return @@ -299,7 +284,7 @@ func init() { os.Setenv("REACT_APP_API_PORT", fmt.Sprintf("%d", *options.port)) if isLocalMode { - generateStaticFiles(kos, guiWebPathUnix) + generateStaticFiles(kos, guiWebPath) } r := chi.NewRouter() @@ -366,57 +351,54 @@ func setMiddleware(r *chi.Mux) { r.Use(middleware.Timeout(60 * time.Second)) } -func downloadCcxtBinary(kos *kelpos.KelpOS, ccxtDirPathUnix string, ccxtZipDownloadPathNative string, filenameWithExt string) error { - log.Printf("making ccxtDirPathUnix: %s ...", ccxtDirPathUnix) - e := kos.Mkdir(ccxtDirPathUnix) +func downloadCcxtBinary(kos *kelpos.KelpOS, ccxtDirPath *kelpos.OSPath, ccxtZipDownloadPath *kelpos.OSPath, filenameWithExt string) error { + log.Printf("mkdir ccxtDirPath: %s ...", ccxtDirPath.AsString()) + e := kos.Mkdir(ccxtDirPath) if e != nil { - return errors.Wrap(e, "could not make directories for ccxtDirPathUnix: "+ccxtDirPathUnix) + return errors.Wrap(e, "could not mkdir for ccxtDirPath: "+ccxtDirPath.AsString()) } - if _, e := os.Stat(ccxtZipDownloadPathNative); !os.IsNotExist(e) { + if _, e := os.Stat(ccxtZipDownloadPath.Native()); !os.IsNotExist(e) { return nil } downloadURL := fmt.Sprintf("%s/%s", ccxtDownloadBaseURL, filenameWithExt) - log.Printf("download ccxt from %s to location: %s", downloadURL, ccxtZipDownloadPathNative) - networking.DownloadFile(downloadURL, ccxtZipDownloadPathNative) + log.Printf("download ccxt from %s to location: %s", downloadURL, ccxtZipDownloadPath.AsString()) + networking.DownloadFile(downloadURL, ccxtZipDownloadPath.Native()) return nil } func unzipCcxtFile( kos *kelpos.KelpOS, - ccxtDirNative string, - ccxtBinPathNative string, - ccxtDirUnix string, + ccxtDir *kelpos.OSPath, + ccxtBinPath *kelpos.OSPath, filenameWithExt string, - originalDirUnix string, + originalDir *kelpos.OSPath, ) { - if _, e := os.Stat(ccxtDirNative); !os.IsNotExist(e) { - if _, e := os.Stat(ccxtBinPathNative); !os.IsNotExist(e) { + if _, e := os.Stat(ccxtDir.Native()); !os.IsNotExist(e) { + if _, e := os.Stat(ccxtBinPath.Native()); !os.IsNotExist(e) { return } } log.Printf("unzipping file %s ... ", filenameWithExt) - - zipCmd := fmt.Sprintf("cd %s && unzip %s && cd %s", ccxtDirUnix, filenameWithExt, originalDirUnix) + zipCmd := fmt.Sprintf("cd %s && unzip %s && cd %s", ccxtDir.Unix(), filenameWithExt, originalDir.Unix()) _, e := kos.Blocking("zip", zipCmd) if e != nil { - log.Fatal(errors.Wrap(e, fmt.Sprintf("unable to unzip file %s in directory %s", filenameWithExt, ccxtDirUnix))) + log.Fatal(errors.Wrap(e, fmt.Sprintf("unable to unzip file %s in directory %s", filenameWithExt, ccxtDir))) } - log.Printf("done\n") } -func runCcxtBinary(kos *kelpos.KelpOS, ccxtBinPathNative string, ccxtBinPathUnix string) error { - if _, e := os.Stat(ccxtBinPathNative); os.IsNotExist(e) { - return fmt.Errorf("path to ccxt binary (%s) does not exist", ccxtBinPathNative) +func runCcxtBinary(kos *kelpos.KelpOS, ccxtBinPath *kelpos.OSPath) error { + if _, e := os.Stat(ccxtBinPath.Native()); os.IsNotExist(e) { + return fmt.Errorf("path to ccxt binary (%s) does not exist", ccxtBinPath.AsString()) } - log.Printf("running binary %s", ccxtBinPathUnix) + log.Printf("running binary %s", ccxtBinPath.AsString()) // TODO CCXT should be run at the port specified by rootCcxtRestURL, currently it will default to port 3000 even if the config file specifies otherwise - _, e := kos.Background("ccxt-rest", ccxtBinPathUnix) + _, e := kos.Background("ccxt-rest", ccxtBinPath.Unix()) if e != nil { - log.Fatal(errors.Wrap(e, fmt.Sprintf("unable to run ccxt file %s", ccxtBinPathUnix))) + log.Fatal(errors.Wrap(e, fmt.Sprintf("unable to run ccxt file at location %s", ccxtBinPath.AsString()))) } log.Printf("waiting up to %d seconds for ccxt-rest to start up ...", ccxtWaitSeconds) @@ -452,35 +434,33 @@ func runAPIServerDevBlocking(s *backend.APIServer, frontendPort uint16, devAPIPo log.Fatal(e) } -func runWithYarn(kos *kelpos.KelpOS, options serverInputs, guiWebPathUnix string) { +func runWithYarn(kos *kelpos.KelpOS, options serverInputs, guiWebPath *kelpos.OSPath) { // yarn requires the PORT variable to be set when serving os.Setenv("PORT", fmt.Sprintf("%d", *options.port)) log.Printf("Serving frontend via yarn on HTTP port: %d\n", *options.port) - e := kos.StreamOutput(exec.Command("yarn", "--cwd", guiWebPathUnix, "start")) + e := kos.StreamOutput(exec.Command("yarn", "--cwd", guiWebPath.Unix(), "start")) if e != nil { panic(e) } } -func generateStaticFiles(kos *kelpos.KelpOS, guiWebPathUnix string) { - log.Printf("generating contents of %s/build ...\n", guiWebPathUnix) +func generateStaticFiles(kos *kelpos.KelpOS, guiWebPath *kelpos.OSPath) { + log.Printf("generating contents of %s/build ...\n", guiWebPath.Unix()) - e := kos.StreamOutput(exec.Command("yarn", "--cwd", guiWebPathUnix, "build")) + e := kos.StreamOutput(exec.Command("yarn", "--cwd", guiWebPath.Unix(), "build")) if e != nil { panic(e) } - log.Printf("... finished generating contents of %s/build\n", guiWebPathUnix) + log.Printf("... finished generating contents of %s/build\n", guiWebPath.Unix()) log.Println() } -func writeTrayIcon(kos *kelpos.KelpOS, trayIconPathNative string, currentDirUnix string, binaryDirectoryNative string) error { - assetsDirPathNative := filepath.Join(binaryDirectoryNative, kelpPrefsDirectory, kelpAssetsPath) - log.Printf("assetsDirPathNative: %s", assetsDirPathNative) - assetsDirPathUnix := toUnixFilepath(filepath.Join(currentDirUnix, kelpPrefsDirectory, kelpAssetsPath)) - log.Printf("assetsDirPathUnix: %s", assetsDirPathUnix) - if _, e := os.Stat(trayIconPathNative); !os.IsNotExist(e) { +func writeTrayIcon(kos *kelpos.KelpOS, trayIconPath *kelpos.OSPath, basepath *kelpos.OSPath) error { + assetsDirPath := basepath.Join(kelpPrefsDirectory, kelpAssetsPath) + log.Printf("assetsDirPath: %s", assetsDirPath.AsString()) + if _, e := os.Stat(trayIconPath.Native()); !os.IsNotExist(e) { // file exists, don't write again return nil } @@ -496,16 +476,16 @@ func writeTrayIcon(kos *kelpos.KelpOS, trayIconPathNative string, currentDirUnix } // create dir if not exists - if _, e := os.Stat(assetsDirPathNative); os.IsNotExist(e) { - log.Printf("making assetsDirPathUnix: %s ...", assetsDirPathUnix) - e = kos.Mkdir(assetsDirPathUnix) + if _, e := os.Stat(assetsDirPath.Native()); os.IsNotExist(e) { + log.Printf("mkdir assetsDirPath: %s ...", assetsDirPath.AsString()) + e = kos.Mkdir(assetsDirPath) if e != nil { - return errors.Wrap(e, "could not make directories for assetsDirPathUnix: "+assetsDirPathUnix) + return errors.Wrap(e, "could not mkdir for assetsDirPath: "+assetsDirPath.AsString()) } - log.Printf("... made assetsDirPathUnix (%s)", assetsDirPathUnix) + log.Printf("... made assetsDirPath (%s)", assetsDirPath.AsString()) } - trayIconFile, e := os.Create(trayIconPathNative) + trayIconFile, e := os.Create(trayIconPath.Native()) if e != nil { return errors.Wrap(e, "could not create tray icon file") } @@ -519,19 +499,6 @@ func writeTrayIcon(kos *kelpos.KelpOS, trayIconPathNative string, currentDirUnix return nil } -func getBinaryDirectoryNative() (string, error) { - return filepath.Abs(filepath.Dir(os.Args[0])) -} - -func getCurrentDirUnix() (string, error) { - kos := kelpos.GetKelpOS() - outputBytes, e := kos.Blocking("pwd", "pwd") - if e != nil { - return "", fmt.Errorf("could not fetch current directory: %s", e) - } - return strings.TrimSpace(string(outputBytes)), nil -} - func openBrowser(url string, openBrowserWg *sync.WaitGroup) { log.Printf("opening URL in native browser: %s", url) openBrowserWg.Wait() @@ -542,7 +509,7 @@ func openBrowser(url string, openBrowserWg *sync.WaitGroup) { } } -func openElectron(trayIconPathNative string, url string) { +func openElectron(trayIconPath *kelpos.OSPath, url string) { log.Printf("opening URL in electron: %s", url) e := bootstrap.Run(bootstrap.Options{ AstilectronOptions: astilectron.Options{ @@ -561,7 +528,7 @@ func openElectron(trayIconPathNative string, url string) { }, }}, TrayOptions: &astilectron.TrayOptions{ - Image: astilectron.PtrStr(trayIconPathNative), + Image: astilectron.PtrStr(trayIconPath.Native()), }, TrayMenuOptions: []*astilectron.MenuItemOptions{ &astilectron.MenuItemOptions{ diff --git a/gui/backend/api_server.go b/gui/backend/api_server.go index 4a490e2a0..8a875e2c0 100644 --- a/gui/backend/api_server.go +++ b/gui/backend/api_server.go @@ -7,7 +7,6 @@ import ( "log" "net/http" "os" - "path/filepath" "github.com/stellar/go/clients/horizonclient" "github.com/stellar/kelp/support/kelpos" @@ -15,10 +14,10 @@ import ( // APIServer is an instance of the API service type APIServer struct { - dirPath string - binPath string - configsDir string - logsDir string + basepath *kelpos.OSPath + kelpBinPath *kelpos.OSPath + configsDir *kelpos.OSPath + logsDir *kelpos.OSPath kos *kelpos.KelpOS horizonTestnetURI string horizonPubnetURI string @@ -34,7 +33,7 @@ type APIServer struct { // MakeAPIServer is a factory method func MakeAPIServer( kos *kelpos.KelpOS, - currentDirUnix string, + basepath *kelpos.OSPath, horizonTestnetURI string, apiTestNet *horizonclient.Client, horizonPubnetURI string, @@ -43,14 +42,9 @@ func MakeAPIServer( noHeaders bool, quitFn func(), ) (*APIServer, error) { - binPath, e := filepath.Abs(os.Args[0]) - if e != nil { - return nil, fmt.Errorf("could not get binPath of currently running binary: %s", e) - } - - dirPath := currentDirUnix - configsDir := filepath.Join(currentDirUnix, "ops", "configs") - logsDir := filepath.Join(currentDirUnix, "ops", "logs") + kelpBinPath := basepath.Join(os.Args[0]) + configsDir := basepath.Join("ops", "configs") + logsDir := basepath.Join("ops", "logs") optionsMetadata, e := loadOptionsMetadata() if e != nil { @@ -58,8 +52,8 @@ func MakeAPIServer( } return &APIServer{ - dirPath: dirPath, - binPath: binPath, + basepath: basepath, + kelpBinPath: kelpBinPath, configsDir: configsDir, logsDir: logsDir, kos: kos, @@ -125,24 +119,24 @@ func (s *APIServer) writeJsonWithLog(w http.ResponseWriter, v interface{}, doLog } func (s *APIServer) runKelpCommandBlocking(namespace string, cmd string) ([]byte, error) { - cmdString := fmt.Sprintf("%s %s", s.binPath, cmd) + cmdString := fmt.Sprintf("%s %s", s.kelpBinPath.Unix(), cmd) return s.kos.Blocking(namespace, cmdString) } func (s *APIServer) runKelpCommandBackground(namespace string, cmd string) (*kelpos.Process, error) { - cmdString := fmt.Sprintf("%s %s", s.binPath, cmd) + cmdString := fmt.Sprintf("%s %s", s.kelpBinPath.Unix(), cmd) return s.kos.Background(namespace, cmdString) } func (s *APIServer) setupOpsDirectory() error { e := s.kos.Mkdir(s.configsDir) if e != nil { - return fmt.Errorf("error setting up configs directory: %s\n", e) + return fmt.Errorf("error setting up configs directory (%s): %s\n", s.configsDir, e) } e = s.kos.Mkdir(s.logsDir) if e != nil { - return fmt.Errorf("error setting up logs directory: %s\n", e) + return fmt.Errorf("error setting up logs directory (%s): %s\n", s.logsDir, e) } return nil diff --git a/gui/backend/autogenerate_bot.go b/gui/backend/autogenerate_bot.go index b9f35a4e8..45ed5d1f1 100644 --- a/gui/backend/autogenerate_bot.go +++ b/gui/backend/autogenerate_bot.go @@ -49,18 +49,18 @@ func (s *APIServer) autogenerateBot(w http.ResponseWriter, r *http.Request) { filenamePair := bot.Filenames() sampleTrader := s.makeSampleTrader(kp.Seed()) - traderFilePath := fmt.Sprintf("%s/%s", s.configsDir, filenamePair.Trader) - log.Printf("writing autogenerated bot config to file: %s\n", traderFilePath) - e = toml.WriteFile(traderFilePath, sampleTrader) + traderFilePath := s.configsDir.Join(filenamePair.Trader) + log.Printf("writing autogenerated bot config to file: %s\n", traderFilePath.AsString()) + e = toml.WriteFile(traderFilePath.Native(), sampleTrader) if e != nil { s.writeError(w, fmt.Sprintf("error writing trader toml file: %s\n", e)) return } sampleBuysell := makeSampleBuysell() - strategyFilePath := fmt.Sprintf("%s/%s", s.configsDir, filenamePair.Strategy) - log.Printf("writing autogenerated strategy config to file: %s\n", strategyFilePath) - e = toml.WriteFile(strategyFilePath, sampleBuysell) + strategyFilePath := s.configsDir.Join(filenamePair.Strategy) + log.Printf("writing autogenerated strategy config to file: %s\n", strategyFilePath.AsString()) + e = toml.WriteFile(strategyFilePath.Native(), sampleBuysell) if e != nil { s.writeError(w, fmt.Sprintf("error writing strategy toml file: %s\n", e)) return diff --git a/gui/backend/delete_bot.go b/gui/backend/delete_bot.go index b0d1153db..349588c91 100644 --- a/gui/backend/delete_bot.go +++ b/gui/backend/delete_bot.go @@ -52,7 +52,8 @@ func (s *APIServer) deleteBot(w http.ResponseWriter, r *http.Request) { // delete configs botPrefix := model2.GetPrefix(botName) - _, e = s.kos.Blocking("rm", fmt.Sprintf("rm %s/%s*", s.configsDir, botPrefix)) + botConfigPath := s.configsDir.Join(botPrefix) + _, e = s.kos.Blocking("rm", fmt.Sprintf("rm %s*", botConfigPath.Unix())) if e != nil { s.writeError(w, fmt.Sprintf("error running rm command for bot configs: %s\n", e)) return diff --git a/gui/backend/generate_bot_name.go b/gui/backend/generate_bot_name.go index 8a3ec8bb2..375d06983 100644 --- a/gui/backend/generate_bot_name.go +++ b/gui/backend/generate_bot_name.go @@ -57,7 +57,7 @@ func (s *APIServer) doGenerateBotName() (string, error) { } func (s *APIServer) prefixExists(prefix string) (bool, error) { - command := fmt.Sprintf("ls %s | grep %s", s.configsDir, prefix) + command := fmt.Sprintf("ls %s | grep %s", s.configsDir.Unix(), prefix) _, e := s.kos.Blocking("prefix", command) if e != nil { if strings.Contains(e.Error(), "exit status 1") { diff --git a/gui/backend/get_bot_config.go b/gui/backend/get_bot_config.go index 9d77392c8..ebedb2a8f 100644 --- a/gui/backend/get_bot_config.go +++ b/gui/backend/get_bot_config.go @@ -27,16 +27,16 @@ func (s *APIServer) getBotConfig(w http.ResponseWriter, r *http.Request) { } filenamePair := model2.GetBotFilenames(botName, "buysell") - traderFilePath := fmt.Sprintf("%s/%s", s.configsDir, filenamePair.Trader) + traderFilePath := s.configsDir.Join(filenamePair.Trader) var botConfig trader.BotConfig - e = config.Read(traderFilePath, &botConfig) + e = config.Read(traderFilePath.Native(), &botConfig) if e != nil { s.writeErrorJson(w, fmt.Sprintf("cannot read bot config at path '%s': %s\n", traderFilePath, e)) return } - strategyFilePath := fmt.Sprintf("%s/%s", s.configsDir, filenamePair.Strategy) + strategyFilePath := s.configsDir.Join(filenamePair.Strategy) var buysellConfig plugins.BuySellConfig - e = config.Read(strategyFilePath, &buysellConfig) + e = config.Read(strategyFilePath.Native(), &buysellConfig) if e != nil { s.writeErrorJson(w, fmt.Sprintf("cannot read strategy config at path '%s': %s\n", strategyFilePath, e)) return diff --git a/gui/backend/get_bot_info.go b/gui/backend/get_bot_info.go index 5f07e31fe..987a73a24 100644 --- a/gui/backend/get_bot_info.go +++ b/gui/backend/get_bot_info.go @@ -64,9 +64,9 @@ func (s *APIServer) runGetBotInfoDirect(w http.ResponseWriter, botName string) { } filenamePair := model2.GetBotFilenames(botName, buysell) - traderFilePath := fmt.Sprintf("%s/%s", s.configsDir, filenamePair.Trader) + traderFilePath := s.configsDir.Join(filenamePair.Trader) var botConfig trader.BotConfig - e = config.Read(traderFilePath, &botConfig) + e = config.Read(traderFilePath.Native(), &botConfig) if e != nil { s.writeErrorJson(w, fmt.Sprintf("cannot read bot config at path '%s': %s\n", traderFilePath, e)) return diff --git a/gui/backend/list_bots.go b/gui/backend/list_bots.go index c85d35821..6a15899d5 100644 --- a/gui/backend/list_bots.go +++ b/gui/backend/list_bots.go @@ -13,7 +13,7 @@ import ( func (s *APIServer) listBots(w http.ResponseWriter, r *http.Request) { log.Printf("listing bots\n") - resultBytes, e := s.kos.Blocking("ls", fmt.Sprintf("ls %s | sort", s.configsDir)) + resultBytes, e := s.kos.Blocking("ls", fmt.Sprintf("ls %s | sort", s.configsDir.Unix())) if e != nil { s.writeErrorJson(w, fmt.Sprintf("error when listing bots: %s\n", e)) return diff --git a/gui/backend/start_bot.go b/gui/backend/start_bot.go index 73e8091f9..80d344773 100644 --- a/gui/backend/start_bot.go +++ b/gui/backend/start_bot.go @@ -38,7 +38,33 @@ func (s *APIServer) startBot(w http.ResponseWriter, r *http.Request) { func (s *APIServer) doStartBot(botName string, strategy string, iterations *uint8, maybeFinishCallback func()) error { filenamePair := model2.GetBotFilenames(botName, strategy) logPrefix := model2.GetLogPrefix(botName, strategy) - command := fmt.Sprintf("trade -c %s/%s -s %s -f %s/%s -l %s/%s --ui", s.configsDir, filenamePair.Trader, strategy, s.configsDir, filenamePair.Strategy, s.logsDir, logPrefix) + + // use relative paths here so it works under windows. In windows we use unix paths to reference the config files since it is + // started under the linux subsystem, but it is a windows binary so uses the windows naming scheme (C:\ etc.). Therefore we need + // to either find a regex replacement to convert from unix to windows (/mnt/c -> C:\) or we can use relative paths which we did. + // Note that /mnt/c is unlikely to be valid in windows (but is valid in the linux subsystem) since it's usually prefixed by the + // volume (C:\ etc.), which is why relative paths works so well here as it avoids this confusion. + traderRelativeConfigPath, e := s.configsDir.Join(filenamePair.Trader).RelFromPath(s.basepath) + if e != nil { + return fmt.Errorf("unable to get relative path of trader config file from basepath: %s", e) + } + + stratRelativeConfigPath, e := s.configsDir.Join(filenamePair.Strategy).RelFromPath(s.basepath) + if e != nil { + return fmt.Errorf("unable to get relative path of strategy config file from basepath: %s", e) + } + + logRelativePrefix, e := s.logsDir.Join(logPrefix).RelFromPath(s.basepath) + if e != nil { + return fmt.Errorf("unable to get relative log prefix from basepath: %s", e) + } + + command := fmt.Sprintf("trade -c %s -s %s -f %s -l %s --ui", + traderRelativeConfigPath.Unix(), + strategy, + stratRelativeConfigPath.Unix(), + logRelativePrefix.Unix(), + ) if iterations != nil { command = fmt.Sprintf("%s --iter %d", command, *iterations) } diff --git a/gui/backend/upsert_bot_config.go b/gui/backend/upsert_bot_config.go index 7d49ba298..78235bd78 100644 --- a/gui/backend/upsert_bot_config.go +++ b/gui/backend/upsert_bot_config.go @@ -87,19 +87,19 @@ func (s *APIServer) upsertBotConfig(w http.ResponseWriter, r *http.Request) { } filenamePair := model2.GetBotFilenames(req.Name, req.Strategy) - traderFilePath := fmt.Sprintf("%s/%s", s.configsDir, filenamePair.Trader) + traderFilePath := s.configsDir.Join(filenamePair.Trader) botConfig := req.TraderConfig - log.Printf("upsert bot config to file: %s\n", traderFilePath) - e = toml.WriteFile(traderFilePath, &botConfig) + log.Printf("upsert bot config to file: %s\n", traderFilePath.AsString()) + e = toml.WriteFile(traderFilePath.Native(), &botConfig) if e != nil { s.writeErrorJson(w, fmt.Sprintf("error writing trader botConfig toml file for bot '%s': %s", req.Name, e)) return } - strategyFilePath := fmt.Sprintf("%s/%s", s.configsDir, filenamePair.Strategy) + strategyFilePath := s.configsDir.Join(filenamePair.Strategy) strategyConfig := req.StrategyConfig - log.Printf("upsert strategy config to file: %s\n", strategyFilePath) - e = toml.WriteFile(strategyFilePath, &strategyConfig) + log.Printf("upsert strategy config to file: %s\n", strategyFilePath.AsString()) + e = toml.WriteFile(strategyFilePath.Native(), &strategyConfig) if e != nil { s.writeErrorJson(w, fmt.Sprintf("error writing strategy toml file for bot '%s': %s", req.Name, e)) return diff --git a/scripts/ccxt_bin_gen/ccxt_bin_gen.go b/scripts/ccxt_bin_gen/ccxt_bin_gen.go index 2785976f5..f9d06607c 100644 --- a/scripts/ccxt_bin_gen/ccxt_bin_gen.go +++ b/scripts/ccxt_bin_gen/ccxt_bin_gen.go @@ -73,7 +73,7 @@ func checkPkgTool(kos *kelpos.KelpOS) { func downloadCcxtSource(kos *kelpos.KelpOS, downloadDir string) { fmt.Printf("making directory where we can download ccxt file %s ... ", downloadDir) - e := kos.Mkdir(downloadDir) + _, e := kos.Blocking("mkdir", fmt.Sprintf("mkdir -p %s", downloadDir)) if e != nil { log.Fatal(errors.Wrap(e, "could not make directory for downloadDir "+downloadDir)) } @@ -147,7 +147,7 @@ func copyDependencyFiles(kos *kelpos.KelpOS, outDir string, pkgCmdOutput string) func mkDir(kos *kelpos.KelpOS, zipDir string) { fmt.Printf("making directory %s ... ", zipDir) - e := kos.Mkdir(zipDir) + _, e := kos.Blocking("mkdir", fmt.Sprintf("mkdir -p %s", zipDir)) if e != nil { log.Fatal(errors.Wrap(e, "unable to make directory "+zipDir)) } diff --git a/support/kelpos/ospath.go b/support/kelpos/ospath.go new file mode 100644 index 000000000..95ab166f5 --- /dev/null +++ b/support/kelpos/ospath.go @@ -0,0 +1,131 @@ +package kelpos + +import ( + "fmt" + "log" + "os" + "path/filepath" + "runtime/debug" + "strings" +) + +// OSPath encapsulates the pair of the native path (i.e. windows or unix) and the unix path +// this allows certain commands which are unix-specific to have access to the path instead of running transformations +type OSPath struct { + native string + unix string + isRel bool +} + +// String() is the Stringer method which is unsupprted +func (o *OSPath) String() string { + log.Fatalf("String method is unsupported because the usage is ambiguous for this struct, use .Unix(), .Native(), or .AsString() instead, trace:\n%s", string(debug.Stack())) + return "" +} + +// AsString produces a string representation and we intentionally don't use the Stringer API because this can mistakenly +// be used in place of a string path which will produce hidden runtime errors which is dangerous +func (o *OSPath) AsString() string { + return fmt.Sprintf("OSPath[native=%s, unix=%s, isRel=%v]", o.native, o.unix, o.isRel) +} + +// makeOSPath is an internal helper that enforced always using toUnixFilepath on the unix path +func makeOSPath(native string, unix string, isRel bool) *OSPath { + return &OSPath{ + native: filepath.Clean(native), + unix: toUnixFilepath(filepath.Clean(unix)), + isRel: isRel, + } +} + +// MakeOsPathBase is a factory method for the OSPath struct based on the current binary's directory +func MakeOsPathBase() (*OSPath, error) { + currentDirUnslashed, e := getCurrentDirUnix() + if e != nil { + return nil, fmt.Errorf("could not get current directory: %s", e) + } + binaryDirectoryNative, e := getBinaryDirectoryNative() + if e != nil { + return nil, fmt.Errorf("could not get binary directory: %s", e) + } + ospath := makeOSPath(binaryDirectoryNative, currentDirUnslashed, false) + + if filepath.Base(ospath.Native()) != filepath.Base(ospath.Unix()) { + return nil, fmt.Errorf("ran from directory (%s) but need to run from the same directory as the location of the binary (%s), cd over to the location of the binary", ospath.Unix(), ospath.Native()) + } + + return ospath, nil +} + +// Native returns the native representation of the path as a string +func (o *OSPath) Native() string { + return o.native +} + +// Unix returns the unix representation of the path as a string +func (o *OSPath) Unix() string { + return o.unix +} + +// IsRelative returns true if this is a relative path, otherwise false +func (o *OSPath) IsRelative() bool { + return o.isRel +} + +// Join makes a new OSPath struct by modifying the internal path representations together +func (o *OSPath) Join(elem ...string) *OSPath { + nativePaths := []string{o.native} + nativePaths = append(nativePaths, elem...) + + unixPaths := []string{o.unix} + unixPaths = append(unixPaths, elem...) + + return makeOSPath( + filepath.Join(nativePaths...), + filepath.Join(unixPaths...), + o.isRel, + ) +} + +// RelFromBase returns a *OSPath that is relative to the basepath +func (o *OSPath) RelFromBase() (*OSPath, error) { + basepath, e := MakeOsPathBase() + if e != nil { + return nil, fmt.Errorf("unable to fetch ospathbase: %s", e) + } + + return o.RelFromPath(basepath) +} + +// RelFromPath returns a *OSPath that is relative from the provided path +func (o *OSPath) RelFromPath(basepath *OSPath) (*OSPath, error) { + newRelNative, e := filepath.Rel(basepath.Native(), o.Native()) + if e != nil { + return nil, fmt.Errorf("unable to make relative native path from basepath: %s", e) + } + + newRelUnix, e := filepath.Rel(basepath.Unix(), o.Unix()) + if e != nil { + return nil, fmt.Errorf("unable to make relative unix path from basepath: %s", e) + } + + // set this to be a relative path + return makeOSPath(newRelNative, newRelUnix, true), nil +} + +func getCurrentDirUnix() (string, error) { + kos := GetKelpOS() + outputBytes, e := kos.Blocking("pwd", "pwd") + if e != nil { + return "", fmt.Errorf("could not fetch current directory: %s", e) + } + return strings.TrimSpace(string(outputBytes)), nil +} + +func getBinaryDirectoryNative() (string, error) { + return filepath.Abs(filepath.Dir(os.Args[0])) +} + +func toUnixFilepath(path string) string { + return filepath.ToSlash(path) +} diff --git a/support/kelpos/ospath_test.go b/support/kelpos/ospath_test.go new file mode 100644 index 000000000..1da0f5313 --- /dev/null +++ b/support/kelpos/ospath_test.go @@ -0,0 +1,68 @@ +package kelpos + +import ( + "runtime" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestOSPath(t *testing.T) { + testCases := []struct { + runGoos []string + basePathNative string + wantFinalNative string + }{ + { + runGoos: []string{"linux", "darwin"}, + basePathNative: "/mnt/c/testfolder", + wantFinalNative: "/mnt/c/testfolder/subfolder", + }, { + runGoos: []string{"windows"}, + basePathNative: "C:\\testfolder", + wantFinalNative: "C:\\testfolder\\subfolder", + }, + } + + for _, k := range testCases { + t.Run(k.basePathNative, func(t *testing.T) { + // early exit if running on a disallowed platform to avoid false negatives + isValid := false + for _, allowedGoos := range k.runGoos { + if runtime.GOOS == allowedGoos { + isValid = true + break + } + } + if !isValid { + return + } + + ospath1 := makeOSPath(k.basePathNative, "/mnt/c/testfolder", false) + if !assert.Equal(t, false, ospath1.IsRelative()) { + return + } + ospath2 := ospath1.Join("subfolder") + if !assert.Equal(t, false, ospath2.IsRelative()) { + return + } + if !assert.Equal(t, k.wantFinalNative, ospath2.Native()) { + return + } + if !assert.Equal(t, ospath1.Unix()+"/subfolder", ospath2.Unix()) { + return + } + + rel1, e := ospath2.RelFromPath(ospath1) + if !assert.NoError(t, e) { + return + } + if !assert.Equal(t, true, rel1.IsRelative()) { + return + } + if !assert.Equal(t, "subfolder", rel1.Unix()) { + return + } + }) + } +} diff --git a/support/kelpos/process.go b/support/kelpos/process.go index a9b1ed169..3fd845028 100644 --- a/support/kelpos/process.go +++ b/support/kelpos/process.go @@ -173,8 +173,8 @@ func (kos *KelpOS) RegisteredProcesses() []string { } // Mkdir function with a neat error message -func (kos *KelpOS) Mkdir(dirPath string) error { - _, e := kos.Blocking("mkdir", fmt.Sprintf("mkdir -p %s", dirPath)) +func (kos *KelpOS) Mkdir(dirPath *OSPath) error { + _, e := kos.Blocking("mkdir", fmt.Sprintf("mkdir -p %s", dirPath.Unix())) if e != nil { return fmt.Errorf("error running mkdir command for dir (%s): %s\n", dirPath, e) }