diff --git a/cmd/wsh/cmd/wshcmd-connreinstall.go b/cmd/wsh/cmd/wshcmd-connreinstall.go new file mode 100644 index 000000000..e73cbe5d6 --- /dev/null +++ b/cmd/wsh/cmd/wshcmd-connreinstall.go @@ -0,0 +1,38 @@ +// Copyright 2024, Command Line Inc. +// SPDX-License-Identifier: Apache-2.0 + +package cmd + +import ( + "github.com/spf13/cobra" + "github.com/wavetermdev/thenextwave/pkg/remote" + "github.com/wavetermdev/thenextwave/pkg/wshrpc" + "github.com/wavetermdev/thenextwave/pkg/wshrpc/wshclient" +) + +var connReinstallCmd = &cobra.Command{ + Use: "connreinstall", + Short: "reinstall wsh on a connection", + Args: cobra.ExactArgs(1), + Run: connReinstallRun, + PreRunE: preRunSetupRpcClient, +} + +func init() { + rootCmd.AddCommand(connReinstallCmd) +} + +func connReinstallRun(cmd *cobra.Command, args []string) { + connName := args[0] + _, err := remote.ParseOpts(connName) + if err != nil { + WriteStderr("[error] cannot parse connection name: %v\n", err) + return + } + err = wshclient.ConnReinstallWshCommand(RpcClient, connName, &wshrpc.RpcOpts{Timeout: 60000}) + if err != nil { + WriteStderr("[error] getting metadata: %v\n", err) + return + } + WriteStdout("wsh reinstalled on connection %q\n", connName) +} diff --git a/frontend/app/store/wshserver.ts b/frontend/app/store/wshserver.ts index 63d57113c..500d2cf79 100644 --- a/frontend/app/store/wshserver.ts +++ b/frontend/app/store/wshserver.ts @@ -17,6 +17,26 @@ class WshServerType { return WOS.wshServerRpcHelper_call("authenticate", data, opts); } + // command "conndisconnect" [call] + ConnDisconnectCommand(data: string, opts?: RpcOpts): Promise { + return WOS.wshServerRpcHelper_call("conndisconnect", data, opts); + } + + // command "connensure" [call] + ConnEnsureCommand(data: string, opts?: RpcOpts): Promise { + return WOS.wshServerRpcHelper_call("connensure", data, opts); + } + + // command "connforceconnect" [call] + ConnForceConnectCommand(data: string, opts?: RpcOpts): Promise { + return WOS.wshServerRpcHelper_call("connforceconnect", data, opts); + } + + // command "connreinstallwsh" [call] + ConnReinstallWshCommand(data: string, opts?: RpcOpts): Promise { + return WOS.wshServerRpcHelper_call("connreinstallwsh", data, opts); + } + // command "controllerinput" [call] ControllerInputCommand(data: CommandBlockInputData, opts?: RpcOpts): Promise { return WOS.wshServerRpcHelper_call("controllerinput", data, opts); diff --git a/frontend/types/gotypes.d.ts b/frontend/types/gotypes.d.ts index 0da8492da..79f861e4b 100644 --- a/frontend/types/gotypes.d.ts +++ b/frontend/types/gotypes.d.ts @@ -154,6 +154,7 @@ declare global { status: string; connection: string; connected: boolean; + hasconnected: boolean; error?: string; }; diff --git a/pkg/remote/conncontroller/conncontroller.go b/pkg/remote/conncontroller/conncontroller.go index 154d51ce9..f378f56df 100644 --- a/pkg/remote/conncontroller/conncontroller.go +++ b/pkg/remote/conncontroller/conncontroller.go @@ -53,6 +53,7 @@ type SSHConn struct { ConnController *ssh.Session Error string HasWaiter *atomic.Bool + LastConnectTime int64 } func GetAllConnStatus() []wshrpc.ConnStatus { @@ -70,10 +71,11 @@ func (conn *SSHConn) DeriveConnStatus() wshrpc.ConnStatus { conn.Lock.Lock() defer conn.Lock.Unlock() return wshrpc.ConnStatus{ - Status: conn.Status, - Connected: conn.Status == Status_Connected, - Connection: conn.Opts.String(), - Error: conn.Error, + Status: conn.Status, + Connected: conn.Status == Status_Connected, + Connection: conn.Opts.String(), + HasConnected: (conn.LastConnectTime > 0), + Error: conn.Error, } } @@ -243,7 +245,15 @@ func (conn *SSHConn) StartConnServer() error { return nil } -func (conn *SSHConn) checkAndInstallWsh(ctx context.Context, clientDisplayName string) error { +type WshInstallOpts struct { + Force bool + NoUserPrompt bool +} + +func (conn *SSHConn) CheckAndInstallWsh(ctx context.Context, clientDisplayName string, opts *WshInstallOpts) error { + if opts == nil { + opts = &WshInstallOpts{} + } client := conn.GetClient() if client == nil { return fmt.Errorf("client is nil") @@ -251,12 +261,15 @@ func (conn *SSHConn) checkAndInstallWsh(ctx context.Context, clientDisplayName s // check that correct wsh extensions are installed expectedVersion := fmt.Sprintf("wsh v%s", wavebase.WaveVersion) clientVersion, err := remote.GetWshVersion(client) - if err == nil && clientVersion == expectedVersion { + if err == nil && clientVersion == expectedVersion && !opts.Force { return nil } var queryText string var title string - if err != nil { + if opts.Force { + queryText = fmt.Sprintf("ReInstalling Wave Shell Extensions (%s) on `%s`\n", wavebase.WaveVersion, clientDisplayName) + title = "Install Wave Shell Extensions" + } else if err != nil { queryText = fmt.Sprintf("Wave requires Wave Shell Extensions to be \n"+ "installed on `%s` \n"+ "to ensure a seamless experience. \n\n"+ @@ -269,16 +282,18 @@ func (conn *SSHConn) checkAndInstallWsh(ctx context.Context, clientDisplayName s "Would you like to update?", clientDisplayName, clientVersion, expectedVersion) title = "Update Wave Shell Extensions" } - request := &userinput.UserInputRequest{ - ResponseType: "confirm", - QueryText: queryText, - Title: title, - Markdown: true, - CheckBoxMsg: "Don't show me this again", - } - response, err := userinput.GetUserInput(ctx, request) - if err != nil || !response.Confirm { - return err + if !opts.NoUserPrompt { + request := &userinput.UserInputRequest{ + ResponseType: "confirm", + QueryText: queryText, + Title: title, + Markdown: true, + CheckBoxMsg: "Don't show me this again", + } + response, err := userinput.GetUserInput(ctx, request) + if err != nil || !response.Confirm { + return err + } } log.Printf("attempting to install wsh to `%s`", clientDisplayName) clientOs, err := remote.GetClientOs(client) @@ -337,6 +352,7 @@ func (conn *SSHConn) Connect(ctx context.Context) error { conn.close_nolock() } else { conn.Status = Status_Connected + conn.LastConnectTime = time.Now().UnixMilli() } }) conn.FireConnChangeEvent() @@ -363,7 +379,7 @@ func (conn *SSHConn) connectInternal(ctx context.Context) error { if err != nil { return err } - installErr := conn.checkAndInstallWsh(ctx, clientDisplayName) + installErr := conn.CheckAndInstallWsh(ctx, clientDisplayName, nil) if installErr != nil { return fmt.Errorf("conncontroller %s wsh install error: %v", conn.GetName(), installErr) } @@ -385,14 +401,13 @@ func (conn *SSHConn) waitForDisconnect() { } err := client.Wait() conn.WithLock(func() { - if err != nil { - if conn.Status != Status_Disconnected { - // don't set the error if our status is disconnected (because this error was caused by an explicit close) - conn.Status = Status_Error - conn.Error = err.Error() - } - } else { - // not sure if this is possible, because I think Wait() always returns an error (although that's not in the docs) + // disconnects happen for a variety of reasons (like network, etc. and are typically transient) + // so we just set the status to "disconnected" here (not error) + // don't overwrite any existing error (or error status) + if err != nil && conn.Error == "" { + conn.Error = err.Error() + } + if conn.Status != Status_Error { conn.Status = Status_Disconnected } conn.close_nolock() diff --git a/pkg/wshrpc/wshclient/wshclient.go b/pkg/wshrpc/wshclient/wshclient.go index cfc65028d..24a0ec485 100644 --- a/pkg/wshrpc/wshclient/wshclient.go +++ b/pkg/wshrpc/wshclient/wshclient.go @@ -24,6 +24,30 @@ func AuthenticateCommand(w *wshutil.WshRpc, data string, opts *wshrpc.RpcOpts) ( return resp, err } +// command "conndisconnect", wshserver.ConnDisconnectCommand +func ConnDisconnectCommand(w *wshutil.WshRpc, data string, opts *wshrpc.RpcOpts) error { + _, err := sendRpcRequestCallHelper[any](w, "conndisconnect", data, opts) + return err +} + +// command "connensure", wshserver.ConnEnsureCommand +func ConnEnsureCommand(w *wshutil.WshRpc, data string, opts *wshrpc.RpcOpts) error { + _, err := sendRpcRequestCallHelper[any](w, "connensure", data, opts) + return err +} + +// command "connforceconnect", wshserver.ConnForceConnectCommand +func ConnForceConnectCommand(w *wshutil.WshRpc, data string, opts *wshrpc.RpcOpts) error { + _, err := sendRpcRequestCallHelper[any](w, "connforceconnect", data, opts) + return err +} + +// command "connreinstallwsh", wshserver.ConnReinstallWshCommand +func ConnReinstallWshCommand(w *wshutil.WshRpc, data string, opts *wshrpc.RpcOpts) error { + _, err := sendRpcRequestCallHelper[any](w, "connreinstallwsh", data, opts) + return err +} + // command "controllerinput", wshserver.ControllerInputCommand func ControllerInputCommand(w *wshutil.WshRpc, data wshrpc.CommandBlockInputData, opts *wshrpc.RpcOpts) error { _, err := sendRpcRequestCallHelper[any](w, "controllerinput", data, opts) diff --git a/pkg/wshrpc/wshrpctypes.go b/pkg/wshrpc/wshrpctypes.go index 4c780c4d3..c87945204 100644 --- a/pkg/wshrpc/wshrpctypes.go +++ b/pkg/wshrpc/wshrpctypes.go @@ -63,6 +63,11 @@ const ( Command_RemoteWriteFile = "remotewritefile" Command_RemoteFileDelete = "remotefiledelete" Command_RemoteFileJoiin = "remotefilejoin" + + Command_ConnEnsure = "connensure" + Command_ConnReinstallWsh = "connreinstallwsh" + Command_ConnForceConnect = "connforceconnect" + Command_ConnDisconnect = "conndisconnect" ) type RespOrErrorUnion[T any] struct { @@ -98,6 +103,12 @@ type WshRpcInterface interface { TestCommand(ctx context.Context, data string) error SetConfigCommand(ctx context.Context, data wconfig.MetaSettingsType) error + // connection functions + ConnEnsureCommand(ctx context.Context, connName string) error + ConnReinstallWshCommand(ctx context.Context, connName string) error + ConnForceConnectCommand(ctx context.Context, connName string) error + ConnDisconnectCommand(ctx context.Context, connName string) error + // eventrecv is special, it's handled internally by WshRpc with EventListener EventRecvCommand(ctx context.Context, data WaveEvent) error @@ -344,8 +355,9 @@ type TimeSeriesData struct { } type ConnStatus struct { - Status string `json:"status"` - Connection string `json:"connection"` - Connected bool `json:"connected"` - Error string `json:"error,omitempty"` + Status string `json:"status"` + Connection string `json:"connection"` + Connected bool `json:"connected"` + HasConnected bool `json:"hasconnected"` // true if it has *ever* connected successfully + Error string `json:"error,omitempty"` } diff --git a/pkg/wshrpc/wshserver/wshserver.go b/pkg/wshrpc/wshserver/wshserver.go index 8411f11b8..3145117ea 100644 --- a/pkg/wshrpc/wshserver/wshserver.go +++ b/pkg/wshrpc/wshserver/wshserver.go @@ -20,6 +20,8 @@ import ( "github.com/wavetermdev/thenextwave/pkg/blockcontroller" "github.com/wavetermdev/thenextwave/pkg/eventbus" "github.com/wavetermdev/thenextwave/pkg/filestore" + "github.com/wavetermdev/thenextwave/pkg/remote" + "github.com/wavetermdev/thenextwave/pkg/remote/conncontroller" "github.com/wavetermdev/thenextwave/pkg/waveai" "github.com/wavetermdev/thenextwave/pkg/waveobj" "github.com/wavetermdev/thenextwave/pkg/wconfig" @@ -467,3 +469,27 @@ func (ws *WshServer) SetConfigCommand(ctx context.Context, data wconfig.MetaSett log.Printf("SETCONFIG: %v\n", data) return wconfig.SetBaseConfigValue(data.MetaMapType) } + +func (ws *WshServer) ConnEnsureCommand(ctx context.Context, connName string) error { + return nil +} + +func (ws *WshServer) ConnDisconnectCommand(ctx context.Context, connName string) error { + return nil +} + +func (ws *WshServer) ConnForceConnectCommand(ctx context.Context, connName string) error { + return nil +} + +func (ws *WshServer) ConnReinstallWshCommand(ctx context.Context, connName string) error { + connOpts, err := remote.ParseOpts(connName) + if err != nil { + return fmt.Errorf("error parsing connection name: %w", err) + } + conn := conncontroller.GetConn(ctx, connOpts, false) + if conn == nil { + return fmt.Errorf("connection not found: %s", connName) + } + return conn.CheckAndInstallWsh(ctx, connName, &conncontroller.WshInstallOpts{Force: true, NoUserPrompt: true}) +}