Przeglądaj źródła

Merge branch 'websocket'

jamesread 2 lat temu
rodzic
commit
07bd09473c

+ 21 - 4
OliveTin.proto

@@ -9,13 +9,13 @@ message Action {
 	string title = 2;
 	string icon = 3;
 	bool canExec = 4;
-
 	repeated ActionArgument arguments = 5;
+	bool popupOnStart = 6;
 }
 
 message ActionArgument {
 	string name = 1;
-	string title = 2; 
+	string title = 2;
 	string type = 3;
 	string defaultValue = 4;
 
@@ -55,13 +55,13 @@ message StartActionArgument {
 }
 
 message StartActionResponse {
-	LogEntry logEntry = 1;
+	string executionUuid = 2;
 }
 
 message GetLogsRequest{};
 
 message LogEntry {
-	string datetime = 1;
+	string datetimeStarted = 1;
 	string actionTitle = 2;
 	string stdout = 3;
 	string stderr = 4;
@@ -71,6 +71,8 @@ message LogEntry {
 	string userClass = 8;
 	string actionIcon = 9;
 	repeated string tags = 10;
+	string executionUuid = 11;
+	string datetimeFinished = 12;
 }
 
 message GetLogsResponse {
@@ -87,6 +89,14 @@ message ValidateArgumentTypeResponse {
 	string description = 2;
 }
 
+message WatchExecutionRequest {
+	string executionUuid = 1;
+}
+
+message WatchExecutionUpdate {
+	string update = 1;
+}
+
 message WhoAmIRequest {}
 
 message WhoAmIResponse {
@@ -113,6 +123,13 @@ service OliveTinApi {
 		};
 	}
 
+	rpc WatchExecution(WatchExecutionRequest) returns (stream WatchExecutionUpdate) {
+		option (google.api.http) = {
+			post: "/api/WatchExecution"
+			body: "*"
+		};
+	}
+
 	rpc GetLogs(GetLogsRequest) returns (GetLogsResponse) {
 		option (google.api.http) = {
 			get: "/api/GetLogs"

+ 1 - 0
go.mod

@@ -20,6 +20,7 @@ require (
 	google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.3.0
 	google.golang.org/protobuf v1.31.0
 	gopkg.in/yaml.v3 v3.0.1
+	nhooyr.io/websocket v1.8.7
 )
 
 require (

+ 41 - 0
go.sum

@@ -94,6 +94,10 @@ github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4
 github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw=
 github.com/fzipp/gocyclo v0.6.0 h1:lsblElZG7d3ALtGMx9fmxeTKZaLLpU8mET09yN4BBLo=
 github.com/fzipp/gocyclo v0.6.0/go.mod h1:rXPyn8fnlpa0R2csP/31uerbiVBugk5whMdlyaLkLoA=
+github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
+github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
+github.com/gin-gonic/gin v1.6.3 h1:ahKqKTFpO5KTPHxWZjEdPScmYaGtLo8Y4DMHoEsnp14=
+github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M=
 github.com/go-chi/chi/v5 v5.0.10 h1:rLz5avzKpjqxrYwXNfmjkrYYXOyLJd37pz53UFHC6vk=
 github.com/go-chi/chi/v5 v5.0.10/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
 github.com/go-critic/go-critic v0.8.2 h1:mekhZ9jw5NBEj3I8o/EywXw5zBfGAJuMo4VVVjtxF80=
@@ -106,6 +110,13 @@ github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ=
 github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
 github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
 github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
+github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
+github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q=
+github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
+github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no=
+github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=
+github.com/go-playground/validator/v10 v10.2.0 h1:KgJ0snyC2R9VXYN2rneOtQcw5aHQB1Vv0sFl1UcHBOY=
+github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI=
 github.com/go-toolsmith/astcast v1.1.0 h1:+JN9xZV1A+Re+95pgnMgDboWNVnIMMQXwfBwLRPgSC8=
 github.com/go-toolsmith/astcast v1.1.0/go.mod h1:qdcuFWeGGS2xX5bLM/c3U9lewg7+Zu4mr+xPwZIB4ZU=
 github.com/go-toolsmith/astcopy v1.1.0 h1:YGwBN0WM+ekI/6SS6+52zLDEf8Yvp3n2seZITCUBt5s=
@@ -124,6 +135,12 @@ github.com/go-toolsmith/strparse v1.1.0 h1:GAioeZUK9TGxnLS+qfdqNbA4z0SSm5zVNtCQi
 github.com/go-toolsmith/strparse v1.1.0/go.mod h1:7ksGy58fsaQkGQlY8WVoBFNyEPMGuJin1rfoPS4lBSQ=
 github.com/go-toolsmith/typep v1.1.0 h1:fIRYDyF+JywLfqzyhdiHzRop/GQDxxNhLGQ6gFUNHus=
 github.com/go-toolsmith/typep v1.1.0/go.mod h1:fVIw+7zjdsMxDA3ITWnH1yOiw1rnTQKCsF/sk2H/qig=
+github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee h1:s+21KNqlpePfkah2I+gwHF8xmJWRjooY+5248k6m4A0=
+github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo=
+github.com/gobwas/pool v0.2.0 h1:QEmUOlnSjWtnpRGHF3SauEiOsy82Cup83Vf2LcMlnc8=
+github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
+github.com/gobwas/ws v1.0.2 h1:CoAavW/wd/kulfZmSIBt6p24n4j7tHgNVCjsfHVNUbo=
+github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM=
 github.com/gofrs/flock v0.8.1 h1:+gYjHKf32LDeiEEFhQaotPbLuUXjY5ZqxKgXy7n59aw=
 github.com/gofrs/uuid/v5 v5.0.0 h1:p544++a97kEL+svbcFbCQVM9KFu0Yo25UoISXGNNH9M=
 github.com/gofrs/uuid/v5 v5.0.0/go.mod h1:CDOjlDMVAtN56jqyRUZh58JT31Tiw7/oQyEXZV+9bD8=
@@ -177,6 +194,7 @@ github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeN
 github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
 github.com/google/go-containerregistry v0.16.1 h1:rUEt426sR6nyrL3gt+18ibRcvYpKYdpsa5ZW7MA08dQ=
 github.com/google/go-containerregistry v0.16.1/go.mod h1:u0qB2l7mvtWVR5kNcbFIhFY1hLbf8eeGapA+vbFDCtQ=
+github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
 github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
 github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
 github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
@@ -200,6 +218,8 @@ github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+
 github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
 github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
 github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g=
+github.com/gorilla/websocket v1.4.1 h1:q7AeDBpnBk8AogcD4DSag/Ukw/KV+YhzLj2bP5HvKCM=
+github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
 github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.2 h1:dygLcbEBA+t/P7ck6a8AkXv6juQ4cK0RHBoh32jxhHM=
 github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.2/go.mod h1:Ap9RLCIJVtgQg1/BBgVEfypOAySvvlcpcVQkSzJCH4Y=
 github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
@@ -214,10 +234,13 @@ github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLf
 github.com/jdxcode/netrc v0.0.0-20221124155335-4616370d1a84 h1:2uT3aivO7NVpUPGcQX7RbHijHMyWix/yCnIrCWc+5co=
 github.com/jdxcode/netrc v0.0.0-20221124155335-4616370d1a84/go.mod h1:Zi/ZFkEqFHTm7qkjyNJjaWH4LQA9LQhGJyF0lTYGpxw=
 github.com/jhump/protoreflect v1.15.1 h1:HUMERORf3I3ZdX05WaQ6MIpd/NJ434hTp5YiKgfCL6c=
+github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
+github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
 github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
 github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
 github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
 github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
+github.com/klauspost/compress v1.10.3/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
 github.com/klauspost/compress v1.16.7 h1:2mk3MPGNzKyxErAw8YaohYh69+pa4sIQSC0fPGCFR9I=
 github.com/klauspost/compress v1.16.7/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
 github.com/klauspost/pgzip v1.2.6 h1:8RXeL5crjEUFnR2/Sn6GJNWtSQ3Dk8pq4CL3jvdDyjU=
@@ -228,14 +251,22 @@ github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
 github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
 github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
 github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
+github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y=
+github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
 github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
 github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
+github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
+github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng=
 github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
 github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
 github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
 github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
 github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0=
 github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y=
+github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
+github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
+github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
+github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
 github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
 github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
 github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
@@ -289,6 +320,7 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+
 github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
 github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
 github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
+github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
 github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
 github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
 github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
@@ -301,6 +333,10 @@ github.com/subosito/gotenv v1.4.2 h1:X1TuBLAMDFbaTAChgCBLu3DU3UPyELpnF2jjJ2cz/S8
 github.com/subosito/gotenv v1.4.2/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0=
 github.com/tetratelabs/wazero v1.4.0 h1:9/MirYvmkJ/zSUOygKY/ia3t+e+RqIZXKbylIby1WYk=
 github.com/tetratelabs/wazero v1.4.0/go.mod h1:0U0G41+ochRKoPKCJlh0jMg1CHkyfK8kDqiirMmKY8A=
+github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo=
+github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
+github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs=
+github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
 github.com/vbatts/tar-split v0.11.5 h1:3bHCTIheBm1qFTcgh9oPu+nNBtX+XJIupG/vacinCts=
 github.com/vbatts/tar-split v0.11.5/go.mod h1:yZbwRsSeGjusneWgA781EKej9HF8vme8okylkAeNKLk=
 github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
@@ -446,6 +482,7 @@ golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7w
 golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -654,6 +691,8 @@ gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
 gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
 gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
 gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
+gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
 gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
@@ -665,6 +704,8 @@ honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWh
 honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
 honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
 honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
+nhooyr.io/websocket v1.8.7 h1:usjR2uOr/zjjkVMy0lW+PPohFok7PCow5sDjLgX4P4g=
+nhooyr.io/websocket v1.8.7/go.mod h1:B70DZP8IakI65RVQ51MsWP/8jndNma26DVA/nFSCgW0=
 rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
 rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
 rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=

+ 4 - 1
internal/acl/acl.go

@@ -75,7 +75,10 @@ func getMetdataKeyOrEmpty(md metadata.MD, key string) string {
 func UserFromContext(ctx context.Context, cfg *config.Config) *AuthenticatedUser {
 	md, ok := metadata.FromIncomingContext(ctx)
 
-	ret := &AuthenticatedUser{}
+	ret := &AuthenticatedUser{
+		Username:  "guest",
+		Usergroup: "guest",
+	}
 
 	if ok {
 		ret.Username = getMetdataKeyOrEmpty(md, "username")

+ 1 - 0
internal/config/config.go

@@ -13,6 +13,7 @@ type Action struct {
 	ExecOnStartup bool
 	ExecOnCron    []string
 	Arguments     []ActionArgument
+	PopupOnStart  bool
 }
 
 // ActionArgument objects appear on Actions.

+ 59 - 39
internal/executor/executor.go

@@ -4,10 +4,12 @@ import (
 	pb "github.com/OliveTin/OliveTin/gen/grpc"
 	acl "github.com/OliveTin/OliveTin/internal/acl"
 	config "github.com/OliveTin/OliveTin/internal/config"
+	"github.com/google/uuid"
 	log "github.com/sirupsen/logrus"
 
 	"bytes"
 	"context"
+	"io"
 	"os/exec"
 	"runtime"
 	"time"
@@ -24,18 +26,22 @@ type ExecutionRequest struct {
 	AuthenticatedUser  *acl.AuthenticatedUser
 	logEntry           *InternalLogEntry
 	finalParsedCommand string
+	uuid               string
 }
 
 // InternalLogEntry objects are created by an Executor, and represent the final
 // state of execution (even if the command is not executed). It's designed to be
 // easily serializable.
 type InternalLogEntry struct {
-	Datetime string
-	Stdout   string
-	Stderr   string
-	TimedOut bool
-	ExitCode int32
-	Tags     []string
+	DatetimeStarted  string
+	DatetimeFinished string
+	Stdout           string
+	Stderr           string
+	StdoutBuffer     io.ReadCloser
+	StderrBuffer     io.ReadCloser
+	TimedOut         bool
+	ExitCode         int32
+	Tags             []string
 
 	/*
 		The following two properties are obviously on Action normally, but it's useful
@@ -44,6 +50,9 @@ type InternalLogEntry struct {
 	*/
 	ActionTitle string
 	ActionIcon  string
+
+	ExecutionStarted   bool
+	ExecutionCompleted bool
 }
 
 type executorStepFunc func(*ExecutionRequest) bool
@@ -51,46 +60,17 @@ type executorStepFunc func(*ExecutionRequest) bool
 // Executor represents a helper class for executing commands. It's main method
 // is ExecRequest
 type Executor struct {
-	Logs []InternalLogEntry
+	Logs map[string]*InternalLogEntry
 
 	chainOfCommand []executorStepFunc
 }
 
-// ExecRequest processes an ExecutionRequest
-func (e *Executor) ExecRequest(req *ExecutionRequest) *pb.StartActionResponse {
-	req.logEntry = &InternalLogEntry{
-		Datetime:    time.Now().Format("2006-01-02 15:04:05"),
-		ActionTitle: req.ActionName,
-		Stdout:      "",
-		Stderr:      "",
-		ExitCode:    -1337, // If an Action is not actually executed, this is the default exit code.
-	}
-
-	for _, step := range e.chainOfCommand {
-		if !step(req) {
-			break
-		}
-	}
-
-	e.Logs = append(e.Logs, *req.logEntry)
-
-	return &pb.StartActionResponse{
-		LogEntry: &pb.LogEntry{
-			ActionTitle: req.logEntry.ActionTitle,
-			ActionIcon:  req.logEntry.ActionIcon,
-			Datetime:    req.logEntry.Datetime,
-			Stderr:      req.logEntry.Stderr,
-			Stdout:      req.logEntry.Stdout,
-			TimedOut:    req.logEntry.TimedOut,
-			ExitCode:    req.logEntry.ExitCode,
-		},
-	}
-}
-
 // DefaultExecutor returns an Executor, with a sensible "chain of command" for
 // executing actions.
 func DefaultExecutor() *Executor {
 	e := Executor{}
+	e.Logs = make(map[string]*InternalLogEntry)
+
 	e.chainOfCommand = []executorStepFunc{
 		stepFindAction,
 		stepACLCheck,
@@ -103,6 +83,36 @@ func DefaultExecutor() *Executor {
 	return &e
 }
 
+// ExecRequest processes an ExecutionRequest
+func (e *Executor) ExecRequest(req *ExecutionRequest) *pb.StartActionResponse {
+	req.uuid = uuid.New().String()
+	req.logEntry = &InternalLogEntry{
+		DatetimeStarted:    time.Now().Format("2006-01-02 15:04:05"),
+		ActionTitle:        req.ActionName,
+		Stdout:             "",
+		Stderr:             "",
+		ExitCode:           -1337, // If an Action is not actually executed, this is the default exit code.
+		ExecutionStarted:   false,
+		ExecutionCompleted: false,
+	}
+
+	e.Logs[req.uuid] = req.logEntry
+
+	go e.execChain(req)
+
+	return &pb.StartActionResponse{
+		ExecutionUuid: req.uuid,
+	}
+}
+
+func (e *Executor) execChain(req *ExecutionRequest) {
+	for _, step := range e.chainOfCommand {
+		if !step(req) {
+			break
+		}
+	}
+}
+
 func stepFindAction(req *ExecutionRequest) bool {
 	actualAction := req.Cfg.FindAction(req.ActionName)
 
@@ -181,9 +191,19 @@ func stepExec(req *ExecutionRequest) bool {
 	cmd := wrapCommandInShell(ctx, req.finalParsedCommand)
 	cmd.Stdout = &stdout
 	cmd.Stderr = &stderr
+	req.logEntry.StdoutBuffer, _ = cmd.StdoutPipe()
+	req.logEntry.StderrBuffer, _ = cmd.StderrPipe()
+
+	req.logEntry.ExecutionStarted = true
+
+	runerr := cmd.Start()
+
+	cmd.Wait()
 
-	runerr := cmd.Run()
+	//req.logEntry.Stdout = req.logEntry.StdoutBuffer.String()
+	//req.logEntry.Stderr = req.logEntry.StderrBuffer.String()
 
+	req.logEntry.ExecutionCompleted = true
 	req.logEntry.ExitCode = int32(cmd.ProcessState.ExitCode())
 	req.logEntry.Stdout = stdout.String()
 	req.logEntry.Stderr = stderr.String()

+ 38 - 9
internal/grpcapi/grpcApi.go

@@ -6,6 +6,7 @@ import (
 	log "github.com/sirupsen/logrus"
 	"google.golang.org/grpc"
 
+	"io"
 	"net"
 
 	acl "github.com/OliveTin/OliveTin/internal/acl"
@@ -43,6 +44,32 @@ func (api *oliveTinAPI) StartAction(ctx ctx.Context, req *pb.StartActionRequest)
 	return api.executor.ExecRequest(&execReq), nil
 }
 
+func (api *oliveTinAPI) WatchExecution(req *pb.WatchExecutionRequest, srv pb.OliveTinApi_WatchExecutionServer) error {
+	log.Infof("Watch")
+
+	if logEntry, ok := api.executor.Logs[req.ExecutionUuid]; !ok {
+		log.Errorf("Execution not found: %v", req.ExecutionUuid)
+
+		return nil
+	} else {
+		if logEntry.ExecutionStarted {
+			for !logEntry.ExecutionCompleted {
+				tmp := make([]byte, 256)
+
+				red, err := io.ReadAtLeast(logEntry.StdoutBuffer, tmp, 1)
+
+				log.Infof("%v %v", red, err)
+
+				srv.Send(&pb.WatchExecutionUpdate{
+					Update: string(tmp),
+				})
+			}
+		}
+
+		return nil
+	}
+}
+
 func (api *oliveTinAPI) GetDashboardComponents(ctx ctx.Context, req *pb.GetDashboardComponentsRequest) (*pb.GetDashboardComponentsResponse, error) {
 	user := acl.UserFromContext(ctx, cfg)
 
@@ -62,16 +89,18 @@ func (api *oliveTinAPI) GetLogs(ctx ctx.Context, req *pb.GetLogsRequest) (*pb.Ge
 
 	// TODO Limit to 10 entries or something to prevent browser lag.
 
-	for _, logEntry := range api.executor.Logs {
+	for uuid, logEntry := range api.executor.Logs {
 		ret.Logs = append(ret.Logs, &pb.LogEntry{
-			ActionTitle: logEntry.ActionTitle,
-			ActionIcon:  logEntry.ActionIcon,
-			Datetime:    logEntry.Datetime,
-			Stdout:      logEntry.Stdout,
-			Stderr:      logEntry.Stderr,
-			TimedOut:    logEntry.TimedOut,
-			ExitCode:    logEntry.ExitCode,
-			Tags:        logEntry.Tags,
+			ActionTitle:      logEntry.ActionTitle,
+			ActionIcon:       logEntry.ActionIcon,
+			DatetimeStarted:  logEntry.DatetimeStarted,
+			DatetimeFinished: logEntry.DatetimeFinished,
+			Stdout:           logEntry.Stdout,
+			Stderr:           logEntry.Stderr,
+			TimedOut:         logEntry.TimedOut,
+			ExitCode:         logEntry.ExitCode,
+			Tags:             logEntry.Tags,
+			ExecutionUuid:    uuid,
 		})
 	}
 

+ 5 - 4
internal/grpcapi/grpcApiActions.go

@@ -25,10 +25,11 @@ func actionsCfgToPb(cfgActions []config.Action, user *acl.AuthenticatedUser) *pb
 
 func actionCfgToPb(action config.Action, user *acl.AuthenticatedUser) *pb.Action {
 	btn := pb.Action{
-		Id:      fmt.Sprintf("%x", md5.Sum([]byte(action.Title))),
-		Title:   action.Title,
-		Icon:    action.Icon,
-		CanExec: acl.IsAllowedExec(cfg, user, &action),
+		Id:           fmt.Sprintf("%x", md5.Sum([]byte(action.Title))),
+		Title:        action.Title,
+		Icon:         action.Icon,
+		CanExec:      acl.IsAllowedExec(cfg, user, &action),
+		PopupOnStart: action.PopupOnStart,
 	}
 
 	for _, cfgArg := range action.Arguments {

+ 7 - 2
internal/httpservers/singleFrontend.go

@@ -14,6 +14,7 @@ import (
 	"net/http"
 	"net/http/httputil"
 	"net/url"
+	"strings"
 )
 
 // StartSingleHTTPFrontend will create a reverse proxy that proxies the API
@@ -37,8 +38,12 @@ func StartSingleHTTPFrontend(cfg *config.Config) {
 	})
 
 	mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
-		log.Debugf("ui req: %q", r.URL)
-		webuiProxy.ServeHTTP(w, r)
+		if strings.Contains(r.Header.Get("Connection"), "Upgrade") {
+			handleWebsocket(w, r)
+		} else {
+			log.Debugf("ui req: %q", r.URL)
+			webuiProxy.ServeHTTP(w, r)
+		}
 	})
 
 	srv := &http.Server{

+ 40 - 0
internal/httpservers/websocket.go

@@ -0,0 +1,40 @@
+package httpservers
+
+import (
+	"time"
+	log "github.com/sirupsen/logrus"
+	"net/http"
+	"nhooyr.io/websocket"
+	"nhooyr.io/websocket/wsjson"
+	"context"
+)
+
+func handleWebsocket(w http.ResponseWriter, r *http.Request) bool {
+	c, err := websocket.Accept(w, r, nil)
+
+	if err != nil {
+		log.Warnf("Websocket issue: %v", err)
+		return false
+	}
+
+	defer c.Close(websocket.StatusInternalError, "Goodbye")
+
+	ctx, cancel := context.WithTimeout(r.Context(), time.Second*10)
+
+	defer cancel()
+
+	var v interface{}
+
+	err = wsjson.Read(ctx, c, v)
+
+	log.Printf("recv: %v", v)
+
+	if err != nil {
+		log.Warnf("Websocket issue: %v", err)
+		return false
+	}
+
+
+	c.Close(websocket.StatusNormalClosure, "")
+	return true
+}

+ 3 - 0
internal/httpservers/webuiServer_test.go

@@ -4,11 +4,14 @@ import (
 	"github.com/stretchr/testify/assert"
 	"os"
 	"testing"
+	config "github.com/OliveTin/OliveTin/internal/config"
 )
 
 func TestGetWebuiDir(t *testing.T) {
 	os.Chdir("../../") // go test sets the cwd to "httpservers" by default
 
+	cfg = config.DefaultConfig();
+
 	dir := findWebuiDir()
 
 	assert.Equal(t, "./webui", dir, "Finding the webui dir")

+ 1 - 0
webui/index.html

@@ -146,5 +146,6 @@
 		</script>
 
 		<script type = "module" src = "main.js"></script>
+		<script type = "module" src = "js/websocket.js"></script>
 	</body>
 </html>

+ 39 - 0
webui/js/websocket.js

@@ -0,0 +1,39 @@
+export function setupWebsocket () {
+  window.websocketAvailable = false
+
+  let proto = 'ws:'
+
+  if (window.location.protocol === 'https:') {
+    proto = 'wss:'
+  }
+
+  const websocketConnectionUrl = proto + window.location.host + '/websocket'
+  const ws = window.ws = new WebSocket(websocketConnectionUrl)
+
+  ws.addEventListener('open', websocketOnOpen)
+  ws.addEventListener('message', websocketOnMessage)
+  ws.addEventListener('error', websocketOnError)
+  ws.addEventListener('close', websocketOnClose)
+}
+
+function websocketOnOpen (evt) {
+  window.websocketAvailable = true
+  console.log('open')
+
+  const foo = '{}'
+
+  ws.send(foo)
+}
+
+function websocketOnMessage (msg) {
+  console.log(msg)
+}
+
+function websocketOnError (err) {
+  window.websocketAvailable = false
+  console.log(err)
+}
+
+function websocketOnClose () {
+  window.websocketAvailable = false
+}

+ 3 - 0
webui/main.js

@@ -1,6 +1,7 @@
 'use strict'
 
 import { marshalActionButtonsJsonToHtml, marshalLogsJsonToHtml } from './js/marshaller.js'
+import { setupWebsocket } from './js/websocket.js'
 
 function showSection (name) {
   for (const otherName of ['Actions', 'Logs']) {
@@ -75,6 +76,8 @@ function processWebuiSettingsJson (settings) {
 function main () {
   setupSections()
 
+  setupWebsocket()
+
   window.fetch('webUiSettings.json').then(res => {
     return res.json()
   }).then(res => {