feat: first pass at writing connections to hujson

This replaces the current connection writing system with one that can
write to a hujson file non-destructively. This not only allows us to
write to hujson but also to preserve comments in the process.
This commit is contained in:
Sylvia Crowe 2025-02-15 02:02:24 -08:00
parent cca2f0ed7b
commit 6ecc5412a6
11 changed files with 155 additions and 6 deletions

View File

@ -5,6 +5,7 @@ import { WindowService } from "@/app/store/services";
import { RpcApi } from "@/app/store/wshclientapi";
import { Notification } from "electron";
import { getResolvedUpdateChannel } from "emain/updater";
import { createPatch } from "rfc6902";
import { RpcResponseHelper, WshClient } from "../frontend/app/store/wshclient";
import { getWebContentsByBlockId, webGetSelector } from "./emain-web";
import { createBrowserWindow, getWaveWindowById, getWaveWindowByWorkspaceId } from "./emain-window";
@ -66,6 +67,11 @@ export class ElectronWshClientType extends WshClient {
// workspaceMenu.submenu = Menu.buildFromTemplate(updatedWorkspaceMenu);
// });
// }
async handle_createjsonpatch(rh: RpcResponseHelper, jsonCompare: JsonCompare): Promise<string> {
const patch = createPatch(jsonCompare.original, jsonCompare.modified);
return JSON.stringify(patch);
}
}
export let ElectronWshClient: ElectronWshClientType;

View File

@ -92,6 +92,11 @@ class RpcApiType {
return client.wshRpcCall("createblock", data, opts);
}
// command "createjsonpatch" [call]
CreateJsonPatchCommand(client: WshClient, data: JsonCompare, opts?: RpcOpts): Promise<string> {
return client.wshRpcCall("createjsonpatch", data, opts);
}
// command "createsubblock" [call]
CreateSubBlockCommand(client: WshClient, data: CommandCreateSubBlockData, opts?: RpcOpts): Promise<ORef> {
return client.wshRpcCall("createsubblock", data, opts);

View File

@ -473,6 +473,12 @@ declare global {
configerrors: ConfigError[];
};
// wshrpc.JsonCompare
type JsonCompare = {
original: MetaType;
modified: MetaType;
};
// waveobj.LayoutActionData
type LayoutActionData = {
actiontype: string;

View File

@ -147,6 +147,7 @@
"remark-flexible-toc": "^1.1.1",
"remark-gfm": "^4.0.1",
"remark-github-blockquote-alert": "^1.3.0",
"rfc6902": "^5.1.2",
"rxjs": "^7.8.1",
"sharp": "^0.33.5",
"shell-quote": "^1.8.2",

View File

@ -33,6 +33,7 @@ import (
"github.com/wavetermdev/waveterm/pkg/waveobj"
"github.com/wavetermdev/waveterm/pkg/wconfig"
"github.com/wavetermdev/waveterm/pkg/wps"
"github.com/wavetermdev/waveterm/pkg/wsaveconfig"
"github.com/wavetermdev/waveterm/pkg/wshrpc"
"github.com/wavetermdev/waveterm/pkg/wshutil"
"golang.org/x/crypto/ssh"
@ -450,7 +451,7 @@ func (conn *SSHConn) getPermissionToInstallWsh(ctx context.Context, clientDispla
meta := make(map[string]any)
meta["conn:wshenabled"] = response.Confirm
conn.Infof(ctx, "writing conn:wshenabled=%v to connections.json\n", response.Confirm)
err = wconfig.SetConnectionsConfigValue(conn.GetName(), meta)
err = wsaveconfig.SetConnectionsConfigValue(conn.GetName(), meta)
if err != nil {
log.Printf("warning: error writing to connections file: %v", err)
}
@ -608,7 +609,7 @@ func (conn *SSHConn) Connect(ctx context.Context, connFlags *wconfig.ConnKeyword
}
meta["ssh:identityfile"] = identityFiles
}
err = wconfig.SetConnectionsConfigValue(conn.GetName(), meta)
err = wsaveconfig.SetConnectionsConfigValue(conn.GetName(), meta)
if err != nil {
// i do not consider this a critical failure
log.Printf("config write error: unable to save connection %s: %v", conn.GetName(), err)
@ -733,7 +734,7 @@ func (conn *SSHConn) persistWshInstalled(ctx context.Context, result WshCheckRes
}
meta := make(map[string]any)
meta["conn:wshenabled"] = result.WshEnabled
err := wconfig.SetConnectionsConfigValue(conn.GetName(), meta)
err := wsaveconfig.SetConnectionsConfigValue(conn.GetName(), meta)
if err != nil {
conn.Infof(ctx, "WARN could not write conn:wshenabled=%v to connections.json: %v\n", result.WshEnabled, err)
log.Printf("warning: error writing to connections file: %v", err)

View File

@ -0,0 +1,107 @@
// Copyright 2025, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
package wsaveconfig
import (
"fmt"
"io/fs"
"os"
"path/filepath"
"github.com/tailscale/hujson"
"github.com/wavetermdev/waveterm/pkg/wavebase"
"github.com/wavetermdev/waveterm/pkg/waveobj"
"github.com/wavetermdev/waveterm/pkg/wconfig"
"github.com/wavetermdev/waveterm/pkg/wshrpc"
"github.com/wavetermdev/waveterm/pkg/wshrpc/wshclient"
"github.com/wavetermdev/waveterm/pkg/wshutil"
)
func readConfigFileFSRaw(fsys fs.FS, fileName string) ([]byte, error) {
barr, readErr := fs.ReadFile(fsys, fileName)
if readErr != nil {
// If we get an error, we may be using the wrong path separator for the given FS interface. Try switching the separator.
barr, readErr = fs.ReadFile(fsys, filepath.ToSlash(fileName))
}
return barr, readErr
}
func SetConnectionsConfigValue(connName string, toMerge waveobj.MetaMapType) error {
originalM, cerrs := wconfig.ReadWaveHomeConfigFile(wconfig.ConnectionsFile)
if len(cerrs) > 0 {
return fmt.Errorf("error reading config file: %v", cerrs[0])
}
mergedM, err := createMergedConnections(connName, toMerge)
if err != nil {
return fmt.Errorf("error merging config: %w", err)
}
jsonCompare := wshrpc.JsonCompare{
Original: originalM,
Modified: mergedM,
}
rpcClient := wshclient.GetBareRpcClient()
patchStr, err := wshclient.CreateJsonPatchCommand(rpcClient, jsonCompare, &wshrpc.RpcOpts{
Route: wshutil.ElectronRoute,
})
if err != nil {
return fmt.Errorf("error creating json patch: %w", err)
}
patchBarr := []byte(patchStr)
mTarget, err := jsonMarshallHujson(wconfig.ConnectionsFile)
if err != nil {
return fmt.Errorf("cannot parse hujson: %w", err)
}
err = mTarget.Patch(patchBarr)
if err != nil {
return fmt.Errorf("failed to apply patch: %w", err)
}
outBarr := mTarget.Pack()
outBarr, err = hujson.Format(outBarr)
if err != nil {
return fmt.Errorf("failed to format: %w", err)
}
return writeWaveHomeConfigFile(wconfig.ConnectionsFile, outBarr)
}
func writeWaveHomeConfigFile(fileName string, barr []byte) error {
configDirAbsPath := wavebase.GetWaveConfigDir()
fullFileName := filepath.Join(configDirAbsPath, fileName)
return os.WriteFile(fullFileName, barr, 0644)
}
func createMergedConnections(connName string, toMerge waveobj.MetaMapType) (waveobj.MetaMapType, error) {
m, cerrs := wconfig.ReadWaveHomeConfigFile(wconfig.ConnectionsFile)
if len(cerrs) > 0 {
return nil, fmt.Errorf("error reading config file: %v", cerrs[0])
}
if m == nil {
m = make(waveobj.MetaMapType)
}
connData := m.GetMap(connName)
if connData == nil {
connData = make(waveobj.MetaMapType)
}
for configKey, val := range toMerge {
if val == nil {
delete(connData, configKey)
continue
}
connData[configKey] = val
}
m[connName] = connData
return m, nil
}
func jsonMarshallHujson(filename string) (*hujson.Value, error) {
configDirAbsPath := wavebase.GetWaveConfigDir()
configDirFsys := os.DirFS(configDirAbsPath)
barr, err := readConfigFileFSRaw(configDirFsys, filename)
if err != nil {
// TODO just create one from scratch instead (still can fail if the newly created cannot be opened)
return nil, fmt.Errorf("unable to read existing config file: %w", err)
}
outConfig, err := hujson.Parse(barr)
return &outConfig, err
}

View File

@ -118,6 +118,12 @@ func CreateBlockCommand(w *wshutil.WshRpc, data wshrpc.CommandCreateBlockData, o
return resp, err
}
// command "createjsonpatch", wshserver.CreateJsonPatchCommand
func CreateJsonPatchCommand(w *wshutil.WshRpc, data wshrpc.JsonCompare, opts *wshrpc.RpcOpts) (string, error) {
resp, err := sendRpcRequestCallHelper[string](w, "createjsonpatch", data, opts)
return resp, err
}
// command "createsubblock", wshserver.CreateSubBlockCommand
func CreateSubBlockCommand(w *wshutil.WshRpc, data wshrpc.CommandCreateSubBlockData, opts *wshrpc.RpcOpts) (waveobj.ORef, error) {
resp, err := sendRpcRequestCallHelper[waveobj.ORef](w, "createsubblock", data, opts)

View File

@ -124,6 +124,7 @@ const (
Command_Notify = "notify"
Command_FocusWindow = "focuswindow"
Command_GetUpdateChannel = "getupdatechannel"
Command_CreateJsonPatch = "createjsonpatch"
Command_VDomCreateContext = "vdomcreatecontext"
Command_VDomAsyncInitiation = "vdomasyncinitiation"
@ -234,6 +235,7 @@ type WshRpcInterface interface {
WorkspaceListCommand(ctx context.Context) ([]WorkspaceInfoData, error)
GetUpdateChannelCommand(ctx context.Context) (string, error)
CreateJsonPatchCommand(ctx context.Context, jsonCompare JsonCompare) (string, error)
// terminal
VDomCreateContextCommand(ctx context.Context, data vdom.VDomCreateContext) (*waveobj.ORef, error)
@ -763,3 +765,8 @@ type SuggestionType struct {
FileName string `json:"file:name,omitempty"`
UrlUrl string `json:"url:url,omitempty"`
}
type JsonCompare struct {
Original waveobj.MetaMapType `json:"original"`
Modified waveobj.MetaMapType `json:"modified"`
}

View File

@ -41,6 +41,7 @@ import (
"github.com/wavetermdev/waveterm/pkg/wconfig"
"github.com/wavetermdev/waveterm/pkg/wcore"
"github.com/wavetermdev/waveterm/pkg/wps"
"github.com/wavetermdev/waveterm/pkg/wsaveconfig"
"github.com/wavetermdev/waveterm/pkg/wshrpc"
"github.com/wavetermdev/waveterm/pkg/wshutil"
"github.com/wavetermdev/waveterm/pkg/wsl"
@ -516,7 +517,7 @@ func (ws *WshServer) SetConfigCommand(ctx context.Context, data wshrpc.MetaSetti
func (ws *WshServer) SetConnectionsConfigCommand(ctx context.Context, data wshrpc.ConnConfigRequest) error {
log.Printf("SET CONNECTIONS CONFIG: %v\n", data)
return wconfig.SetConnectionsConfigValue(data.Host, data.MetaMapType)
return wsaveconfig.SetConnectionsConfigValue(data.Host, data.MetaMapType)
}
func (ws *WshServer) GetFullConfigCommand(ctx context.Context) (wconfig.FullConfigType, error) {

View File

@ -28,6 +28,7 @@ import (
"github.com/wavetermdev/waveterm/pkg/waveobj"
"github.com/wavetermdev/waveterm/pkg/wconfig"
"github.com/wavetermdev/waveterm/pkg/wps"
"github.com/wavetermdev/waveterm/pkg/wsaveconfig"
"github.com/wavetermdev/waveterm/pkg/wshrpc"
"github.com/wavetermdev/waveterm/pkg/wshutil"
"github.com/wavetermdev/waveterm/pkg/wsl"
@ -421,7 +422,7 @@ func (conn *WslConn) getPermissionToInstallWsh(ctx context.Context, clientDispla
meta := make(map[string]any)
meta["conn:wshenabled"] = response.Confirm
conn.Infof(ctx, "writing conn:wshenabled=%v to connections.json\n", response.Confirm)
err = wconfig.SetConnectionsConfigValue(conn.GetName(), meta)
err = wsaveconfig.SetConnectionsConfigValue(conn.GetName(), meta)
if err != nil {
log.Printf("warning: error writing to connections file: %v", err)
}
@ -680,7 +681,7 @@ func (conn *WslConn) persistWshInstalled(ctx context.Context, result WshCheckRes
}
meta := make(map[string]any)
meta["conn:wshenabled"] = result.WshEnabled
err := wconfig.SetConnectionsConfigValue(conn.GetName(), meta)
err := wsaveconfig.SetConnectionsConfigValue(conn.GetName(), meta)
if err != nil {
conn.Infof(ctx, "WARN could not write conn:wshenabled=%v to connections.json: %v\n", result.WshEnabled, err)
log.Printf("warning: error writing to connections file: %v", err)

View File

@ -18774,6 +18774,13 @@ __metadata:
languageName: node
linkType: hard
"rfc6902@npm:^5.1.2":
version: 5.1.2
resolution: "rfc6902@npm:5.1.2"
checksum: 10c0/29d944ea416230e9589476a646e68a8684437f6ea2883b6447abd3a1a741f12bbedd7a15e300055b35a938ff3944116e5f3ae95a30b3d2f9f0c980957566b364
languageName: node
linkType: hard
"rimraf@npm:^3.0.2":
version: 3.0.2
resolution: "rimraf@npm:3.0.2"
@ -21823,6 +21830,7 @@ __metadata:
remark-flexible-toc: "npm:^1.1.1"
remark-gfm: "npm:^4.0.1"
remark-github-blockquote-alert: "npm:^1.3.0"
rfc6902: "npm:^5.1.2"
rollup-plugin-flow: "npm:^1.1.1"
rxjs: "npm:^7.8.1"
sass: "npm:^1.84.0"