From 0bc17c709a414462230736f7f036ce4acc65a21a Mon Sep 17 00:00:00 2001 From: Nikhil Saraf <1028334+nikhilsaraf@users.noreply.github.com> Date: Thu, 8 Apr 2021 19:07:32 +0530 Subject: [PATCH] Kelp GUI: kaas mode, quit() disabled in KaaS mode, update AppName (closes #687) (#689) * 1 - add enable-kaas mode to server command; trade.go accepts --trigger as 'ui' or 'kaas' instead of bool --ui flag - update AppName to include difference between trigger mode 'ui' and 'kaas' - disable /quit endpoint, logic, frontend button if kaas is enabled (and by default until we get server metadata) - new constants.go file (and support/constants package) * 2 - validate trigger input * 3 - log cli input flags * 4 - move log file in server to be after logging setup * 5 - log client.AppName being used in trade.go * 6 - add correct AppName in server.go for kaas trigger and log --- cmd/server_amd64.go | 63 ++++++++++++++++++++++------------ cmd/trade.go | 18 +++++++--- gui/backend/api_server.go | 3 ++ gui/backend/quit.go | 6 ++++ gui/backend/routes.go | 8 +++-- gui/backend/serverMetadata.go | 2 ++ gui/backend/start_bot.go | 10 +++++- gui/web/src/App.js | 34 +++++++++++++----- support/constants/constants.go | 8 +++++ 9 files changed, 115 insertions(+), 37 deletions(-) create mode 100644 support/constants/constants.go diff --git a/cmd/server_amd64.go b/cmd/server_amd64.go index da374881c..becbda49e 100644 --- a/cmd/server_amd64.go +++ b/cmd/server_amd64.go @@ -58,7 +58,7 @@ const readyPlaceholder = "READY_STRING" const readyStringIndicator = "Serving frontend and API server on HTTP port" const downloadCcxtUpdateIntervalLogMillis = 1000 -type serverInputs struct { +type serverInputOptions struct { port *uint16 dev *bool devAPIPort *uint16 @@ -68,10 +68,17 @@ type serverInputs struct { verbose *bool noElectron *bool disablePubnet *bool + enableKaas *bool +} + +// String is the stringer method impl. +func (o serverInputOptions) String() string { + return fmt.Sprintf("serverInputOptions[port=%d, dev=%v, devAPIPort=%d, horizonTestnetURI='%s', horizonPubnetURI='%s', noHeaders=%v, verbose=%v, noElectron=%v, disablePubnet=%v, enableKaas=%v]", + *o.port, *o.dev, *o.devAPIPort, *o.horizonTestnetURI, *o.horizonPubnetURI, *o.noHeaders, *o.verbose, *o.noElectron, *o.disablePubnet, *o.enableKaas) } func init() { - options := serverInputs{} + options := serverInputOptions{} options.port = serverCmd.Flags().Uint16P("port", "p", 8000, "port on which to serve") options.dev = serverCmd.Flags().Bool("dev", false, "run in dev mode for hot-reloading of JS code") options.devAPIPort = serverCmd.Flags().Uint16("dev-api-port", 8001, "port on which to run API server when in dev mode") @@ -79,8 +86,9 @@ func init() { options.horizonPubnetURI = serverCmd.Flags().String("horizon-pubnet-uri", "https://horizon.stellar.org", "URI to use for the horizon instance connected to the Stellar Public Network (must not contain the word 'test')") options.noHeaders = serverCmd.Flags().Bool("no-headers", false, "do not use Amplitude or set X-App-Name and X-App-Version headers on requests to horizon") options.verbose = serverCmd.Flags().BoolP("verbose", "v", false, "enable verbose log lines typically used for debugging") - options.noElectron = serverCmd.Flags().Bool("no-electron", false, "open in browser instead of using electron") + options.noElectron = serverCmd.Flags().Bool("no-electron", false, "open in browser instead of using electron, only applies when not in KaaS mode") options.disablePubnet = serverCmd.Flags().Bool("disable-pubnet", false, "disable pubnet option") + options.enableKaas = serverCmd.Flags().Bool("enable-kaas", false, "enable kelp-as-a-service (KaaS) mode, which does not bring up browser or electron") serverCmd.Run = func(ccmd *cobra.Command, args []string) { isLocalMode := env == envDev @@ -126,6 +134,8 @@ func init() { } } + log.Printf("initialized server with cli flag inputs: %s", options) + if runtime.GOOS == "windows" { if !*options.noElectron { log.Printf("input options had specified noElectron=false for windows, but electron is not supported on windows yet. force setting noElectron=true for windows.\n") @@ -180,15 +190,18 @@ func init() { electronURL = tailFilepath.Native() } - // kick off the desktop window for UI feedback to the user - // local mode (non --dev) and release binary should open browser (since --dev already opens browser via yarn and returns) - go func() { - if *options.noElectron { - openBrowser(appURL, openBrowserWg) - } else { - openElectron(trayIconPath, electronURL) - } - }() + // only open browser or electron when not running in kaas mode + if !*options.enableKaas { + // kick off the desktop window for UI feedback to the user + // local mode (non --dev) and release binary should open browser (since --dev already opens browser via yarn and returns) + go func() { + if *options.noElectron { + openBrowser(appURL, openBrowserWg) + } else { + openElectron(trayIconPath, electronURL) + } + }() + } } log.Printf("Starting Kelp GUI Server, gui=%s, cli=%s [%s]\n", guiVersion, version, gitHash) @@ -223,12 +236,17 @@ func init() { HTTP: http.DefaultClient, } if !*options.noHeaders { - if *options.noElectron { - apiTestNet.AppName = "kelp--gui-desktop--admin-browser" - apiPubNet.AppName = "kelp--gui-desktop--admin-browser" + if *options.enableKaas { + apiTestNet.AppName = "kelp--gui-kaas--admin" + apiPubNet.AppName = "kelp--gui-kaas--admin" } else { - apiTestNet.AppName = "kelp--gui-desktop--admin-electron" - apiPubNet.AppName = "kelp--gui-desktop--admin-electron" + if *options.noElectron { + apiTestNet.AppName = "kelp--gui-desktop--admin-browser" + apiPubNet.AppName = "kelp--gui-desktop--admin-browser" + } else { + apiTestNet.AppName = "kelp--gui-desktop--admin-electron" + apiPubNet.AppName = "kelp--gui-desktop--admin-electron" + } } apiTestNet.AppVersion = version @@ -245,6 +263,7 @@ func init() { } } } + log.Printf("using apiTestNet.AppName = '%s' and apiPubNet.AppName = '%s'", apiTestNet.AppName, apiPubNet.AppName) if isLocalDevMode { log.Printf("not checking ccxt in local dev mode") @@ -346,6 +365,7 @@ func init() { apiPubNet, *rootCcxtRestURL, *options.disablePubnet, + *options.enableKaas, *options.noHeaders, quit, metricsTracker, @@ -364,7 +384,7 @@ func init() { // 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, guiWebPath) + runWithYarn(kos, *options.port, guiWebPath) log.Printf("should not have reached here after running yarn") return @@ -574,11 +594,11 @@ func runAPIServerDevBlocking(s *backend.APIServer, frontendPort uint16, devAPIPo log.Fatal(e) } -func runWithYarn(kos *kelpos.KelpOS, options serverInputs, guiWebPath *kelpos.OSPath) { +func runWithYarn(kos *kelpos.KelpOS, port uint16, guiWebPath *kelpos.OSPath) { // yarn requires the PORT variable to be set when serving - os.Setenv("PORT", fmt.Sprintf("%d", *options.port)) + os.Setenv("PORT", fmt.Sprintf("%d", port)) - log.Printf("Serving frontend via yarn on HTTP port: %d\n", *options.port) + log.Printf("Serving frontend via yarn on HTTP port: %d\n", port) e := kos.StreamOutput(exec.Command("yarn", "--cwd", guiWebPath.Unix(), "start")) if e != nil { panic(e) @@ -710,6 +730,7 @@ func openElectron(trayIconPath *kelpos.OSPath, url string) { } func quit() { + // this is still valid when running in KaaS mode since it doesn't matter. we can disable it (or make it error) if we wanted log.Printf("quitting...") os.Exit(0) } diff --git a/cmd/trade.go b/cmd/trade.go index fc0d0cd68..0fa814b04 100644 --- a/cmd/trade.go +++ b/cmd/trade.go @@ -24,6 +24,7 @@ import ( "github.com/stellar/kelp/kelpdb" "github.com/stellar/kelp/model" "github.com/stellar/kelp/plugins" + "github.com/stellar/kelp/support/constants" "github.com/stellar/kelp/support/database" "github.com/stellar/kelp/support/logger" "github.com/stellar/kelp/support/monitoring" @@ -105,7 +106,7 @@ type inputs struct { logPrefix *string fixedIterations *uint64 noHeaders *bool - ui *bool + trigger *string cpuProfile *string memProfile *string } @@ -127,6 +128,10 @@ func validateCliParams(l logger.Logger, options inputs) { } else { l.Infof("will run only %d update iterations\n", *options.fixedIterations) } + + if *options.trigger != constants.TriggerDefault && *options.trigger != constants.TriggerUI && *options.trigger != constants.TriggerKaas { + panic(fmt.Sprintf("invalid trigger argument: '%s'", *options.trigger)) + } } func validateBotConfig(l logger.Logger, botConfig trader.BotConfig) { @@ -167,7 +172,7 @@ func init() { options.logPrefix = tradeCmd.Flags().StringP("log", "l", "", "log to a file (and stdout) with this prefix for the filename") options.fixedIterations = tradeCmd.Flags().Uint64("iter", 0, "only run the bot for the first N iterations (defaults value 0 runs unboundedly)") options.noHeaders = tradeCmd.Flags().Bool("no-headers", false, "do not use Amplitude or set X-App-Name and X-App-Version headers on requests to horizon") - options.ui = tradeCmd.Flags().Bool("ui", false, "indicates a bot that is started from the Kelp UI server") + options.trigger = tradeCmd.Flags().String("trigger", constants.TriggerDefault, fmt.Sprintf("indicates a bot that is triggered from a parent process ('%s' or '%s')", constants.TriggerUI, constants.TriggerKaas)) options.cpuProfile = tradeCmd.Flags().String("cpuprofile", "", "write cpu profile to `file`") options.memProfile = tradeCmd.Flags().String("memprofile", "", "write memory profile to `file`") @@ -175,7 +180,7 @@ func init() { requiredFlag("strategy") hiddenFlag("operationalBuffer") hiddenFlag("operationalBufferNonNativePct") - hiddenFlag("ui") + hiddenFlag("trigger") tradeCmd.Flags().SortFlags = false tradeCmd.Run = func(ccmd *cobra.Command, args []string) { @@ -564,7 +569,7 @@ func runTradeCmd(options inputs) { l.Infof("Trading %s:%s for %s:%s\n", botConfig.AssetCodeA, botConfig.IssuerA, botConfig.AssetCodeB, botConfig.IssuerB) var guiVersionFlag string - if *options.ui { + if *options.trigger == constants.TriggerUI || *options.trigger == constants.TriggerKaas { guiVersionFlag = guiVersion } @@ -644,8 +649,10 @@ func runTradeCmd(options inputs) { } if !*options.noHeaders { client.AppName = "kelp--cli--bot" - if *options.ui { + if *options.trigger == constants.TriggerUI { client.AppName = "kelp--gui-desktop--bot" + } else if *options.trigger == constants.TriggerKaas { + client.AppName = "kelp--gui-kaas--bot" } client.AppVersion = version @@ -660,6 +667,7 @@ func runTradeCmd(options inputs) { } } } + log.Printf("using client.AppName = %s", client.AppName) if *rootCcxtRestURL == "" && botConfig.CcxtRestURL != nil { e := sdk.SetBaseURL(*botConfig.CcxtRestURL) diff --git a/gui/backend/api_server.go b/gui/backend/api_server.go index 975ae595f..600d3dd7d 100644 --- a/gui/backend/api_server.go +++ b/gui/backend/api_server.go @@ -51,6 +51,7 @@ type APIServer struct { apiTestNet *horizonclient.Client apiPubNet *horizonclient.Client disablePubnet bool + enableKaas bool noHeaders bool quitFn func() metricsTracker *plugins.MetricsTracker @@ -71,6 +72,7 @@ func MakeAPIServer( apiPubNet *horizonclient.Client, ccxtRestUrl string, disablePubnet bool, + enableKaas bool, noHeaders bool, quitFn func(), metricsTracker *plugins.MetricsTracker, @@ -93,6 +95,7 @@ func MakeAPIServer( apiTestNet: apiTestNet, apiPubNet: apiPubNet, disablePubnet: disablePubnet, + enableKaas: enableKaas, noHeaders: noHeaders, cachedOptionsMetadata: optionsMetadata, quitFn: quitFn, diff --git a/gui/backend/quit.go b/gui/backend/quit.go index bbba96d65..8f7d36a67 100644 --- a/gui/backend/quit.go +++ b/gui/backend/quit.go @@ -1,11 +1,17 @@ package backend import ( + "fmt" "net/http" "time" ) func (s *APIServer) quit(w http.ResponseWriter, r *http.Request) { + if s.enableKaas { + w.WriteHeader(http.StatusInternalServerError) + panic(fmt.Errorf("quit functionality should have been disabled in routes when running in KaaS mode")) + } + go func() { // sleep so we can respond to the request time.Sleep(1 * time.Second) diff --git a/gui/backend/routes.go b/gui/backend/routes.go index 0cfd6dbf7..5fa0a555b 100644 --- a/gui/backend/routes.go +++ b/gui/backend/routes.go @@ -6,11 +6,15 @@ import ( "github.com/go-chi/chi" ) -// SetRoutes +// SetRoutes adds the handlers for the endpoints func SetRoutes(r *chi.Mux, s *APIServer) { r.Route("/api/v1", func(r chi.Router) { + if !s.enableKaas { + // /quit is only enabled when we are not in KaaS mode + r.Get("/quit", http.HandlerFunc(s.quit)) + } + r.Get("/version", http.HandlerFunc(s.version)) - r.Get("/quit", http.HandlerFunc(s.quit)) r.Get("/serverMetadata", http.HandlerFunc(s.serverMetadata)) r.Get("/newSecretKey", http.HandlerFunc(s.newSecretKey)) r.Get("/optionsMetadata", http.HandlerFunc(s.optionsMetadata)) diff --git a/gui/backend/serverMetadata.go b/gui/backend/serverMetadata.go index 4cee8155d..a512acffa 100644 --- a/gui/backend/serverMetadata.go +++ b/gui/backend/serverMetadata.go @@ -9,11 +9,13 @@ import ( // ServerMetadataResponse is the response from the /serverMetadata endpoint type ServerMetadataResponse struct { DisablePubnet bool `json:"disable_pubnet"` + EnableKaas bool `json:"enable_kaas"` } func (s *APIServer) serverMetadata(w http.ResponseWriter, r *http.Request) { metadata := ServerMetadataResponse{ DisablePubnet: s.disablePubnet, + EnableKaas: s.enableKaas, } b, e := json.Marshal(metadata) diff --git a/gui/backend/start_bot.go b/gui/backend/start_bot.go index 50284f855..8f284de0e 100644 --- a/gui/backend/start_bot.go +++ b/gui/backend/start_bot.go @@ -12,6 +12,7 @@ import ( "github.com/stellar/go/support/config" "github.com/stellar/kelp/gui/model2" + "github.com/stellar/kelp/support/constants" "github.com/stellar/kelp/support/kelpos" "github.com/stellar/kelp/trader" ) @@ -120,11 +121,18 @@ func (s *APIServer) doStartBot(userData UserData, botName string, strategy strin return fmt.Errorf("cannnot start pubnet bots when pubnet is disabled") } - command := fmt.Sprintf("trade -c %s -s %s -f %s -l %s --ui", + // triggerMode informs the underlying bot process how it was started so it can set anything specific on that bot that it needs to + // it is only one of these two because it is not started up manually, which would not have a trigger mode (i.e. default) + triggerMode := constants.TriggerUI + if s.enableKaas { + triggerMode = constants.TriggerKaas + } + command := fmt.Sprintf("trade -c %s -s %s -f %s -l %s --trigger %s", traderRelativeConfigPath.Unix(), strategy, stratRelativeConfigPath.Unix(), logRelativePrefixPath.Unix(), + triggerMode, ) if iterations != nil { command = fmt.Sprintf("%s --iter %d", command, *iterations) diff --git a/gui/web/src/App.js b/gui/web/src/App.js index 57f624111..2d7201296 100644 --- a/gui/web/src/App.js +++ b/gui/web/src/App.js @@ -35,6 +35,7 @@ class App extends Component { this.setVersion = this.setVersion.bind(this); this.fetchServerMetadata = this.fetchServerMetadata.bind(this); + this.showQuitButton = this.showQuitButton.bind(this); this.quit = this.quit.bind(this); this.addError = this.addError.bind(this); this.addErrorToObject = this.addErrorToObject.bind(this); @@ -92,6 +93,11 @@ class App extends Component { } quit() { + if (!this.showQuitButton()) { + console.error("calling quit function when showQuitButton() returned false!"); + return + } + var _this = this this._asyncRequests["quit"] = quit(baseUrl).then(resp => { if (!_this._asyncRequests["quit"]) { @@ -313,20 +319,32 @@ class App extends Component { this.setState({ "active_error": null }); } + showQuitButton() { + // showQuit defaults to false, use this instead of enableKaas so it's more explicit + return this.state.server_metadata ? !this.state.server_metadata.enable_kaas : false; + } + render() { // construction of metricsTracker in server_amd64.go (isTestnet) needs to logically match this variable // we use the state because that is updated from the /serverMetadata endpoint const enablePubnetBots = this.state.server_metadata ? !this.state.server_metadata.disable_pubnet : false; + let quitButton = ""; + if (this.showQuitButton()) { + quitButton = ( + + ); + } + let banner = (