Skip to content
This repository has been archived by the owner on Feb 1, 2024. It is now read-only.

Commit

Permalink
Kelp UI: Wrap GUI as a standalone desktop application using Electron (#…
Browse files Browse the repository at this point in the history
…308)

* 1 - open browser when running GUI in production mode

* 2 - add server command to kelp in release mode and invoke in root command

* 3 - open browser automatically in local mode as well

* 4 - incorporated go-astilectron and opened up app

* 5 - add astilectron-bootstrap and astilectron-bundler as dependencies

* 6 - use astilectron-bootstrap to start up server

* 7 - basic build for GUI desktop app using build script

* 8 - include logic building UI with multiple GOOS values in build script

* 9 - update clean script with change of filesystem file generation

* 10 - zip UI output, and wrap into a versioned folder

* 11 - remove sleep when serving through desktop

* 12 - add quit logic with tray icon (dev mode only)

* 13 - write out tray icon from bind file to directory so it can be used

* 14 - update README to indicate that astilectron-bundler is a dependency

* 15 - generate bind file before compiling binary

* 16 - circleci script should also install astilectron-bundler as a dependency

* 17 - circleci update cache key since the steps changed

* 18 - circleci build kelp before running tests
  • Loading branch information
nikhilsaraf authored Oct 18, 2019
1 parent da0e5fa commit b725cba
Show file tree
Hide file tree
Showing 17 changed files with 369 additions and 104 deletions.
47 changes: 12 additions & 35 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,25 +5,31 @@ commands:
- checkout
- restore_cache:
keys:
- v1-pkg-cache
- v2-pkg-cache
- v1-node-cache
- run:
name: Install glide
command: curl https://glide.sh/get | sh
- run:
name: Install dependencies using glide
command: glide install
- run:
name: Fetch astilectron-bundler
command: go get -u github.com/asticode/go-astilectron-bundler/...
- run:
name: Install astilectron-bundler
command: go install github.com/asticode/go-astilectron-bundler

- save_cache:
key: v1-pkg-cache
key: v2-pkg-cache
paths:
- "/go/src/github.com/stellar/kelp/vendor"

test_kelp:
steps:
- run:
name: Run Kelp tests
command: go test -tags dev --short ./...
command: go test --short ./...

build_kelp:
steps:
Expand Down Expand Up @@ -55,6 +61,7 @@ jobs:

steps:
- install_deps
- build_kelp
- test_kelp

test_1_11:
Expand All @@ -66,6 +73,7 @@ jobs:

steps:
- install_deps
- build_kelp
- test_kelp

test_1_12:
Expand All @@ -75,38 +83,10 @@ jobs:
- image: franzsee/ccxt-rest:v0.0.4
command: ["node", "/usr/local/bin/ccxt-rest"]

steps:
- install_deps
- test_kelp


# build tries building Kelp
build_1_10:
working_directory: /go/src/github.com/stellar/kelp
docker:
- image: circleci/golang:1.10-node

steps:
- install_deps
- build_kelp

build_1_11:
working_directory: /go/src/github.com/stellar/kelp
docker:
- image: circleci/golang:1.11-node

steps:
- install_deps
- build_kelp

build_1_12:
working_directory: /go/src/github.com/stellar/kelp
docker:
- image: circleci/golang:1.12-node

steps:
- install_deps
- build_kelp
- test_kelp

workflows:
version: 2
Expand All @@ -115,6 +95,3 @@ workflows:
- test_1_10
- test_1_11
- test_1_12
- build_1_10
- build_1_11
- build_1_12
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,6 @@ vendor/
build/
bin/
.idea
gui/filesystem_vfsdata_release.go
gui/filesystem_vfsdata.go
kelp.prefs
bind_*.go
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ To compile Kelp from source:
* `git clone [email protected]:stellar/kelp.git`
5. Change to the kelp directory and install the dependencies:
* `glide install`
6. Install the [astilectron-bundler][astilectron-bundler] binary in a folder that is in your `PATH`
6. Build the binaries using the provided build script (the _go install_ command will produce a faulty binary):
* `./scripts/build.sh`
7. Confirm one new binary file:
Expand Down Expand Up @@ -310,6 +311,7 @@ See the [Code of Conduct](CODE_OF_CONDUCT.md).
[glide-install]: https://github.com/Masterminds/glide#install
[yarn-install]: https://yarnpkg.com/lang/en/docs/install/
[nodejs-install]: https://nodejs.org/en/download/
[astilectron-bundler]: https://github.com/asticode/go-astilectron-bundler
[spread]: https://en.wikipedia.org/wiki/Bid%E2%80%93ask_spread
[hedge]: https://en.wikipedia.org/wiki/Hedge_(finance)
[cmc]: https://coinmarketcap.com/
Expand Down
10 changes: 10 additions & 0 deletions bundler.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"app_name": "Kelp",
"icon_path_darwin": "resources/[email protected]",
"icon_path_linux": "resources/[email protected]",
"icon_path_windows": "resources/[email protected]",
"bind": {
"output_path": "./cmd",
"package": "cmd"
}
}
10 changes: 2 additions & 8 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package cmd

import (
"fmt"
"log"
"net/http"
"os"
"strings"
Expand Down Expand Up @@ -45,10 +44,7 @@ var RootCmd = &cobra.Command{
` + version + `
`
fmt.Println(intro)
e := ccmd.Help()
if e != nil {
log.Fatal(e)
}
serverCmd.Run(ccmd, args)
},
}

Expand All @@ -60,9 +56,7 @@ func init() {
rootCcxtRestURL = RootCmd.PersistentFlags().String("ccxt-rest-url", "", "URL to use for the CCXT-rest API. Takes precendence over the CCXT_REST_URL param set in the botConfg file for the trade command and passed as a parameter into the Kelp subprocesses started by the GUI (default URL is https://localhost:3000)")

RootCmd.AddCommand(tradeCmd)
if env == envDev {
RootCmd.AddCommand(serverCmd)
}
RootCmd.AddCommand(serverCmd)
RootCmd.AddCommand(strategiesCmd)
RootCmd.AddCommand(exchanagesCmd)
RootCmd.AddCommand(terminateCmd)
Expand Down
139 changes: 133 additions & 6 deletions cmd/server.go
Original file line number Diff line number Diff line change
@@ -1,25 +1,37 @@
package cmd

import (
"bytes"
"fmt"
"image"
"image/png"
"log"
"net/http"
"os"
"os/exec"
"path/filepath"
"strings"
"time"

"github.com/asticode/go-astilectron"
bootstrap "github.com/asticode/go-astilectron-bootstrap"
"github.com/go-chi/chi"
"github.com/go-chi/chi/middleware"
"github.com/rs/cors"
"github.com/spf13/cobra"
"github.com/stellar/go/clients/horizonclient"
"github.com/stellar/go/support/errors"
"github.com/stellar/kelp/gui"
"github.com/stellar/kelp/gui/backend"
"github.com/stellar/kelp/support/kelpos"
"github.com/stellar/kelp/support/prefs"
)

const urlOpenDelayMillis = 1500
const kelpPrefsDirectory = ".kelp"
const kelpAssetsPath = "/assets"
const trayIconName = "[email protected]"

var serverCmd = &cobra.Command{
Use: "server",
Short: "Serves the Kelp GUI",
Expand Down Expand Up @@ -97,12 +109,12 @@ func init() {
go runAPIServerDevBlocking(s, *options.port, *options.devAPIPort)
runWithYarn(kos, options)
return
} else {
options.devAPIPort = nil
// 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.port))
}

options.devAPIPort = nil
// 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.port))

if env == envDev {
checkHomeDir()
generateStaticFiles(kos)
Expand All @@ -116,8 +128,22 @@ func init() {

portString := fmt.Sprintf(":%d", *options.port)
log.Printf("Serving frontend and API server on HTTP port: %d\n", *options.port)
e = http.ListenAndServe(portString, r)
log.Fatal(e)
// local mode (non --dev) and release binary should open browser (since --dev already opens browser via yarn and returns)
go func() {
url := fmt.Sprintf("http://localhost:%d", *options.port)
log.Printf("A browser window will open up automatically to %s\n", url)
time.Sleep(urlOpenDelayMillis * time.Millisecond)
openBrowser(kos, url)
}()

if env == envDev {
e = http.ListenAndServe(portString, r)
if e != nil {
log.Fatal(e)
}
} else {
_ = http.ListenAndServe(portString, r)
}
}
}

Expand Down Expand Up @@ -178,3 +204,104 @@ func generateStaticFiles(kos *kelpos.KelpOS) {
log.Printf("... finished generating contents of gui/web/build\n")
log.Println()
}

func writeTrayIcon(kos *kelpos.KelpOS) (string, error) {
binDirectory, e := getBinaryDirectory()
if e != nil {
return "", errors.Wrap(e, "could not get binary directory")
}
log.Printf("binDirectory: %s", binDirectory)
assetsDirPath := filepath.Join(binDirectory, kelpPrefsDirectory, kelpAssetsPath)
log.Printf("assetsDirPath: %s", assetsDirPath)
trayIconPath := filepath.Join(assetsDirPath, trayIconName)
log.Printf("trayIconPath: %s", trayIconPath)
if _, e := os.Stat(trayIconPath); !os.IsNotExist(e) {
// file exists, don't write again
return trayIconPath, nil
}

trayIconBytes, e := resourcesKelpIcon18xPngBytes()
if e != nil {
return "", errors.Wrap(e, "could not fetch tray icon image bytes")
}

img, _, e := image.Decode(bytes.NewReader(trayIconBytes))
if e != nil {
return "", errors.Wrap(e, "could not decode bytes as image data")
}

// create dir if not exists
if _, e := os.Stat(assetsDirPath); os.IsNotExist(e) {
log.Printf("making assetsDirPath: %s ...", assetsDirPath)
e = kos.Mkdir(assetsDirPath)
if e != nil {
return "", errors.Wrap(e, "could not make directories for assetsDirPath: "+assetsDirPath)
}
log.Printf("... made assetsDirPath (%s)", assetsDirPath)
}

trayIconFile, e := os.Create(trayIconPath)
if e != nil {
return "", errors.Wrap(e, "could not create tray icon file")
}
defer trayIconFile.Close()

e = png.Encode(trayIconFile, img)
if e != nil {
return "", errors.Wrap(e, "could not write png encoded icon")
}

return trayIconPath, nil
}

func getBinaryDirectory() (string, error) {
return filepath.Abs(filepath.Dir(os.Args[0]))
}

func openBrowser(kos *kelpos.KelpOS, url string) {
trayIconPath, e := writeTrayIcon(kos)
if e != nil {
log.Fatal(errors.Wrap(e, "could not write tray icon"))
}

e = bootstrap.Run(bootstrap.Options{
AstilectronOptions: astilectron.Options{
AppName: "Kelp",
AppIconDefaultPath: "resources/[email protected]",
},
Debug: false,
Windows: []*bootstrap.Window{&bootstrap.Window{
Homepage: url,
Options: &astilectron.WindowOptions{
Center: astilectron.PtrBool(true),
Width: astilectron.PtrInt(1280),
Height: astilectron.PtrInt(960),
Closable: astilectron.PtrBool(false),
},
}},
TrayOptions: &astilectron.TrayOptions{
Image: astilectron.PtrStr(trayIconPath),
},
TrayMenuOptions: []*astilectron.MenuItemOptions{
&astilectron.MenuItemOptions{
Label: astilectron.PtrStr("Quit"),
Visible: astilectron.PtrBool(true),
Enabled: astilectron.PtrBool(true),
OnClick: astilectron.Listener(func(e astilectron.Event) (deleteListener bool) {
quit()
return false
}),
},
},
})
if e != nil {
log.Fatal(e)
}

quit()
}

func quit() {
log.Printf("quitting...")
os.Exit(0)
}
Loading

0 comments on commit b725cba

Please sign in to comment.