diff --git a/Taskfile.yml b/Taskfile.yml index 45a43bae4..a7559c2f3 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -346,7 +346,8 @@ tasks: method: timestamp cmds: # Generates both .ico and .icns files - - wails3 generate icons -input appicon.png + # commented out for now + # - wails3 generate icons -input appicon.png install:frontend:deps: summary: Install frontend dependencies diff --git a/build/appicon.png b/build/appicon.png index c40e57904..e6852fb3e 100644 Binary files a/build/appicon.png and b/build/appicon.png differ diff --git a/build/icon.ico b/build/icon.ico new file mode 100644 index 000000000..523704eaa Binary files /dev/null and b/build/icon.ico differ diff --git a/build/icons.icns b/build/icons.icns index 79cd56e67..be5c9df50 100644 Binary files a/build/icons.icns and b/build/icons.icns differ diff --git a/db/migrations-wstore/000001_init.down.sql b/db/migrations-wstore/000001_init.down.sql new file mode 100644 index 000000000..9e6fc6d60 --- /dev/null +++ b/db/migrations-wstore/000001_init.down.sql @@ -0,0 +1,8 @@ +DROP TABLE db_client; + +DROP TABLE db_workspace; + +DROP TABLE db_tab; + +DROP TABLE db_block; + diff --git a/db/migrations-wstore/000001_init.up.sql b/db/migrations-wstore/000001_init.up.sql new file mode 100644 index 000000000..8880f644e --- /dev/null +++ b/db/migrations-wstore/000001_init.up.sql @@ -0,0 +1,20 @@ +CREATE TABLE db_client ( + clientid varchar(36) PRIMARY KEY, -- unnecessary, but useful to have a PK + data json NOT NULL +); + +CREATE TABLE db_workspace ( + workspaceid varchar(36) PRIMARY KEY, + data json NOT NULL +); + +CREATE TABLE db_tab ( + tabid varchar(36) PRIMARY KEY, + data json NOT NULL +); + +CREATE TABLE db_block ( + blockid varchar(36) PRIMARY KEY, + tabid varchar(36) NOT NULL, -- the tab this block belongs to + data json NOT NULL +); diff --git a/frontend/app/store/global.ts b/frontend/app/store/global.ts index 5cb0d7364..3c10cfa7b 100644 --- a/frontend/app/store/global.ts +++ b/frontend/app/store/global.ts @@ -8,7 +8,7 @@ import * as rxjs from "rxjs"; import type { WailsEvent } from "@wailsio/runtime/types/events"; import { Events } from "@wailsio/runtime"; import { produce } from "immer"; -import * as BlockService from "@/bindings/pkg/service/blockservice/BlockService"; +import { BlockService } from "@/bindings/blockservice"; const globalStore = jotai.createStore(); diff --git a/frontend/app/view/preview.tsx b/frontend/app/view/preview.tsx index 4724c6cf8..3390cd896 100644 --- a/frontend/app/view/preview.tsx +++ b/frontend/app/view/preview.tsx @@ -5,7 +5,7 @@ import * as React from "react"; import * as jotai from "jotai"; import { atoms, blockDataMap, useBlockAtom } from "@/store/global"; import { Markdown } from "@/element/markdown"; -import * as FileService from "@/bindings/pkg/service/fileservice/FileService"; +import { FileService, FileInfo, FullFile } from "@/bindings/fileservice"; import * as util from "@/util/util"; import { CenteredDiv } from "../element/quickelems"; import { DirectoryTable } from "@/element/directorytable"; diff --git a/frontend/app/view/term.tsx b/frontend/app/view/term.tsx index 53544e2a4..d8d8368ab 100644 --- a/frontend/app/view/term.tsx +++ b/frontend/app/view/term.tsx @@ -7,7 +7,7 @@ import { Terminal } from "@xterm/xterm"; import type { ITheme } from "@xterm/xterm"; import { FitAddon } from "@xterm/addon-fit"; import { Button } from "@/element/button"; -import * as BlockService from "@/bindings/pkg/service/blockservice/BlockService"; +import { BlockService } from "@/bindings/blockservice"; import { getBlockSubject } from "@/store/global"; import { base64ToArray } from "@/util/util"; diff --git a/frontend/app/workspace/workspace.tsx b/frontend/app/workspace/workspace.tsx index 4b5384de2..e61c67f52 100644 --- a/frontend/app/workspace/workspace.tsx +++ b/frontend/app/workspace/workspace.tsx @@ -7,7 +7,7 @@ import { TabContent } from "@/app/tab/tab"; import { clsx } from "clsx"; import { atoms, addBlockIdToTab, blockDataMap } from "@/store/global"; import { v4 as uuidv4 } from "uuid"; -import * as BlockService from "@/bindings/pkg/service/blockservice/BlockService"; +import { BlockService } from "@/bindings/blockservice"; import "./workspace.less"; @@ -49,7 +49,7 @@ function Widgets() { async function createBlock(blockDef: BlockDef) { const rtOpts = { termsize: { rows: 25, cols: 80 } }; - const rtnBlock: BlockData = await BlockService.CreateBlock(blockDef, rtOpts); + const rtnBlock: BlockData = (await BlockService.CreateBlock(blockDef, rtOpts)) as BlockData; const newBlockAtom = jotai.atom(rtnBlock); blockDataMap.set(rtnBlock.blockid, newBlockAtom); addBlockIdToTab(activeTabId, rtnBlock.blockid); diff --git a/frontend/types/custom.d.ts b/frontend/types/custom.d.ts index a67689421..f3f95c4c4 100644 --- a/frontend/types/custom.d.ts +++ b/frontend/types/custom.d.ts @@ -33,21 +33,6 @@ declare global { files?: FileDef[]; meta?: MetaDataType; }; - - type FileInfo = { - path: string; - notfound: boolean; - size: number; - mode: number; - modtime: number; - isdir: boolean; - mimetype: string; - }; - - type FullFile = { - info: FileInfo; - data64: string; - }; } export {}; diff --git a/go.mod b/go.mod index 44ac8e0c5..7ea697ba4 100644 --- a/go.mod +++ b/go.mod @@ -13,6 +13,7 @@ require ( github.com/sawka/txwrap v0.2.0 github.com/wailsapp/wails/v3 v3.0.0-alpha.0 github.com/wavetermdev/waveterm/wavesrv v0.0.0-20240508181017-d07068c09d94 + golang.org/x/sys v0.20.0 ) require ( @@ -51,12 +52,12 @@ require ( github.com/wailsapp/mimetype v1.4.1 // indirect github.com/xanzy/ssh-agent v0.3.3 // indirect go.uber.org/atomic v1.7.0 // indirect - golang.org/x/crypto v0.21.0 // indirect + golang.org/x/crypto v0.23.0 // indirect golang.org/x/exp v0.0.0-20230626212559-97b1e661b5df // indirect - golang.org/x/mod v0.12.0 // indirect - golang.org/x/net v0.21.0 // indirect - golang.org/x/sys v0.19.0 // indirect - golang.org/x/tools v0.13.0 // indirect + golang.org/x/mod v0.17.0 // indirect + golang.org/x/net v0.25.0 // indirect + golang.org/x/sync v0.7.0 // indirect + golang.org/x/tools v0.21.0 // indirect gopkg.in/warnings.v0 v0.1.2 // indirect ) diff --git a/go.sum b/go.sum index 43b01693a..217bbb7f5 100644 --- a/go.sum +++ b/go.sum @@ -137,14 +137,14 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= -golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA= -golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= +golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI= +golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= golang.org/x/exp v0.0.0-20230626212559-97b1e661b5df h1:UA2aFVmmsIlefxMk29Dp2juaUSth8Pyn3Tq5Y5mJGME= golang.org/x/exp v0.0.0-20230626212559-97b1e661b5df/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.12.0 h1:rmsUpXtvNzj340zd98LZ4KntptpfRHwpFOHG188oHXc= -golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA= +golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210505024714-0287a6fb4125/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= @@ -153,8 +153,8 @@ golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= -golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4= -golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= +golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac= +golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -177,15 +177,15 @@ golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o= -golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= +golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= -golang.org/x/term v0.19.0 h1:+ThwsDv+tYfnJFhF4L8jITxu1tdTWRTZpdsWgEgjL6Q= -golang.org/x/term v0.19.0/go.mod h1:2CuTdWZ7KHSQwUzKva0cbMg6q2DMI3Mmxp+gKJbskEk= +golang.org/x/term v0.20.0 h1:VnkxpohqXaOBYJtBmEppKUG6mXpi+4O6purfc2+sMhw= +golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= @@ -193,14 +193,14 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= -golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= -golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk= +golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= -golang.org/x/tools v0.13.0 h1:Iey4qkscZuv0VvIt8E0neZjtPVQFSc870HQ448QgEmQ= -golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= +golang.org/x/tools v0.21.0 h1:qc0xYgIbsSDt9EyWz05J5wfa7LOVW0YTLOXrqdLAWIw= +golang.org/x/tools v0.21.0/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/main.go b/main.go index 18678152e..27ee260b0 100644 --- a/main.go +++ b/main.go @@ -9,6 +9,7 @@ import ( "embed" "log" "net/http" + "runtime" "strings" "github.com/wavetermdev/thenextwave/pkg/blockstore" @@ -24,7 +25,7 @@ import ( //go:embed dist var assets embed.FS -//go:embed build/appicon.png +//go:embed build/icons.icns var appIcon []byte func createAppMenu(app *application.App) *application.Menu { @@ -91,6 +92,12 @@ func main() { log.Printf("error ensuring wave home dir: %v\n", err) return } + waveLock, err := wavebase.AcquireWaveLock() + if err != nil { + log.Printf("error acquiring wave lock (another instance of Wave is likely running): %v\n", err) + return + } + log.Printf("wave home dir: %s\n", wavebase.GetWaveHomeDir()) err = blockstore.InitBlockstore() if err != nil { @@ -101,9 +108,9 @@ func main() { app := application.New(application.Options{ Name: "NextWave", Description: "The Next Wave Terminal", - Bind: []any{ - &fileservice.FileService{}, - &blockservice.BlockService{}, + Services: []application.Service{ + application.NewService(&fileservice.FileService{}), + application.NewService(&blockservice.BlockService{}), }, Icon: appIcon, Assets: application.AssetOptions{ @@ -129,4 +136,5 @@ func main() { if err != nil { log.Printf("run error: %v\n", err) } + runtime.KeepAlive(waveLock) } diff --git a/package.json b/package.json index f68707d13..cf031ef95 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,7 @@ "@types/react": "^18.3.2", "@types/uuid": "^9.0.8", "@vitejs/plugin-react": "^4.2.1", - "@wailsio/runtime": "latest", + "@wailsio/runtime": "^3.0.0-alpha.24", "less": "^4.2.0", "vite": "^5.0.0", "vite-tsconfig-paths": "^4.3.2" diff --git a/pkg/blockcontroller/blockcontroller.go b/pkg/blockcontroller/blockcontroller.go index 8bb9c6638..53ccded96 100644 --- a/pkg/blockcontroller/blockcontroller.go +++ b/pkg/blockcontroller/blockcontroller.go @@ -16,6 +16,7 @@ import ( "github.com/wailsapp/wails/v3/pkg/application" "github.com/wavetermdev/thenextwave/pkg/eventbus" "github.com/wavetermdev/thenextwave/pkg/shellexec" + "github.com/wavetermdev/thenextwave/pkg/wstore" ) const ( @@ -25,48 +26,11 @@ const ( var globalLock = &sync.Mutex{} var blockControllerMap = make(map[string]*BlockController) -var blockDataMap = make(map[string]*BlockData) - -type BlockData struct { - Lock *sync.Mutex `json:"-"` - BlockId string `json:"blockid"` - BlockDef *BlockDef `json:"blockdef"` - Controller string `json:"controller"` - ControllerStatus string `json:"controllerstatus"` - View string `json:"view"` - Meta map[string]any `json:"meta,omitempty"` - RuntimeOpts *RuntimeOpts `json:"runtimeopts,omitempty"` -} - -type FileDef struct { - FileType string `json:"filetype,omitempty"` - Path string `json:"path,omitempty"` - Url string `json:"url,omitempty"` - Content string `json:"content,omitempty"` - Meta map[string]any `json:"meta,omitempty"` -} - -type BlockDef struct { - Controller string `json:"controller"` - View string `json:"view,omitempty"` - Files map[string]*FileDef `json:"files,omitempty"` - Meta map[string]any `json:"meta,omitempty"` -} - -type WinSize struct { - Width int `json:"width"` - Height int `json:"height"` -} - -type RuntimeOpts struct { - TermSize shellexec.TermSize `json:"termsize,omitempty"` - WinSize WinSize `json:"winsize,omitempty"` -} type BlockController struct { Lock *sync.Mutex BlockId string - BlockDef *BlockDef + BlockDef *wstore.BlockDef InputCh chan BlockCommand ShellProc *shellexec.ShellProc @@ -86,9 +50,9 @@ func jsonDeepCopy(val map[string]any) (map[string]any, error) { return rtn, nil } -func CreateBlock(bdef *BlockDef, rtOpts *RuntimeOpts) (*BlockData, error) { +func CreateBlock(bdef *wstore.BlockDef, rtOpts *wstore.RuntimeOpts) (*wstore.Block, error) { blockId := uuid.New().String() - blockData := &BlockData{ + blockData := &wstore.Block{ Lock: &sync.Mutex{}, BlockId: blockId, BlockDef: bdef, @@ -101,7 +65,7 @@ func CreateBlock(bdef *BlockDef, rtOpts *RuntimeOpts) (*BlockData, error) { if err != nil { return nil, fmt.Errorf("error copying meta: %w", err) } - setBlockData(blockData) + wstore.BlockMap.Set(blockId, blockData) if blockData.Controller != "" { StartBlockController(blockId, blockData) } @@ -115,25 +79,7 @@ func CloseBlock(blockId string) { } bc.Close() close(bc.InputCh) - removeBlockData(blockId) -} - -func GetBlockData(blockId string) *BlockData { - globalLock.Lock() - defer globalLock.Unlock() - return blockDataMap[blockId] -} - -func setBlockData(bd *BlockData) { - globalLock.Lock() - defer globalLock.Unlock() - blockDataMap[bd.BlockId] = bd -} - -func removeBlockData(blockId string) { - globalLock.Lock() - defer globalLock.Unlock() - delete(blockDataMap, blockId) + wstore.BlockMap.Delete(blockId) } func (bc *BlockController) setShellProc(shellProc *shellexec.ShellProc) error { @@ -231,7 +177,7 @@ func (bc *BlockController) DoRunShellCommand(rc *RunShellOpts) error { return nil } -func (bc *BlockController) Run(bdata *BlockData) { +func (bc *BlockController) Run(bdata *wstore.Block) { defer func() { bdata.WithLock(func() { // if the controller had an error status, don't change it @@ -272,13 +218,7 @@ func (bc *BlockController) Run(bdata *BlockData) { } } -func (b *BlockData) WithLock(f func()) { - b.Lock.Lock() - defer b.Lock.Unlock() - f() -} - -func StartBlockController(blockId string, bdata *BlockData) { +func StartBlockController(blockId string, bdata *wstore.Block) { if bdata.Controller != BlockController_Shell { log.Printf("unknown controller %q\n", bdata.Controller) bdata.WithLock(func() { @@ -312,7 +252,7 @@ func ProcessStaticCommand(blockId string, cmdGen BlockCommand) { log.Printf("MESSAGE: %s | %q\n", blockId, cmd.Message) case *SetViewCommand: log.Printf("SETVIEW: %s | %q\n", blockId, cmd.View) - block := GetBlockData(blockId) + block := wstore.BlockMap.Get(blockId) if block != nil { block.WithLock(func() { block.View = cmd.View @@ -320,7 +260,7 @@ func ProcessStaticCommand(blockId string, cmdGen BlockCommand) { } case *SetMetaCommand: log.Printf("SETMETA: %s | %v\n", blockId, cmd.Meta) - block := GetBlockData(blockId) + block := wstore.BlockMap.Get(blockId) if block != nil { block.WithLock(func() { for k, v := range cmd.Meta { diff --git a/pkg/blockstore/blockstore_dbops.go b/pkg/blockstore/blockstore_dbops.go index 279b2b38c..5b9a41964 100644 --- a/pkg/blockstore/blockstore_dbops.go +++ b/pkg/blockstore/blockstore_dbops.go @@ -1,3 +1,6 @@ +// Copyright 2024, Command Line Inc. +// SPDX-License-Identifier: Apache-2.0 + package blockstore import ( diff --git a/pkg/blockstore/dbsetup.go b/pkg/blockstore/dbsetup.go index 406f8433b..c6e879742 100644 --- a/pkg/blockstore/dbsetup.go +++ b/pkg/blockstore/dbsetup.go @@ -1,3 +1,6 @@ +// Copyright 2024, Command Line Inc. +// SPDX-License-Identifier: Apache-2.0 + package blockstore // setup for blockstore db @@ -22,7 +25,7 @@ import ( dbfs "github.com/wavetermdev/thenextwave/db" ) -const BlockstoreDbName = "blockstore.db" +const BlockstoreDBName = "blockstore.db" type TxWrap = txwrap.TxWrap @@ -46,8 +49,8 @@ func InitBlockstore() error { } func GetDBName() string { - scHome := wavebase.GetWaveHomeDir() - return path.Join(scHome, BlockstoreDbName) + waveHome := wavebase.GetWaveHomeDir() + return path.Join(waveHome, BlockstoreDBName) } func MakeDB(ctx context.Context) (*sqlx.DB, error) { @@ -60,7 +63,7 @@ func MakeDB(ctx context.Context) (*sqlx.DB, error) { } else { dbName := GetDBName() log.Printf("[db] opening db %s\n", dbName) - rtn, err = sqlx.Open("sqlite3", fmt.Sprintf("file:%s?cache=shared&mode=rwc&_journal_mode=WAL&_busy_timeout=5000", dbName)) + rtn, err = sqlx.Open("sqlite3", fmt.Sprintf("file:%s?mode=rwc&_journal_mode=WAL&_busy_timeout=5000", dbName)) } if err != nil { return nil, fmt.Errorf("opening db: %w", err) diff --git a/pkg/service/blockservice/blockservice.go b/pkg/service/blockservice/blockservice.go index b832a3e31..81df911fa 100644 --- a/pkg/service/blockservice/blockservice.go +++ b/pkg/service/blockservice/blockservice.go @@ -9,17 +9,18 @@ import ( "github.com/wavetermdev/thenextwave/pkg/blockcontroller" "github.com/wavetermdev/thenextwave/pkg/util/utilfn" + "github.com/wavetermdev/thenextwave/pkg/wstore" ) type BlockService struct{} func (bs *BlockService) CreateBlock(bdefMap map[string]any, rtOptsMap map[string]any) (map[string]any, error) { - var bdef blockcontroller.BlockDef + var bdef wstore.BlockDef err := utilfn.JsonMapToStruct(bdefMap, &bdef) if err != nil { return nil, fmt.Errorf("error unmarshalling BlockDef: %w", err) } - var rtOpts blockcontroller.RuntimeOpts + var rtOpts wstore.RuntimeOpts err = utilfn.JsonMapToStruct(rtOptsMap, &rtOpts) if err != nil { return nil, fmt.Errorf("error unmarshalling RuntimeOpts: %w", err) @@ -40,7 +41,7 @@ func (bs *BlockService) CloseBlock(blockId string) { } func (bs *BlockService) GetBlockData(blockId string) (map[string]any, error) { - blockData := blockcontroller.GetBlockData(blockId) + blockData := wstore.BlockMap.Get(blockId) if blockData == nil { return nil, nil } diff --git a/pkg/util/ds/syncmap.go b/pkg/util/ds/syncmap.go new file mode 100644 index 000000000..02d0f80d8 --- /dev/null +++ b/pkg/util/ds/syncmap.go @@ -0,0 +1,43 @@ +// Copyright 2024, Command Line Inc. +// SPDX-License-Identifier: Apache-2.0 + +package ds + +import "sync" + +type SyncMap[T any] struct { + lock *sync.Mutex + m map[string]T +} + +func NewSyncMap[T any]() *SyncMap[T] { + return &SyncMap[T]{ + lock: &sync.Mutex{}, + m: make(map[string]T), + } +} + +func (sm *SyncMap[T]) Set(key string, value T) { + sm.lock.Lock() + defer sm.lock.Unlock() + sm.m[key] = value +} + +func (sm *SyncMap[T]) Get(key string) T { + sm.lock.Lock() + defer sm.lock.Unlock() + return sm.m[key] +} + +func (sm *SyncMap[T]) GetEx(key string) (T, bool) { + sm.lock.Lock() + defer sm.lock.Unlock() + v, ok := sm.m[key] + return v, ok +} + +func (sm *SyncMap[T]) Delete(key string) { + sm.lock.Lock() + defer sm.lock.Unlock() + delete(sm.m, key) +} diff --git a/pkg/wavebase/wavebase.go b/pkg/wavebase/wavebase.go index 4f5d74b62..5e5bcac25 100644 --- a/pkg/wavebase/wavebase.go +++ b/pkg/wavebase/wavebase.go @@ -12,10 +12,13 @@ import ( "os" "os/exec" "path" + "path/filepath" "runtime" "strings" "sync" "time" + + "golang.org/x/sys/unix" ) const WaveVersion = "v0.1.0" @@ -23,6 +26,7 @@ const DefaultWaveHome = "~/.w2" const WaveHomeVarName = "WAVETERM_HOME" const WaveDevVarName = "WAVETERM_DEV" const HomeVarName = "HOME" +const WaveLockFile = "waveterm.lock" var baseLock = &sync.Mutex{} var ensureDirCache = map[string]bool{} @@ -137,3 +141,19 @@ func DetermineLang() string { }) return osLang } + +func AcquireWaveLock() (*os.File, error) { + homeDir := GetWaveHomeDir() + lockFileName := filepath.Join(homeDir, WaveLockFile) + log.Printf("[base] acquiring lock on %s\n", lockFileName) + fd, err := os.OpenFile(lockFileName, os.O_RDWR|os.O_CREATE, 0600) + if err != nil { + return nil, err + } + err = unix.Flock(int(fd.Fd()), unix.LOCK_EX|unix.LOCK_NB) + if err != nil { + fd.Close() + return nil, err + } + return fd, nil +} diff --git a/pkg/wstore/wstore.go b/pkg/wstore/wstore.go new file mode 100644 index 000000000..f1c5466f5 --- /dev/null +++ b/pkg/wstore/wstore.go @@ -0,0 +1,120 @@ +// Copyright 2024, Command Line Inc. +// SPDX-License-Identifier: Apache-2.0 + +package wstore + +import ( + "fmt" + "sync" + + "github.com/google/uuid" + "github.com/wavetermdev/thenextwave/pkg/shellexec" + "github.com/wavetermdev/thenextwave/pkg/util/ds" +) + +var WorkspaceMap = ds.NewSyncMap[*Workspace]() +var TabMap = ds.NewSyncMap[*Tab]() +var BlockMap = ds.NewSyncMap[*Block]() + +type Client struct { + DefaultWorkspaceId string `json:"defaultworkspaceid"` +} + +type Workspace struct { + Lock *sync.Mutex `json:"-"` + WorkspaceId string `json:"workspaceid"` + TabIds []string `json:"tabids"` +} + +func (ws *Workspace) WithLock(f func()) { + ws.Lock.Lock() + defer ws.Lock.Unlock() + f() +} + +type Tab struct { + Lock *sync.Mutex `json:"-"` + TabId string `json:"tabid"` + Name string `json:"name"` + BlockIds []string `json:"blockids"` +} + +func (tab *Tab) WithLock(f func()) { + tab.Lock.Lock() + defer tab.Lock.Unlock() + f() +} + +type FileDef struct { + FileType string `json:"filetype,omitempty"` + Path string `json:"path,omitempty"` + Url string `json:"url,omitempty"` + Content string `json:"content,omitempty"` + Meta map[string]any `json:"meta,omitempty"` +} + +type BlockDef struct { + Controller string `json:"controller"` + View string `json:"view,omitempty"` + Files map[string]*FileDef `json:"files,omitempty"` + Meta map[string]any `json:"meta,omitempty"` +} + +type RuntimeOpts struct { + TermSize shellexec.TermSize `json:"termsize,omitempty"` + WinSize WinSize `json:"winsize,omitempty"` +} + +type WinSize struct { + Width int `json:"width"` + Height int `json:"height"` +} + +type Block struct { + Lock *sync.Mutex `json:"-"` + BlockId string `json:"blockid"` + BlockDef *BlockDef `json:"blockdef"` + Controller string `json:"controller"` + ControllerStatus string `json:"controllerstatus"` + View string `json:"view"` + Meta map[string]any `json:"meta,omitempty"` + RuntimeOpts *RuntimeOpts `json:"runtimeopts,omitempty"` +} + +func (b *Block) WithLock(f func()) { + b.Lock.Lock() + defer b.Lock.Unlock() + f() +} + +func CreateTab(workspaceId string, name string) (*Tab, error) { + tab := &Tab{ + Lock: &sync.Mutex{}, + TabId: uuid.New().String(), + Name: name, + BlockIds: []string{}, + } + TabMap.Set(tab.TabId, tab) + ws := WorkspaceMap.Get(workspaceId) + if ws == nil { + return nil, fmt.Errorf("workspace not found: %q", workspaceId) + } + ws.WithLock(func() { + ws.TabIds = append(ws.TabIds, tab.TabId) + }) + return tab, nil +} + +func CreateWorkspace() (*Workspace, error) { + ws := &Workspace{ + Lock: &sync.Mutex{}, + WorkspaceId: uuid.New().String(), + TabIds: []string{}, + } + WorkspaceMap.Set(ws.WorkspaceId, ws) + _, err := CreateTab(ws.WorkspaceId, "Tab 1") + if err != nil { + return nil, err + } + return ws, nil +} diff --git a/pkg/wstore/wstore_dbsetup.go b/pkg/wstore/wstore_dbsetup.go new file mode 100644 index 000000000..1b97352f9 --- /dev/null +++ b/pkg/wstore/wstore_dbsetup.go @@ -0,0 +1,57 @@ +// Copyright 2024, Command Line Inc. +// SPDX-License-Identifier: Apache-2.0 + +package wstore + +import ( + "context" + "fmt" + "log" + "path" + "time" + + "github.com/jmoiron/sqlx" + "github.com/sawka/txwrap" + "github.com/wavetermdev/thenextwave/pkg/wavebase" +) + +const WStoreDBName = "waveterm.db" + +type TxWrap = txwrap.TxWrap + +var globalDB *sqlx.DB + +func InitWStore() error { + ctx, cancelFn := context.WithTimeout(context.Background(), 2*time.Second) + defer cancelFn() + var err error + globalDB, err = MakeDB(ctx) + if err != nil { + return err + } + err = MigrateWStore() + if err != nil { + return err + } + log.Printf("wstore initialized\n") + return nil +} + +func GetDBName() string { + waveHome := wavebase.GetWaveHomeDir() + return path.Join(waveHome, WStoreDBName) +} + +func MakeDB(ctx context.Context) (*sqlx.DB, error) { + dbName := GetDBName() + rtn, err := sqlx.Open("sqlite3", fmt.Sprintf("file:%s?mode=rwc&_journal_mode=WAL&_busy_timeout=5000", dbName)) + if err != nil { + return nil, err + } + rtn.DB.SetMaxOpenConns(1) + return rtn, nil +} + +func MigrateWStore() error { + return nil +} diff --git a/tsconfig.json b/tsconfig.json index ea96d997c..5a3c5ec78 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -19,7 +19,7 @@ "@/util/*": ["frontend/util/*"], "@/store/*": ["frontend/app/store/*"], "@/element/*": ["frontend/app/element/*"], - "@/bindings/*": ["frontend/bindings/*"], + "@/bindings/*": ["frontend/bindings/github.com/wavetermdev/thenextwave/pkg/service/*"], } } } diff --git a/yarn.lock b/yarn.lock index f82276d18..eccbc4b89 100644 --- a/yarn.lock +++ b/yarn.lock @@ -572,9 +572,9 @@ "@types/babel__core" "^7.20.5" react-refresh "^0.14.0" -"@wailsio/runtime@latest": +"@wailsio/runtime@^3.0.0-alpha.24": version "3.0.0-alpha.24" - resolved "https://registry.npmjs.org/@wailsio/runtime/-/runtime-3.0.0-alpha.24.tgz" + resolved "https://registry.yarnpkg.com/@wailsio/runtime/-/runtime-3.0.0-alpha.24.tgz#87d8465a08fbfc7e3131cd8f8832601fb2a2c99c" integrity sha512-bd36Qj6CfIMPeUd+fhLpcb9O/XEUEOgLzelkHw45lk0OWHMRlTV5QYdAT3cBncvztH7xYijoN5v2+O3U/Jwreg== dependencies: nanoid "^5.0.7"