From 6dfc85b324eb3e74dce2c164ef262d61753d19c6 Mon Sep 17 00:00:00 2001 From: Sylvie Crowe <107814465+oneirocosm@users.noreply.github.com> Date: Fri, 6 Dec 2024 10:11:38 -0800 Subject: [PATCH] Retry Without Wsh on Fail (#1406) Adds the ability for connections to continue without wsh if they fail. This involves creating a menu that warns the user that wsh could not be used. --- docs/docs/connections.mdx | 25 +++++- docs/docs/wsh.mdx | 2 +- frontend/app/block/block.scss | 1 + frontend/app/block/blockframe.tsx | 65 +++++++++++++- frontend/app/store/wshclientapi.ts | 10 +++ frontend/types/gotypes.d.ts | 7 ++ pkg/blockcontroller/blockcontroller.go | 21 ++++- pkg/remote/conncontroller/conncontroller.go | 16 +++- pkg/shellexec/shellexec.go | 83 +++++++++--------- pkg/wshrpc/wshclient/wshclient.go | 12 +++ pkg/wshrpc/wshrpctypes.go | 95 ++++++++++++--------- pkg/wshrpc/wshserver/wshserver.go | 24 ++++++ 12 files changed, 268 insertions(+), 93 deletions(-) diff --git a/docs/docs/connections.mdx b/docs/docs/connections.mdx index a497d2f49..565a799ee 100644 --- a/docs/docs/connections.mdx +++ b/docs/docs/connections.mdx @@ -16,11 +16,18 @@ The easiest way to access connections is to click the { if (!magnified || preview || prevMagifiedState.current) { @@ -342,6 +345,8 @@ const ConnStatusOverlay = React.memo( const [overlayRefCallback, _, domRect] = useDimensionsWithCallbackRef(30); const width = domRect?.width; const [showError, setShowError] = React.useState(false); + const fullConfig = jotai.useAtomValue(atoms.fullConfigAtom); + const [showWshError, setShowWshError] = React.useState(false); React.useEffect(() => { if (width) { @@ -356,12 +361,40 @@ const ConnStatusOverlay = React.memo( prtn.catch((e) => console.log("error reconnecting", connName, e)); }, [connName]); + const handleDisableWsh = React.useCallback(async () => { + // using unknown is a hack. we need proper types for the + // connection config on the frontend + const metamaptype: unknown = { + "conn:wshenabled": false, + }; + const data: ConnConfigRequest = { + host: connName, + metamaptype: metamaptype, + }; + try { + await RpcApi.SetConnectionsConfigCommand(TabRpcClient, data); + } catch (e) { + console.log("problem setting connection config: ", e); + } + }, [connName]); + + const handleRemoveWshError = React.useCallback(async () => { + try { + await RpcApi.DismissWshFailCommand(TabRpcClient, connName); + } catch (e) { + console.log("unable to dismiss wsh error: ", e); + } + }, [connName]); + let statusText = `Disconnected from "${connName}"`; let showReconnect = true; if (connStatus.status == "connecting") { statusText = `Connecting to "${connName}"...`; showReconnect = false; } + if (connStatus.status == "connected") { + showReconnect = false; + } let reconDisplay = null; let reconClassName = "outlined grey"; if (width && width < 350) { @@ -373,18 +406,37 @@ const ConnStatusOverlay = React.memo( } const showIcon = connStatus.status != "connecting"; - if (isLayoutMode || connStatus.status == "connected" || connModalOpen) { + const wshConfigEnabled = fullConfig?.connections?.[connName]?.["conn:wshenabled"] ?? true; + React.useEffect(() => { + const showWshErrorTemp = + connStatus.status == "connected" && + connStatus.wsherror && + connStatus.wsherror != "" && + wshConfigEnabled; + + setShowWshError(showWshErrorTemp); + }, [connStatus, wshConfigEnabled]); + + if (!showWshError && (isLayoutMode || connStatus.status == "connected" || connModalOpen)) { return null; } return (
-
+
{showIcon && }
{statusText}
{showError ?
error: {connStatus.error}
: null} + {showWshError ? ( +
unable to use wsh: {connStatus.wsherror}
+ ) : null} + {showWshError && ( + + )}
{showReconnect ? ( @@ -394,6 +446,11 @@ const ConnStatusOverlay = React.memo(
) : null} + {showWshError ? ( +
+
+ ) : null}
); diff --git a/frontend/app/store/wshclientapi.ts b/frontend/app/store/wshclientapi.ts index e6142aeee..a5e75774d 100644 --- a/frontend/app/store/wshclientapi.ts +++ b/frontend/app/store/wshclientapi.ts @@ -92,6 +92,11 @@ class RpcApiType { return client.wshRpcCall("deletesubblock", data, opts); } + // command "dismisswshfail" [call] + DismissWshFailCommand(client: WshClient, data: string, opts?: RpcOpts): Promise { + return client.wshRpcCall("dismisswshfail", data, opts); + } + // command "dispose" [call] DisposeCommand(client: WshClient, data: CommandDisposeData, opts?: RpcOpts): Promise { return client.wshRpcCall("dispose", data, opts); @@ -262,6 +267,11 @@ class RpcApiType { return client.wshRpcCall("setconfig", data, opts); } + // command "setconnectionsconfig" [call] + SetConnectionsConfigCommand(client: WshClient, data: ConnConfigRequest, opts?: RpcOpts): Promise { + return client.wshRpcCall("setconnectionsconfig", data, opts); + } + // command "setmeta" [call] SetMetaCommand(client: WshClient, data: CommandSetMetaData, opts?: RpcOpts): Promise { return client.wshRpcCall("setmeta", data, opts); diff --git a/frontend/types/gotypes.d.ts b/frontend/types/gotypes.d.ts index c3d4d7db4..224da154c 100644 --- a/frontend/types/gotypes.d.ts +++ b/frontend/types/gotypes.d.ts @@ -278,6 +278,12 @@ declare global { err: string; }; + // wshrpc.ConnConfigRequest + type ConnConfigRequest = { + host: string; + metamaptype: MetaType; + }; + // wshrpc.ConnKeywords type ConnKeywords = { "conn:wshenabled"?: boolean; @@ -319,6 +325,7 @@ declare global { hasconnected: boolean; activeconnnum: number; error?: string; + wsherror?: string; }; // wshrpc.CpuDataRequest diff --git a/pkg/blockcontroller/blockcontroller.go b/pkg/blockcontroller/blockcontroller.go index 413aa3a20..53921b341 100644 --- a/pkg/blockcontroller/blockcontroller.go +++ b/pkg/blockcontroller/blockcontroller.go @@ -354,7 +354,26 @@ func (bc *BlockController) DoRunShellCommand(rc *RunShellOpts, blockMeta waveobj } cmdOpts.Env[wshutil.WaveJwtTokenVarName] = jwtStr } - shellProc, err = shellexec.StartRemoteShellProc(rc.TermSize, cmdStr, cmdOpts, conn) + if !conn.WshEnabled.Load() { + shellProc, err = shellexec.StartRemoteShellProcNoWsh(rc.TermSize, cmdStr, cmdOpts, conn) + if err != nil { + return err + } + } else { + shellProc, err = shellexec.StartRemoteShellProc(rc.TermSize, cmdStr, cmdOpts, conn) + if err != nil { + conn.WithLock(func() { + conn.WshError = err.Error() + }) + conn.WshEnabled.Store(false) + log.Printf("error starting remote shell proc with wsh: %v", err) + log.Print("attempting install without wsh") + shellProc, err = shellexec.StartRemoteShellProcNoWsh(rc.TermSize, cmdStr, cmdOpts, conn) + if err != nil { + return err + } + } + } if err != nil { return err } diff --git a/pkg/remote/conncontroller/conncontroller.go b/pkg/remote/conncontroller/conncontroller.go index 182180f0c..016898caa 100644 --- a/pkg/remote/conncontroller/conncontroller.go +++ b/pkg/remote/conncontroller/conncontroller.go @@ -59,6 +59,7 @@ type SSHConn struct { DomainSockListener net.Listener ConnController *ssh.Session Error string + WshError string HasWaiter *atomic.Bool LastConnectTime int64 ActiveConnNum int @@ -94,10 +95,12 @@ func (conn *SSHConn) DeriveConnStatus() wshrpc.ConnStatus { return wshrpc.ConnStatus{ Status: conn.Status, Connected: conn.Status == Status_Connected, + WshEnabled: conn.WshEnabled.Load(), Connection: conn.Opts.String(), HasConnected: (conn.LastConnectTime > 0), ActiveConnNum: conn.ActiveConnNum, Error: conn.Error, + WshError: conn.WshError, } } @@ -532,7 +535,11 @@ func (conn *SSHConn) connectInternal(ctx context.Context, connFlags *wshrpc.Conn }) } else if installErr != nil { log.Printf("error: unable to install wsh shell extensions for %s: %v\n", conn.GetName(), err) - return fmt.Errorf("conncontroller %s wsh install error: %v", conn.GetName(), installErr) + log.Print("attempting to run with nowsh instead") + conn.WithLock(func() { + conn.WshError = installErr.Error() + }) + conn.WshEnabled.Store(false) } else { conn.WshEnabled.Store(true) } @@ -541,7 +548,12 @@ func (conn *SSHConn) connectInternal(ctx context.Context, connFlags *wshrpc.Conn csErr := conn.StartConnServer() if csErr != nil { log.Printf("error: unable to start conn server for %s: %v\n", conn.GetName(), csErr) - return fmt.Errorf("conncontroller %s start wsh connserver error: %v", conn.GetName(), csErr) + log.Print("attempting to run with nowsh instead") + conn.WithLock(func() { + conn.WshError = csErr.Error() + }) + conn.WshEnabled.Store(false) + //return fmt.Errorf("conncontroller %s start wsh connserver error: %v", conn.GetName(), csErr) } } } else { diff --git a/pkg/shellexec/shellexec.go b/pkg/shellexec/shellexec.go index 0c4585975..d22cdbb1f 100644 --- a/pkg/shellexec/shellexec.go +++ b/pkg/shellexec/shellexec.go @@ -236,49 +236,50 @@ func StartWslShellProc(ctx context.Context, termSize waveobj.TermSize, cmdStr st return &ShellProc{Cmd: cmdWrap, ConnName: conn.GetName(), CloseOnce: &sync.Once{}, DoneCh: make(chan any)}, nil } +func StartRemoteShellProcNoWsh(termSize waveobj.TermSize, cmdStr string, cmdOpts CommandOptsType, conn *conncontroller.SSHConn) (*ShellProc, error) { + client := conn.GetClient() + session, err := client.NewSession() + if err != nil { + return nil, err + } + + remoteStdinRead, remoteStdinWriteOurs, err := os.Pipe() + if err != nil { + return nil, err + } + + remoteStdoutReadOurs, remoteStdoutWrite, err := os.Pipe() + if err != nil { + return nil, err + } + + pipePty := &PipePty{ + remoteStdinWrite: remoteStdinWriteOurs, + remoteStdoutRead: remoteStdoutReadOurs, + } + if termSize.Rows == 0 || termSize.Cols == 0 { + termSize.Rows = shellutil.DefaultTermRows + termSize.Cols = shellutil.DefaultTermCols + } + if termSize.Rows <= 0 || termSize.Cols <= 0 { + return nil, fmt.Errorf("invalid term size: %v", termSize) + } + session.Stdin = remoteStdinRead + session.Stdout = remoteStdoutWrite + session.Stderr = remoteStdoutWrite + + session.RequestPty("xterm-256color", termSize.Rows, termSize.Cols, nil) + sessionWrap := MakeSessionWrap(session, "", pipePty) + err = session.Shell() + if err != nil { + pipePty.Close() + return nil, err + } + return &ShellProc{Cmd: sessionWrap, ConnName: conn.GetName(), CloseOnce: &sync.Once{}, DoneCh: make(chan any)}, nil +} + func StartRemoteShellProc(termSize waveobj.TermSize, cmdStr string, cmdOpts CommandOptsType, conn *conncontroller.SSHConn) (*ShellProc, error) { client := conn.GetClient() - if !conn.WshEnabled.Load() { - // no wsh code - session, err := client.NewSession() - if err != nil { - return nil, err - } - - remoteStdinRead, remoteStdinWriteOurs, err := os.Pipe() - if err != nil { - return nil, err - } - - remoteStdoutReadOurs, remoteStdoutWrite, err := os.Pipe() - if err != nil { - return nil, err - } - - pipePty := &PipePty{ - remoteStdinWrite: remoteStdinWriteOurs, - remoteStdoutRead: remoteStdoutReadOurs, - } - if termSize.Rows == 0 || termSize.Cols == 0 { - termSize.Rows = shellutil.DefaultTermRows - termSize.Cols = shellutil.DefaultTermCols - } - if termSize.Rows <= 0 || termSize.Cols <= 0 { - return nil, fmt.Errorf("invalid term size: %v", termSize) - } - session.Stdin = remoteStdinRead - session.Stdout = remoteStdoutWrite - session.Stderr = remoteStdoutWrite - - session.RequestPty("xterm-256color", termSize.Rows, termSize.Cols, nil) - sessionWrap := MakeSessionWrap(session, "", pipePty) - err = session.Shell() - if err != nil { - pipePty.Close() - return nil, err - } - return &ShellProc{Cmd: sessionWrap, ConnName: conn.GetName(), CloseOnce: &sync.Once{}, DoneCh: make(chan any)}, nil - } shellPath := cmdOpts.ShellPath if shellPath == "" { remoteShellPath, err := remote.DetectShell(client) diff --git a/pkg/wshrpc/wshclient/wshclient.go b/pkg/wshrpc/wshclient/wshclient.go index 30f81fe8c..d8aa04748 100644 --- a/pkg/wshrpc/wshclient/wshclient.go +++ b/pkg/wshrpc/wshclient/wshclient.go @@ -115,6 +115,12 @@ func DeleteSubBlockCommand(w *wshutil.WshRpc, data wshrpc.CommandDeleteBlockData return err } +// command "dismisswshfail", wshserver.DismissWshFailCommand +func DismissWshFailCommand(w *wshutil.WshRpc, data string, opts *wshrpc.RpcOpts) error { + _, err := sendRpcRequestCallHelper[any](w, "dismisswshfail", data, opts) + return err +} + // command "dispose", wshserver.DisposeCommand func DisposeCommand(w *wshutil.WshRpc, data wshrpc.CommandDisposeData, opts *wshrpc.RpcOpts) error { _, err := sendRpcRequestCallHelper[any](w, "dispose", data, opts) @@ -317,6 +323,12 @@ func SetConfigCommand(w *wshutil.WshRpc, data wshrpc.MetaSettingsType, opts *wsh return err } +// command "setconnectionsconfig", wshserver.SetConnectionsConfigCommand +func SetConnectionsConfigCommand(w *wshutil.WshRpc, data wshrpc.ConnConfigRequest, opts *wshrpc.RpcOpts) error { + _, err := sendRpcRequestCallHelper[any](w, "setconnectionsconfig", data, opts) + return err +} + // command "setmeta", wshserver.SetMetaCommand func SetMetaCommand(w *wshutil.WshRpc, data wshrpc.CommandSetMetaData, opts *wshrpc.RpcOpts) error { _, err := sendRpcRequestCallHelper[any](w, "setmeta", data, opts) diff --git a/pkg/wshrpc/wshrpctypes.go b/pkg/wshrpc/wshrpctypes.go index 334ddd18e..ed5aaa7bb 100644 --- a/pkg/wshrpc/wshrpctypes.go +++ b/pkg/wshrpc/wshrpctypes.go @@ -29,48 +29,50 @@ const ( ) const ( - Command_Authenticate = "authenticate" // special - Command_Dispose = "dispose" // special (disposes of the route, for multiproxy only) - Command_RouteAnnounce = "routeannounce" // special (for routing) - Command_RouteUnannounce = "routeunannounce" // special (for routing) - Command_Message = "message" - Command_GetMeta = "getmeta" - Command_SetMeta = "setmeta" - Command_SetView = "setview" - Command_ControllerInput = "controllerinput" - Command_ControllerRestart = "controllerrestart" - Command_ControllerStop = "controllerstop" - Command_ControllerResync = "controllerresync" - Command_FileAppend = "fileappend" - Command_FileAppendIJson = "fileappendijson" - Command_ResolveIds = "resolveids" - Command_BlockInfo = "blockinfo" - Command_CreateBlock = "createblock" - Command_DeleteBlock = "deleteblock" - Command_FileWrite = "filewrite" - Command_FileRead = "fileread" - Command_EventPublish = "eventpublish" - Command_EventRecv = "eventrecv" - Command_EventSub = "eventsub" - Command_EventUnsub = "eventunsub" - Command_EventUnsubAll = "eventunsuball" - Command_EventReadHistory = "eventreadhistory" - Command_StreamTest = "streamtest" - Command_StreamWaveAi = "streamwaveai" - Command_StreamCpuData = "streamcpudata" - Command_Test = "test" - Command_RemoteStreamFile = "remotestreamfile" - Command_RemoteFileInfo = "remotefileinfo" - Command_RemoteFileTouch = "remotefiletouch" - Command_RemoteWriteFile = "remotewritefile" - Command_RemoteFileDelete = "remotefiledelete" - Command_RemoteFileJoin = "remotefilejoin" - Command_WaveInfo = "waveinfo" - Command_WshActivity = "wshactivity" - Command_Activity = "activity" - Command_GetVar = "getvar" - Command_SetVar = "setvar" - Command_RemoteMkdir = "remotemkdir" + Command_Authenticate = "authenticate" // special + Command_Dispose = "dispose" // special (disposes of the route, for multiproxy only) + Command_RouteAnnounce = "routeannounce" // special (for routing) + Command_RouteUnannounce = "routeunannounce" // special (for routing) + Command_Message = "message" + Command_GetMeta = "getmeta" + Command_SetMeta = "setmeta" + Command_SetView = "setview" + Command_ControllerInput = "controllerinput" + Command_ControllerRestart = "controllerrestart" + Command_ControllerStop = "controllerstop" + Command_ControllerResync = "controllerresync" + Command_FileAppend = "fileappend" + Command_FileAppendIJson = "fileappendijson" + Command_ResolveIds = "resolveids" + Command_BlockInfo = "blockinfo" + Command_CreateBlock = "createblock" + Command_DeleteBlock = "deleteblock" + Command_FileWrite = "filewrite" + Command_FileRead = "fileread" + Command_EventPublish = "eventpublish" + Command_EventRecv = "eventrecv" + Command_EventSub = "eventsub" + Command_EventUnsub = "eventunsub" + Command_EventUnsubAll = "eventunsuball" + Command_EventReadHistory = "eventreadhistory" + Command_StreamTest = "streamtest" + Command_StreamWaveAi = "streamwaveai" + Command_StreamCpuData = "streamcpudata" + Command_Test = "test" + Command_SetConfig = "setconfig" + Command_SetConnectionsConfig = "connectionsconfig" + Command_RemoteStreamFile = "remotestreamfile" + Command_RemoteFileInfo = "remotefileinfo" + Command_RemoteFileTouch = "remotefiletouch" + Command_RemoteWriteFile = "remotewritefile" + Command_RemoteFileDelete = "remotefiledelete" + Command_RemoteFileJoin = "remotefilejoin" + Command_WaveInfo = "waveinfo" + Command_WshActivity = "wshactivity" + Command_Activity = "activity" + Command_GetVar = "getvar" + Command_SetVar = "setvar" + Command_RemoteMkdir = "remotemkdir" Command_ConnStatus = "connstatus" Command_WslStatus = "wslstatus" @@ -81,6 +83,7 @@ const ( Command_ConnList = "connlist" Command_WslList = "wsllist" Command_WslDefaultDistro = "wsldefaultdistro" + Command_DismissWshFail = "dismisswshfail" Command_WorkspaceList = "workspacelist" @@ -139,6 +142,7 @@ type WshRpcInterface interface { StreamCpuDataCommand(ctx context.Context, request CpuDataRequest) chan RespOrErrorUnion[TimeSeriesData] TestCommand(ctx context.Context, data string) error SetConfigCommand(ctx context.Context, data MetaSettingsType) error + SetConnectionsConfigCommand(ctx context.Context, data ConnConfigRequest) error BlockInfoCommand(ctx context.Context, blockId string) (*BlockInfoData, error) WaveInfoCommand(ctx context.Context) (*WaveInfoData, error) WshActivityCommand(ct context.Context, data map[string]int) error @@ -156,6 +160,7 @@ type WshRpcInterface interface { ConnListCommand(ctx context.Context) ([]string, error) WslListCommand(ctx context.Context) ([]string, error) WslDefaultDistroCommand(ctx context.Context) (string, error) + DismissWshFailCommand(ctx context.Context, connName string) error // eventrecv is special, it's handled internally by WshRpc with EventListener EventRecvCommand(ctx context.Context, data wps.WaveEvent) error @@ -512,6 +517,11 @@ func (m MetaSettingsType) MarshalJSON() ([]byte, error) { return json.Marshal(m.MetaMapType) } +type ConnConfigRequest struct { + Host string `json:"host"` + MetaMapType waveobj.MetaMapType `json:"metamaptype"` +} + type ConnStatus struct { Status string `json:"status"` WshEnabled bool `json:"wshenabled"` @@ -520,6 +530,7 @@ type ConnStatus struct { HasConnected bool `json:"hasconnected"` // true if it has *ever* connected successfully ActiveConnNum int `json:"activeconnnum"` Error string `json:"error,omitempty"` + WshError string `json:"wsherror,omitempty"` } type WebSelectorOpts struct { diff --git a/pkg/wshrpc/wshserver/wshserver.go b/pkg/wshrpc/wshserver/wshserver.go index 1b5f1793c..a5f65301b 100644 --- a/pkg/wshrpc/wshserver/wshserver.go +++ b/pkg/wshrpc/wshserver/wshserver.go @@ -575,6 +575,11 @@ func (ws *WshServer) SetConfigCommand(ctx context.Context, data wshrpc.MetaSetti return wconfig.SetBaseConfigValue(data.MetaMapType) } +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) +} + func (ws *WshServer) ConnStatusCommand(ctx context.Context) ([]wshrpc.ConnStatus, error) { rtn := conncontroller.GetAllConnStatus() return rtn, nil @@ -685,6 +690,25 @@ func (ws *WshServer) WslDefaultDistroCommand(ctx context.Context) (string, error return distro.Name(), nil } +/** + * Dismisses the WshFail Command in runtime memory on the backend + */ +func (ws *WshServer) DismissWshFailCommand(ctx context.Context, connName string) error { + opts, err := remote.ParseOpts(connName) + if err != nil { + return err + } + conn := conncontroller.GetConn(ctx, opts, false, nil) + if conn == nil { + return fmt.Errorf("connection %s not found", connName) + } + conn.WithLock(func() { + conn.WshError = "" + }) + conn.FireConnChangeEvent() + return nil +} + func (ws *WshServer) BlockInfoCommand(ctx context.Context, blockId string) (*wshrpc.BlockInfoData, error) { blockData, err := wstore.DBMustGet[*waveobj.Block](ctx, blockId) if err != nil {