Browse Source

Various updates in a ubercommit :-)

jamesread 5 năm trước cách đây
mục cha
commit
3d3999de42

+ 1 - 0
.gitignore

@@ -4,3 +4,4 @@ web/node_modules
 **/*.swo
 **/*.swo
 gen/
 gen/
 go.sum
 go.sum
+OliveTin

+ 1 - 1
Makefile

@@ -3,6 +3,6 @@ default:
 
 
 grpc:
 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/ --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
 .PHONY: grpc

+ 6 - 1
OliveTin.proto

@@ -18,7 +18,12 @@ message StartActionRequest {
 	string actionName = 1;
 	string actionName = 1;
 }
 }
 
 
-message StartActionResponse {}
+message StartActionResponse {
+	string stdout = 1;
+	string stderr = 2;
+	bool timedOut = 3;
+	int32 exitCode = 4;
+}
 
 
 service OliveTinApi {
 service OliveTinApi {
 	rpc GetButtons(GetButtonsRequest) returns (GetButtonsResponse) {
 	rpc GetButtons(GetButtonsRequest) returns (GetButtonsResponse) {

+ 8 - 0
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
 For example, it can be used to turn home automation lights on or off, or start
 workflows in n8n.  
 workflows in n8n.  
+
+## `config.yaml`
+
+```
+listenAddressRestActions: :1337 # Listen on all addresses available, port 1337
+listenAddressWebUi: :1339
+logLevel: "INFO"
+```

+ 14 - 6
cmd/OliveTin/main.go

@@ -5,8 +5,10 @@ import (
 
 
 	restApi "github.com/jamesread/OliveTin/pkg/restApi"
 	restApi "github.com/jamesread/OliveTin/pkg/restApi"
 	grpcApi "github.com/jamesread/OliveTin/pkg/grpcApi"
 	grpcApi "github.com/jamesread/OliveTin/pkg/grpcApi"
+	webuiServer "github.com/jamesread/OliveTin/pkg/webuiServer"
 
 
 	config "github.com/jamesread/OliveTin/pkg/config"
 	config "github.com/jamesread/OliveTin/pkg/config"
+	executor "github.com/jamesread/OliveTin/pkg/executor"
 	"github.com/spf13/viper"
 	"github.com/spf13/viper"
 	"os"
 	"os"
 )
 )
@@ -20,12 +22,14 @@ func init() {
 	log.SetLevel(log.DebugLevel) // Default to debug, to catch cfg issues
 	log.SetLevel(log.DebugLevel) // Default to debug, to catch cfg issues
 
 
 	viper.AutomaticEnv();
 	viper.AutomaticEnv();
+	viper.SetConfigName("config.yaml")
 	viper.SetConfigType("yaml")
 	viper.SetConfigType("yaml")
 	viper.AddConfigPath(".")
 	viper.AddConfigPath(".")
-	viper.AddConfigPath("/etc/olivetin/")
+	viper.AddConfigPath("/etc/OliveTin/")
 
 
 	if err := viper.ReadInConfig(); err != nil {
 	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();
 	cfg = config.DefaultConfig();
@@ -42,13 +46,17 @@ func init() {
 
 
 func main() {
 func main() {
 	log.WithFields(log.Fields {
 	log.WithFields(log.Fields {
-		"listenPortRestActions": cfg.ListenPortRestActions,
-		"listenPortWebUi": cfg.ListenPortWebUi,
+		"listenAddressGrpcActions": cfg.ListenAddressGrpcActions,
+		"listenAddressRestActions": cfg.ListenAddressRestActions,
+		"listenAddressWebUi": cfg.ListenAddressWebUi,
 	}).Info("OliveTin started");
 	}).Info("OliveTin started");
 
 
 	log.Debugf("%+v", cfg)
 	log.Debugf("%+v", cfg)
 
 
-	go grpcApi.Start()
+	executor.Cfg = cfg;
+
+	go grpcApi.Start(cfg.ListenAddressGrpcActions, cfg)
+	go restApi.Start(cfg.ListenAddressRestActions, cfg.ListenAddressGrpcActions, cfg)
 
 
-	restApi.Start();
+	webuiServer.Start(cfg.ListenAddressWebUi, cfg.ListenAddressRestActions)
 }
 }

+ 1 - 0
go.mod

@@ -13,6 +13,7 @@ require (
 	golang.org/x/net v0.0.0-20210119194325-5f4716e94777 // indirect
 	golang.org/x/net v0.0.0-20210119194325-5f4716e94777 // indirect
 	golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c // indirect
 	golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c // indirect
 	golang.org/x/text v0.3.5 // 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
 	golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
 	google.golang.org/genproto v0.0.0-20210224155714-063164c882e6
 	google.golang.org/genproto v0.0.0-20210224155714-063164c882e6
 	google.golang.org/grpc v1.37.0
 	google.golang.org/grpc v1.37.0

+ 8 - 4
pkg/config/config.go

@@ -8,6 +8,7 @@ import (
 type ActionButton struct {
 type ActionButton struct {
 	Title string
 	Title string
 	Icon string
 	Icon string
+	Shell string
 	Css map[string]string `mapstructure:omitempty`
 	Css map[string]string `mapstructure:omitempty`
 }
 }
 
 
@@ -19,8 +20,9 @@ type Entity struct {
 }
 }
 
 
 type Config struct {
 type Config struct {
-	ListenPortRestActions int
-	ListenPortWebUi int
+	ListenAddressWebUi string
+	ListenAddressRestActions string
+	ListenAddressGrpcActions string
 	LogLevel string
 	LogLevel string
 	ActionButtons []ActionButton `mapstructure:"actions"`
 	ActionButtons []ActionButton `mapstructure:"actions"`
 	Entities []Entity `mapstructure:omitempty`
 	Entities []Entity `mapstructure:omitempty`
@@ -28,8 +30,9 @@ type Config struct {
 
 
 func DefaultConfig() *Config {
 func DefaultConfig() *Config {
 	config := 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"
 	config.LogLevel = "INFO"
 
 
 	return &config
 	return &config
@@ -43,3 +46,4 @@ func (cfg *Config) GetLogLevel() (log.Level) {
 	default: return log.DebugLevel; 
 	default: return log.DebugLevel; 
 	}
 	}
 }
 }
+

+ 15 - 0
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")
+	}
+}

+ 65 - 0
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")
+}
+
+

+ 20 - 17
pkg/grpcApi/grpcApi.go

@@ -3,12 +3,19 @@ package grpcApi;
 import (
 import (
 	"google.golang.org/grpc"
 	"google.golang.org/grpc"
 	pb "github.com/jamesread/OliveTin/gen/grpc"
 	pb "github.com/jamesread/OliveTin/gen/grpc"
-	"fmt"
 	"net"
 	"net"
 	log "github.com/sirupsen/logrus"
 	log "github.com/sirupsen/logrus"
 	ctx "context"
 	ctx "context"
+
+	config "github.com/jamesread/OliveTin/pkg/config"
+	executor "github.com/jamesread/OliveTin/pkg/executor"
+)
+
+var (
+	cfg *config.Config;
 )
 )
 
 
+
 type OliveTinApi struct {
 type OliveTinApi struct {
 
 
 }
 }
@@ -16,9 +23,7 @@ type OliveTinApi struct {
 func (api *OliveTinApi) StartAction(ctx ctx.Context, req *pb.StartActionRequest) (*pb.StartActionResponse, error) {
 func (api *OliveTinApi) StartAction(ctx ctx.Context, req *pb.StartActionRequest) (*pb.StartActionResponse, error) {
 	res := &pb.StartActionResponse{}
 	res := &pb.StartActionResponse{}
 
 
-	log.WithFields(log.Fields{
-		"actionName": req.ActionName,
-	}).Infof("StartAction")
+	executor.ExecAction(req.ActionName)
 
 
 	return res, nil
 	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) {
 func (api *OliveTinApi) GetButtons(ctx ctx.Context, req *pb.GetButtonsRequest) (*pb.GetButtonsResponse, error) {
 	res := &pb.GetButtonsResponse{};
 	res := &pb.GetButtonsResponse{};
 
 
-	btn1 := pb.ActionButton {
-		Title: "foo",
-		Icon: "&#x1F1E6",
-	};
+	for _, action := range cfg.ActionButtons {
+		btn := pb.ActionButton {
+			Title: action.Title,
+			Icon: lookupHtmlIcon(action.Icon),
+		}
 
 
-	btn2 := pb.ActionButton {
-		Title: "bar",
-		Icon: "&#x1F1E6",
-	};
+		res.Actions = append(res.Actions, &btn);
+	}
 
 
-	res.Actions = append(res.Actions, &btn1);
-	res.Actions = append(res.Actions, &btn2);
 
 
 	return res, nil
 	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 {
 	if err != nil {
 		log.Fatalf("Failed to listen - %v", err);
 		log.Fatalf("Failed to listen - %v", err);

+ 14 - 0
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;
+}

+ 10 - 7
pkg/restApi/restapi.go

@@ -2,7 +2,6 @@ package restApi;
 
 
 import (
 import (
 	"google.golang.org/grpc"
 	"google.golang.org/grpc"
-	"fmt"
 	log "github.com/sirupsen/logrus"
 	log "github.com/sirupsen/logrus"
 	"context"
 	"context"
 	"github.com/grpc-ecosystem/grpc-gateway/runtime"
 	"github.com/grpc-ecosystem/grpc-gateway/runtime"
@@ -10,28 +9,32 @@ import (
 	"strings"
 	"strings"
 
 
 	gw "github.com/jamesread/OliveTin/gen/grpc"
 	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 := context.Background()
 	ctx, cancel := context.WithCancel(ctx)
 	ctx, cancel := context.WithCancel(ctx)
 	defer cancel()
 	defer cancel()
 
 
-	//lis, err := net.Listen("tcp", fmt.Sprintf("0.0.0.0:%d", port));
-
 	mux := runtime.NewServeMux()
 	mux := runtime.NewServeMux()
 	opts := []grpc.DialOption{grpc.WithInsecure()}
 	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 {
 	if err != nil {
 		log.Fatalf("gw error %v", err)
 		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 {
 func allowCors(h http.Handler) http.Handler {

+ 27 - 0
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))
+}

+ 0 - 9
web/main.js

@@ -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)
-})

+ 0 - 0
web/.eslintrc.json → webui/.eslintrc.json


+ 0 - 0
web/.stylelintrc.json → webui/.stylelintrc.json


+ 0 - 0
web/OliveTinLogo.png → webui/OliveTinLogo.png


+ 0 - 0
web/OliveTinLogo.svg → webui/OliveTinLogo.svg


+ 0 - 0
web/index.htm → webui/index.html


+ 1 - 1
web/js/classes.js → webui/js/classes.js

@@ -5,7 +5,7 @@ class ActionButton extends window.HTMLButtonElement {
     this.stateLabels = []
     this.stateLabels = []
     this.currentState = 0
     this.currentState = 0
     this.isWaiting = false
     this.isWaiting = false
-    this.actionCallUrl = 'http://mindstorm4:1339/StartAction?actionName=' + this.title
+    this.actionCallUrl = window.restBaseUrl + 'StartAction?actionName=' + this.title
 
 
     if (json.icon !== undefined) {
     if (json.icon !== undefined) {
       this.unicodeIcon = unescape(json.icon)
       this.unicodeIcon = unescape(json.icon)

+ 0 - 0
web/js/loader.js → webui/js/loader.js


+ 38 - 0
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 = "<h1>Error " + friendlyType + "</h1><p>" + message + "</p><p><a href = 'http://github.com/jamesread/OliveTin' target = 'blank'/>OliveTin Documentation</a></p>";
+
+    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);
+})

+ 0 - 0
web/package.json → webui/package.json


+ 5 - 0
web/style.css → webui/style.css

@@ -16,6 +16,11 @@ body {
 	text-align: center;
 	text-align: center;
 }
 }
 
 
+.error {
+	background-color: salmon;
+	padding: 1em;
+}
+
 div.entity {
 div.entity {
 	background-color: #222;
 	background-color: #222;
 	box-shadow: 0 0 10px 0 #444;
 	box-shadow: 0 0 10px 0 #444;