From 3d3999de42cb9677af9bea2684bd9a43c245cb52 Mon Sep 17 00:00:00 2001 From: jamesread Date: Wed, 12 May 2021 10:43:32 +0100 Subject: [PATCH] Various updates in a ubercommit :-) --- .gitignore | 1 + Makefile | 2 +- OliveTin.proto | 7 +++- README.md | 8 ++++ cmd/OliveTin/main.go | 20 ++++++--- go.mod | 1 + pkg/config/config.go | 12 ++++-- pkg/config/config_test.go | 15 +++++++ pkg/executor/executor.go | 65 ++++++++++++++++++++++++++++++ pkg/grpcApi/grpcApi.go | 37 +++++++++-------- pkg/grpcApi/utils.go | 14 +++++++ pkg/restApi/restapi.go | 17 ++++---- pkg/webuiServer/webuiServer.go | 27 +++++++++++++ web/main.js | 9 ----- {web => webui}/.eslintrc.json | 0 {web => webui}/.stylelintrc.json | 0 {web => webui}/OliveTinLogo.png | Bin {web => webui}/OliveTinLogo.svg | 0 web/index.htm => webui/index.html | 0 {web => webui}/js/classes.js | 2 +- {web => webui}/js/loader.js | 0 webui/main.js | 38 +++++++++++++++++ {web => webui}/package.json | 0 {web => webui}/style.css | 5 +++ 24 files changed, 234 insertions(+), 46 deletions(-) create mode 100644 pkg/config/config_test.go create mode 100644 pkg/executor/executor.go create mode 100644 pkg/grpcApi/utils.go create mode 100644 pkg/webuiServer/webuiServer.go delete mode 100644 web/main.js rename {web => webui}/.eslintrc.json (100%) rename {web => webui}/.stylelintrc.json (100%) rename {web => webui}/OliveTinLogo.png (100%) rename {web => webui}/OliveTinLogo.svg (100%) rename web/index.htm => webui/index.html (100%) rename {web => webui}/js/classes.js (96%) rename {web => webui}/js/loader.js (100%) create mode 100644 webui/main.js rename {web => webui}/package.json (100%) rename {web => webui}/style.css (96%) diff --git a/.gitignore b/.gitignore index 92b8a41..a41636c 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ web/node_modules **/*.swo gen/ go.sum +OliveTin diff --git a/Makefile b/Makefile index 1b60d7b..ac75593 100644 --- a/Makefile +++ b/Makefile @@ -3,6 +3,6 @@ default: grpc: protoc -I.:/usr/share/gocode/src/github.com/grpc-ecosystem/grpc-gateway/third_party/googleapis/ --go_out=plugins=grpc:gen/grpc/ OliveTin.proto - protoc -I.:/usr/share/gocode/src/github.com/grpc-ecosystem/grpc-gateway/third_party/googleapis/ --grpc-gateway_out=gen/grpc --grpc-gateway_opt paths=source_relative --grpc-gateway_opt generate_unbound_methods=true OliveTin.proto + protoc -I.:/usr/share/gocode/src/github.com/grpc-ecosystem/grpc-gateway/third_party/googleapis/ --grpc-gateway_out=gen/grpc --grpc-gateway_opt paths=source_relative OliveTin.proto .PHONY: grpc diff --git a/OliveTin.proto b/OliveTin.proto index f112d33..b93fd60 100644 --- a/OliveTin.proto +++ b/OliveTin.proto @@ -18,7 +18,12 @@ message StartActionRequest { string actionName = 1; } -message StartActionResponse {} +message StartActionResponse { + string stdout = 1; + string stderr = 2; + bool timedOut = 3; + int32 exitCode = 4; +} service OliveTinApi { rpc GetButtons(GetButtonsRequest) returns (GetButtonsResponse) { diff --git a/README.md b/README.md index 367a5b3..671d0a6 100644 --- a/README.md +++ b/README.md @@ -4,3 +4,11 @@ OliveTin is a web based quick access control panel for running jobs. For example, it can be used to turn home automation lights on or off, or start workflows in n8n. + +## `config.yaml` + +``` +listenAddressRestActions: :1337 # Listen on all addresses available, port 1337 +listenAddressWebUi: :1339 +logLevel: "INFO" +``` diff --git a/cmd/OliveTin/main.go b/cmd/OliveTin/main.go index 3735fb9..e4c6828 100644 --- a/cmd/OliveTin/main.go +++ b/cmd/OliveTin/main.go @@ -5,8 +5,10 @@ import ( restApi "github.com/jamesread/OliveTin/pkg/restApi" grpcApi "github.com/jamesread/OliveTin/pkg/grpcApi" + webuiServer "github.com/jamesread/OliveTin/pkg/webuiServer" config "github.com/jamesread/OliveTin/pkg/config" + executor "github.com/jamesread/OliveTin/pkg/executor" "github.com/spf13/viper" "os" ) @@ -20,12 +22,14 @@ func init() { log.SetLevel(log.DebugLevel) // Default to debug, to catch cfg issues viper.AutomaticEnv(); + viper.SetConfigName("config.yaml") viper.SetConfigType("yaml") viper.AddConfigPath(".") - viper.AddConfigPath("/etc/olivetin/") + viper.AddConfigPath("/etc/OliveTin/") if err := viper.ReadInConfig(); err != nil { - log.Panicf("Config file error %s", err); + log.Errorf("Config file error at startup. %s", err); + os.Exit(1); }; cfg = config.DefaultConfig(); @@ -42,13 +46,17 @@ func init() { func main() { log.WithFields(log.Fields { - "listenPortRestActions": cfg.ListenPortRestActions, - "listenPortWebUi": cfg.ListenPortWebUi, + "listenAddressGrpcActions": cfg.ListenAddressGrpcActions, + "listenAddressRestActions": cfg.ListenAddressRestActions, + "listenAddressWebUi": cfg.ListenAddressWebUi, }).Info("OliveTin started"); log.Debugf("%+v", cfg) - go grpcApi.Start() + executor.Cfg = cfg; - restApi.Start(); + go grpcApi.Start(cfg.ListenAddressGrpcActions, cfg) + go restApi.Start(cfg.ListenAddressRestActions, cfg.ListenAddressGrpcActions, cfg) + + webuiServer.Start(cfg.ListenAddressWebUi, cfg.ListenAddressRestActions) } diff --git a/go.mod b/go.mod index e2e4e7f..e106975 100644 --- a/go.mod +++ b/go.mod @@ -13,6 +13,7 @@ require ( golang.org/x/net v0.0.0-20210119194325-5f4716e94777 // indirect golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c // indirect golang.org/x/text v0.3.5 // indirect + golang.org/x/tools v0.1.0 // indirect golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect google.golang.org/genproto v0.0.0-20210224155714-063164c882e6 google.golang.org/grpc v1.37.0 diff --git a/pkg/config/config.go b/pkg/config/config.go index 76d136f..f1ce2b8 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -8,6 +8,7 @@ import ( type ActionButton struct { Title string Icon string + Shell string Css map[string]string `mapstructure:omitempty` } @@ -19,8 +20,9 @@ type Entity struct { } type Config struct { - ListenPortRestActions int - ListenPortWebUi int + ListenAddressWebUi string + ListenAddressRestActions string + ListenAddressGrpcActions string LogLevel string ActionButtons []ActionButton `mapstructure:"actions"` Entities []Entity `mapstructure:omitempty` @@ -28,8 +30,9 @@ type Config struct { func DefaultConfig() *Config { config := Config{}; - config.ListenPortRestActions = 1337; - config.ListenPortWebUi = 1339; + config.ListenAddressWebUi = "0.0.0.0:1337" + config.ListenAddressRestActions = "0.0.0.0:1338" + config.ListenAddressGrpcActions = "0.0.0.0:1339" config.LogLevel = "INFO" return &config @@ -43,3 +46,4 @@ func (cfg *Config) GetLogLevel() (log.Level) { default: return log.DebugLevel; } } + diff --git a/pkg/config/config_test.go b/pkg/config/config_test.go new file mode 100644 index 0000000..74259ef --- /dev/null +++ b/pkg/config/config_test.go @@ -0,0 +1,15 @@ +package config + +import ( + "testing" + log "github.com/sirupsen/logrus" +) + +func TestGetLog(t *testing.T) { + c := Config{} + c.LogLevel = "" + + if c.GetLogLevel() != log.InfoLevel { + t.Errorf("Info expected") + } +} diff --git a/pkg/executor/executor.go b/pkg/executor/executor.go new file mode 100644 index 0000000..bdb93b4 --- /dev/null +++ b/pkg/executor/executor.go @@ -0,0 +1,65 @@ +package executor + +import ( + config "github.com/jamesread/OliveTin/pkg/config" + log "github.com/sirupsen/logrus" + pb "github.com/jamesread/OliveTin/gen/grpc" + + "errors" + "os/exec" + "context" + "time" + "fmt" +) + +var ( + Cfg *config.Config; +) + +func ExecAction(action string) (*pb.StartActionResponse) { + res := &pb.StartActionResponse{} + res.TimedOut = false; + + log.WithFields(log.Fields{ + "actionName": action, + }).Infof("StartAction") + + actualAction, err := findAction(action) + + if err != nil { + log.Errorf("Error finding action %s, %s", err, action) + return res + } + + log.Infof("Found action %s", actualAction.Title) + + ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) + defer cancel() + + cmd := exec.CommandContext(ctx, "sh", "-c", actualAction.Shell) + stdout, stderr := cmd.Output() + + res.Stdout = string(stdout) + res.Stderr = fmt.Sprintf("%s", stderr) + + if ctx.Err() == context.DeadlineExceeded { + res.TimedOut = true + } + + log.Infof("Command %v stdout %v", actualAction.Title, res.Stdout) + log.Infof("Command %v stderr %v", actualAction.Title, res.Stderr) + + return res +} + +func findAction(actionTitle string) (*config.ActionButton, error) { + for _, action := range Cfg.ActionButtons { + if action.Title == actionTitle { + return &action, nil + } + } + + return nil, errors.New("Action not found") +} + + diff --git a/pkg/grpcApi/grpcApi.go b/pkg/grpcApi/grpcApi.go index 52aa452..1323931 100644 --- a/pkg/grpcApi/grpcApi.go +++ b/pkg/grpcApi/grpcApi.go @@ -3,12 +3,19 @@ package grpcApi; import ( "google.golang.org/grpc" pb "github.com/jamesread/OliveTin/gen/grpc" - "fmt" "net" log "github.com/sirupsen/logrus" ctx "context" + + config "github.com/jamesread/OliveTin/pkg/config" + executor "github.com/jamesread/OliveTin/pkg/executor" ) +var ( + cfg *config.Config; +) + + type OliveTinApi struct { } @@ -16,9 +23,7 @@ type OliveTinApi struct { func (api *OliveTinApi) StartAction(ctx ctx.Context, req *pb.StartActionRequest) (*pb.StartActionResponse, error) { res := &pb.StartActionResponse{} - log.WithFields(log.Fields{ - "actionName": req.ActionName, - }).Infof("StartAction") + executor.ExecAction(req.ActionName) return res, nil } @@ -26,25 +31,23 @@ func (api *OliveTinApi) StartAction(ctx ctx.Context, req *pb.StartActionRequest) func (api *OliveTinApi) GetButtons(ctx ctx.Context, req *pb.GetButtonsRequest) (*pb.GetButtonsResponse, error) { res := &pb.GetButtonsResponse{}; - btn1 := pb.ActionButton { - Title: "foo", - Icon: "🇦", - }; + for _, action := range cfg.ActionButtons { + btn := pb.ActionButton { + Title: action.Title, + Icon: lookupHtmlIcon(action.Icon), + } - btn2 := pb.ActionButton { - Title: "bar", - Icon: "🇦", - }; + res.Actions = append(res.Actions, &btn); + } - res.Actions = append(res.Actions, &btn1); - res.Actions = append(res.Actions, &btn2); return res, nil } -func Start() { - port := 1337; - lis, err := net.Listen("tcp", fmt.Sprintf("0.0.0.0:%d", port)); +func Start(listenAddress string, globalConfig *config.Config) { + cfg = globalConfig + + lis, err := net.Listen("tcp", listenAddress); if err != nil { log.Fatalf("Failed to listen - %v", err); diff --git a/pkg/grpcApi/utils.go b/pkg/grpcApi/utils.go new file mode 100644 index 0000000..95138cd --- /dev/null +++ b/pkg/grpcApi/utils.go @@ -0,0 +1,14 @@ +package grpcApi + +var emojis = map[string]string { + "poop": "💩", + "smile": "😀", +} + +func lookupHtmlIcon(keyToLookup string) (string) { + if emoji, found := emojis[keyToLookup]; found { + return emoji; + } + + return keyToLookup; +} diff --git a/pkg/restApi/restapi.go b/pkg/restApi/restapi.go index 56de026..052a6aa 100644 --- a/pkg/restApi/restapi.go +++ b/pkg/restApi/restapi.go @@ -2,7 +2,6 @@ package restApi; import ( "google.golang.org/grpc" - "fmt" log "github.com/sirupsen/logrus" "context" "github.com/grpc-ecosystem/grpc-gateway/runtime" @@ -10,28 +9,32 @@ import ( "strings" gw "github.com/jamesread/OliveTin/gen/grpc" + + config "github.com/jamesread/OliveTin/pkg/config" +) + +var ( + cfg *config.Config; ) -func Start() (error) { - port := 1339; +func Start(listenAddressRest string, listenAddressGrpc string, globalConfig *config.Config) (error) { + cfg = globalConfig ctx := context.Background() ctx, cancel := context.WithCancel(ctx) defer cancel() - //lis, err := net.Listen("tcp", fmt.Sprintf("0.0.0.0:%d", port)); - mux := runtime.NewServeMux() opts := []grpc.DialOption{grpc.WithInsecure()} - err := gw.RegisterOliveTinApiHandlerFromEndpoint(ctx, mux, "127.0.0.1:1337", opts) + err := gw.RegisterOliveTinApiHandlerFromEndpoint(ctx, mux, listenAddressGrpc, opts) if err != nil { log.Fatalf("gw error %v", err) } - return http.ListenAndServe(fmt.Sprintf(":%d", port), allowCors(mux)) + return http.ListenAndServe(listenAddressRest, allowCors(mux)) } func allowCors(h http.Handler) http.Handler { diff --git a/pkg/webuiServer/webuiServer.go b/pkg/webuiServer/webuiServer.go new file mode 100644 index 0000000..4297d2c --- /dev/null +++ b/pkg/webuiServer/webuiServer.go @@ -0,0 +1,27 @@ +package staticWebserverForUi + +import ( + "net/http" + "encoding/json" + log "github.com/sirupsen/logrus" +) + +type WebUiSettings struct { + Rest string +} + +func Start(listenAddress string, listenAddressRest string) { + http.Handle("/", http.FileServer(http.Dir("webui"))) + + http.HandleFunc("/webUiSettings.json", func(w http.ResponseWriter, r *http.Request) { + ret := WebUiSettings { + Rest: "http://" + listenAddressRest + "/", + } + + jsonRet, _ := json.Marshal(ret) + + w.Write([]byte(jsonRet)); + }) + + log.Fatal(http.ListenAndServe(listenAddress, nil)) +} diff --git a/web/main.js b/web/main.js deleted file mode 100644 index b26b686..0000000 --- a/web/main.js +++ /dev/null @@ -1,9 +0,0 @@ -'use strict' - -import { loadContents } from './js/loader.js' - -window.fetch('http://mindstorm4:1339/GetButtons').then(res => { - return res.json() -}).then(res => { - loadContents(res) -}) diff --git a/web/.eslintrc.json b/webui/.eslintrc.json similarity index 100% rename from web/.eslintrc.json rename to webui/.eslintrc.json diff --git a/web/.stylelintrc.json b/webui/.stylelintrc.json similarity index 100% rename from web/.stylelintrc.json rename to webui/.stylelintrc.json diff --git a/web/OliveTinLogo.png b/webui/OliveTinLogo.png similarity index 100% rename from web/OliveTinLogo.png rename to webui/OliveTinLogo.png diff --git a/web/OliveTinLogo.svg b/webui/OliveTinLogo.svg similarity index 100% rename from web/OliveTinLogo.svg rename to webui/OliveTinLogo.svg diff --git a/web/index.htm b/webui/index.html similarity index 100% rename from web/index.htm rename to webui/index.html diff --git a/web/js/classes.js b/webui/js/classes.js similarity index 96% rename from web/js/classes.js rename to webui/js/classes.js index 5a1a2ae..779a38a 100644 --- a/web/js/classes.js +++ b/webui/js/classes.js @@ -5,7 +5,7 @@ class ActionButton extends window.HTMLButtonElement { this.stateLabels = [] this.currentState = 0 this.isWaiting = false - this.actionCallUrl = 'http://mindstorm4:1339/StartAction?actionName=' + this.title + this.actionCallUrl = window.restBaseUrl + 'StartAction?actionName=' + this.title if (json.icon !== undefined) { this.unicodeIcon = unescape(json.icon) diff --git a/web/js/loader.js b/webui/js/loader.js similarity index 100% rename from web/js/loader.js rename to webui/js/loader.js diff --git a/webui/main.js b/webui/main.js new file mode 100644 index 0000000..3edb073 --- /dev/null +++ b/webui/main.js @@ -0,0 +1,38 @@ +'use strict' + +import { loadContents } from './js/loader.js' + +/** + * Design choice; define this as a "global function" (on window) so that it can + * easily be used anywhere without needing to import it, as it's a pretty + * fundemental thing. + */ +window.showBigError = (type, friendlyType, message) => { + console.error("Error " + type + ": ", err) + + var err = document.createElement("div") + err.classList.add("error") + err.innerHTML = "

Error " + friendlyType + "

" + message + "

OliveTin Documentation

"; + + document.getElementById('rootGroup').appendChild(err) +} + +function onInitialLoad(res) { + window.restBaseUrl = res.Rest; + + window.fetch(window.restBaseUrl + "GetButtons").then(res => { + return res.json() + }).then(res => { + loadContents(res) + }).catch(err => { + showBigError("fetch-initial-buttons", "getting initial buttons", err, "blat") + }); +} + +window.fetch('webUiSettings.json').then(res => { + return res.json() +}).then(res => { + onInitialLoad(res) +}).catch(err => { + showBigError("fetch-webui-settings", "getting webui settings", err); +}) diff --git a/web/package.json b/webui/package.json similarity index 100% rename from web/package.json rename to webui/package.json diff --git a/web/style.css b/webui/style.css similarity index 96% rename from web/style.css rename to webui/style.css index 1e78793..dd232ae 100644 --- a/web/style.css +++ b/webui/style.css @@ -16,6 +16,11 @@ body { text-align: center; } +.error { + background-color: salmon; + padding: 1em; +} + div.entity { background-color: #222; box-shadow: 0 0 10px 0 #444;