conn updates 3 (#1711)

lots of misc connection refactoring / fixes:

* adds blocklogger as a way to writing logging information from the backend directly to the a terminal block
* use blocklogger in conncontroller
* use blocklogger in sshclient
* fix remote name in password prompt
* use sh -c to get around shell weirdness
* remove cmd.exe special cases
* use GetWatcher().GetFullConfig() rather than re-reading the config file
* change order of things we do when establishing a connection.  ask for wsh up front.  then do domain socket, then connserver
* reduce number of sessions required in the common case when wsh is already installed.  running the connserver is now a "multi-command" which checks if it is installed, then asks for the version
* send jwt token over stdin instead of in initial command string
* fix focus bug for frontend conn modal
* track more information in connstatus
* simplify wshinstall function
* add nowshreason
* other misc cleanup
This commit is contained in:
Mike Sawka 2025-01-10 14:09:32 -08:00 committed by GitHub
parent 37929d90c1
commit ba5f929b3f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
22 changed files with 682 additions and 241 deletions

View File

@ -17,6 +17,7 @@ import (
"github.com/wavetermdev/waveterm/pkg/authkey" "github.com/wavetermdev/waveterm/pkg/authkey"
"github.com/wavetermdev/waveterm/pkg/blockcontroller" "github.com/wavetermdev/waveterm/pkg/blockcontroller"
"github.com/wavetermdev/waveterm/pkg/blocklogger"
"github.com/wavetermdev/waveterm/pkg/filestore" "github.com/wavetermdev/waveterm/pkg/filestore"
"github.com/wavetermdev/waveterm/pkg/panichandler" "github.com/wavetermdev/waveterm/pkg/panichandler"
"github.com/wavetermdev/waveterm/pkg/remote/conncontroller" "github.com/wavetermdev/waveterm/pkg/remote/conncontroller"
@ -297,6 +298,7 @@ func main() {
go stdinReadWatch() go stdinReadWatch()
go telemetryLoop() go telemetryLoop()
configWatcher() configWatcher()
blocklogger.InitBlockLogger()
webListener, err := web.MakeTCPListener("web") webListener, err := web.MakeTCPListener("web")
if err != nil { if err != nil {
log.Printf("error creating web listener: %v\n", err) log.Printf("error creating web listener: %v\n", err)

View File

@ -128,7 +128,11 @@ func connReinstallRun(cmd *cobra.Command, args []string) error {
if err := validateConnectionName(connName); err != nil { if err := validateConnectionName(connName); err != nil {
return err return err
} }
err := wshclient.ConnReinstallWshCommand(RpcClient, connName, &wshrpc.RpcOpts{Timeout: 60000}) data := wshrpc.ConnExtData{
ConnName: connName,
LogBlockId: RpcContext.BlockId,
}
err := wshclient.ConnReinstallWshCommand(RpcClient, data, &wshrpc.RpcOpts{Timeout: 60000})
if err != nil { if err != nil {
return fmt.Errorf("reinstalling connection: %w", err) return fmt.Errorf("reinstalling connection: %w", err)
} }
@ -173,7 +177,11 @@ func connConnectRun(cmd *cobra.Command, args []string) error {
if err := validateConnectionName(connName); err != nil { if err := validateConnectionName(connName); err != nil {
return err return err
} }
err := wshclient.ConnConnectCommand(RpcClient, wshrpc.ConnRequest{Host: connName}, &wshrpc.RpcOpts{Timeout: 60000}) data := wshrpc.ConnRequest{
Host: connName,
LogBlockId: RpcContext.BlockId,
}
err := wshclient.ConnConnectCommand(RpcClient, data, &wshrpc.RpcOpts{Timeout: 60000})
if err != nil { if err != nil {
return fmt.Errorf("connecting connection: %w", err) return fmt.Errorf("connecting connection: %w", err)
} }
@ -186,7 +194,11 @@ func connEnsureRun(cmd *cobra.Command, args []string) error {
if err := validateConnectionName(connName); err != nil { if err := validateConnectionName(connName); err != nil {
return err return err
} }
err := wshclient.ConnEnsureCommand(RpcClient, connName, &wshrpc.RpcOpts{Timeout: 60000}) data := wshrpc.ConnExtData{
ConnName: connName,
LogBlockId: RpcContext.BlockId,
}
err := wshclient.ConnEnsureCommand(RpcClient, data, &wshrpc.RpcOpts{Timeout: 60000})
if err != nil { if err != nil {
return fmt.Errorf("ensuring connection: %w", err) return fmt.Errorf("ensuring connection: %w", err)
} }

View File

@ -39,7 +39,8 @@ func sshRun(cmd *cobra.Command, args []string) (rtnErr error) {
} }
// first, make a connection independent of the block // first, make a connection independent of the block
connOpts := wshrpc.ConnRequest{ connOpts := wshrpc.ConnRequest{
Host: sshArg, Host: sshArg,
LogBlockId: blockId,
Keywords: wshrpc.ConnKeywords{ Keywords: wshrpc.ConnKeywords{
SshIdentityFile: identityFiles, SshIdentityFile: identityFiles,
}, },

View File

@ -23,10 +23,10 @@ import {
getSettingsKeyAtom, getSettingsKeyAtom,
getUserName, getUserName,
globalStore, globalStore,
refocusNode,
useBlockAtom, useBlockAtom,
WOS, WOS,
} from "@/app/store/global"; } from "@/app/store/global";
import { globalRefocusWithTimeout } from "@/app/store/keymodel";
import { RpcApi } from "@/app/store/wshclientapi"; import { RpcApi } from "@/app/store/wshclientapi";
import { TabRpcClient } from "@/app/store/wshrpcutil"; import { TabRpcClient } from "@/app/store/wshrpcutil";
import { ErrorBoundary } from "@/element/errorboundary"; import { ErrorBoundary } from "@/element/errorboundary";
@ -356,7 +356,11 @@ const ConnStatusOverlay = React.memo(
}, [width, connStatus, setShowError]); }, [width, connStatus, setShowError]);
const handleTryReconnect = React.useCallback(() => { const handleTryReconnect = React.useCallback(() => {
const prtn = RpcApi.ConnConnectCommand(TabRpcClient, { host: connName }, { timeout: 60000 }); const prtn = RpcApi.ConnConnectCommand(
TabRpcClient,
{ host: connName, logblockid: nodeModel.blockId },
{ timeout: 60000 }
);
prtn.catch((e) => console.log("error reconnecting", connName, e)); prtn.catch((e) => console.log("error reconnecting", connName, e));
}, [connName]); }, [connName]);
@ -541,7 +545,11 @@ const BlockFrame_Default_Component = (props: BlockFrameProps) => {
const connName = blockData?.meta?.connection; const connName = blockData?.meta?.connection;
if (!util.isBlank(connName)) { if (!util.isBlank(connName)) {
console.log("ensure conn", nodeModel.blockId, connName); console.log("ensure conn", nodeModel.blockId, connName);
RpcApi.ConnEnsureCommand(TabRpcClient, connName, { timeout: 60000 }).catch((e) => { RpcApi.ConnEnsureCommand(
TabRpcClient,
{ connname: connName, logblockid: nodeModel.blockId },
{ timeout: 60000 }
).catch((e) => {
console.log("error ensuring connection", nodeModel.blockId, connName, e); console.log("error ensuring connection", nodeModel.blockId, connName, e);
}); });
} }
@ -691,7 +699,11 @@ const ChangeConnectionBlockModal = React.memo(
meta: { connection: connName, file: newCwd }, meta: { connection: connName, file: newCwd },
}); });
try { try {
await RpcApi.ConnEnsureCommand(TabRpcClient, connName, { timeout: 60000 }); await RpcApi.ConnEnsureCommand(
TabRpcClient,
{ connname: connName, logblockid: blockId },
{ timeout: 60000 }
);
} catch (e) { } catch (e) {
console.log("error connecting", blockId, connName, e); console.log("error connecting", blockId, connName, e);
} }
@ -756,7 +768,7 @@ const ChangeConnectionBlockModal = React.memo(
onSelect: async (_: string) => { onSelect: async (_: string) => {
const prtn = RpcApi.ConnConnectCommand( const prtn = RpcApi.ConnConnectCommand(
TabRpcClient, TabRpcClient,
{ host: connStatus.connection }, { host: connStatus.connection, logblockid: blockId },
{ timeout: 60000 } { timeout: 60000 }
); );
prtn.catch((e) => console.log("error reconnecting", connStatus.connection, e)); prtn.catch((e) => console.log("error reconnecting", connStatus.connection, e));
@ -879,12 +891,13 @@ const ChangeConnectionBlockModal = React.memo(
} else { } else {
changeConnection(rowItem.value); changeConnection(rowItem.value);
globalStore.set(changeConnModalAtom, false); globalStore.set(changeConnModalAtom, false);
globalRefocusWithTimeout(10);
} }
} }
if (keyutil.checkKeyPressed(waveEvent, "Escape")) { if (keyutil.checkKeyPressed(waveEvent, "Escape")) {
globalStore.set(changeConnModalAtom, false); globalStore.set(changeConnModalAtom, false);
setConnSelected(""); setConnSelected("");
refocusNode(blockId); globalRefocusWithTimeout(10);
return true; return true;
} }
if (keyutil.checkKeyPressed(waveEvent, "ArrowUp")) { if (keyutil.checkKeyPressed(waveEvent, "ArrowUp")) {
@ -916,6 +929,7 @@ const ChangeConnectionBlockModal = React.memo(
onSelect={(selected: string) => { onSelect={(selected: string) => {
changeConnection(selected); changeConnection(selected);
globalStore.set(changeConnModalAtom, false); globalStore.set(changeConnModalAtom, false);
globalRefocusWithTimeout(10);
}} }}
selectIndex={rowIndex} selectIndex={rowIndex}
autoFocus={isNodeFocused} autoFocus={isNodeFocused}

View File

@ -146,6 +146,12 @@ function handleCmdI() {
globalRefocus(); globalRefocus();
} }
function globalRefocusWithTimeout(timeoutVal: number) {
setTimeout(() => {
globalRefocus();
}, timeoutVal);
}
function globalRefocus() { function globalRefocus() {
const layoutModel = getLayoutModelForStaticTab(); const layoutModel = getLayoutModelForStaticTab();
const focusedNode = globalStore.get(layoutModel.focusedNode); const focusedNode = globalStore.get(layoutModel.focusedNode);
@ -403,6 +409,7 @@ export {
getAllGlobalKeyBindings, getAllGlobalKeyBindings,
getSimpleControlShiftAtom, getSimpleControlShiftAtom,
globalRefocus, globalRefocus,
globalRefocusWithTimeout,
registerControlShiftStateUpdateHandler, registerControlShiftStateUpdateHandler,
registerElectronReinjectKeyHandler, registerElectronReinjectKeyHandler,
registerGlobalKeys, registerGlobalKeys,

View File

@ -38,7 +38,7 @@ class RpcApiType {
} }
// command "connensure" [call] // command "connensure" [call]
ConnEnsureCommand(client: WshClient, data: string, opts?: RpcOpts): Promise<void> { ConnEnsureCommand(client: WshClient, data: ConnExtData, opts?: RpcOpts): Promise<void> {
return client.wshRpcCall("connensure", data, opts); return client.wshRpcCall("connensure", data, opts);
} }
@ -48,7 +48,7 @@ class RpcApiType {
} }
// command "connreinstallwsh" [call] // command "connreinstallwsh" [call]
ConnReinstallWshCommand(client: WshClient, data: string, opts?: RpcOpts): Promise<void> { ConnReinstallWshCommand(client: WshClient, data: ConnExtData, opts?: RpcOpts): Promise<void> {
return client.wshRpcCall("connreinstallwsh", data, opts); return client.wshRpcCall("connreinstallwsh", data, opts);
} }
@ -57,6 +57,11 @@ class RpcApiType {
return client.wshRpcCall("connstatus", null, opts); return client.wshRpcCall("connstatus", null, opts);
} }
// command "controllerappendoutput" [call]
ControllerAppendOutputCommand(client: WshClient, data: CommandControllerAppendOutputData, opts?: RpcOpts): Promise<void> {
return client.wshRpcCall("controllerappendoutput", data, opts);
}
// command "controllerinput" [call] // command "controllerinput" [call]
ControllerInputCommand(client: WshClient, data: CommandBlockInputData, opts?: RpcOpts): Promise<void> { ControllerInputCommand(client: WshClient, data: CommandBlockInputData, opts?: RpcOpts): Promise<void> {
return client.wshRpcCall("controllerinput", data, opts); return client.wshRpcCall("controllerinput", data, opts);

View File

@ -364,7 +364,7 @@ export class PreviewModel implements ViewModel {
this.connection = atom<Promise<string>>(async (get) => { this.connection = atom<Promise<string>>(async (get) => {
const connName = get(this.blockAtom)?.meta?.connection; const connName = get(this.blockAtom)?.meta?.connection;
try { try {
await RpcApi.ConnEnsureCommand(TabRpcClient, connName, { timeout: 60000 }); await RpcApi.ConnEnsureCommand(TabRpcClient, { connname: connName }, { timeout: 60000 });
globalStore.set(this.connectionError, ""); globalStore.set(this.connectionError, "");
} catch (e) { } catch (e) {
globalStore.set(this.connectionError, e as string); globalStore.set(this.connectionError, e as string);

View File

@ -682,6 +682,45 @@ class TermViewModel implements ViewModel {
}, },
}); });
} }
const debugConn = blockData?.meta?.["term:conndebug"];
fullMenu.push({
label: "Debug Connection",
submenu: [
{
label: "Off",
type: "checkbox",
checked: !debugConn,
click: () => {
RpcApi.SetMetaCommand(TabRpcClient, {
oref: WOS.makeORef("block", this.blockId),
meta: { "term:conndebug": null },
});
},
},
{
label: "Info",
type: "checkbox",
checked: debugConn == "info",
click: () => {
RpcApi.SetMetaCommand(TabRpcClient, {
oref: WOS.makeORef("block", this.blockId),
meta: { "term:conndebug": "info" },
});
},
},
{
label: "Verbose",
type: "checkbox",
checked: debugConn == "debug",
click: () => {
RpcApi.SetMetaCommand(TabRpcClient, {
oref: WOS.makeORef("block", this.blockId),
meta: { "term:conndebug": "debug" },
});
},
},
],
});
return fullMenu; return fullMenu;
} }
} }

View File

@ -125,6 +125,12 @@ declare global {
view: string; view: string;
}; };
// wshrpc.CommandControllerAppendOutputData
type CommandControllerAppendOutputData = {
blockid: string;
data64: string;
};
// wshrpc.CommandControllerResyncData // wshrpc.CommandControllerResyncData
type CommandControllerResyncData = { type CommandControllerResyncData = {
forcerestart?: boolean; forcerestart?: boolean;
@ -286,11 +292,18 @@ declare global {
metamaptype: MetaType; metamaptype: MetaType;
}; };
// wshrpc.ConnExtData
type ConnExtData = {
connname: string;
logblockid?: string;
};
// wshrpc.ConnKeywords // wshrpc.ConnKeywords
type ConnKeywords = { type ConnKeywords = {
"conn:wshenabled"?: boolean; "conn:wshenabled"?: boolean;
"conn:askbeforewshinstall"?: boolean; "conn:askbeforewshinstall"?: boolean;
"conn:overrideconfig"?: boolean; "conn:overrideconfig"?: boolean;
"conn:wshpath"?: string;
"display:hidden"?: boolean; "display:hidden"?: boolean;
"display:order"?: number; "display:order"?: number;
"term:*"?: boolean; "term:*"?: boolean;
@ -317,6 +330,7 @@ declare global {
type ConnRequest = { type ConnRequest = {
host: string; host: string;
keywords?: ConnKeywords; keywords?: ConnKeywords;
logblockid?: string;
}; };
// wshrpc.ConnStatus // wshrpc.ConnStatus
@ -329,6 +343,8 @@ declare global {
activeconnnum: number; activeconnnum: number;
error?: string; error?: string;
wsherror?: string; wsherror?: string;
nowshreason?: string;
wshversion?: string;
}; };
// wshrpc.CpuDataRequest // wshrpc.CpuDataRequest
@ -494,6 +510,7 @@ declare global {
"term:vdomtoolbarblockid"?: string; "term:vdomtoolbarblockid"?: string;
"term:transparency"?: number; "term:transparency"?: number;
"term:allowbracketedpaste"?: boolean; "term:allowbracketedpaste"?: boolean;
"term:conndebug"?: string;
"web:zoom"?: number; "web:zoom"?: number;
"web:hidenav"?: boolean; "web:hidenav"?: boolean;
"markdown:fontsize"?: number; "markdown:fontsize"?: number;

View File

@ -16,6 +16,7 @@ import (
"sync/atomic" "sync/atomic"
"time" "time"
"github.com/wavetermdev/waveterm/pkg/blocklogger"
"github.com/wavetermdev/waveterm/pkg/filestore" "github.com/wavetermdev/waveterm/pkg/filestore"
"github.com/wavetermdev/waveterm/pkg/panichandler" "github.com/wavetermdev/waveterm/pkg/panichandler"
"github.com/wavetermdev/waveterm/pkg/remote" "github.com/wavetermdev/waveterm/pkg/remote"
@ -375,9 +376,7 @@ func (bc *BlockController) setupAndStartShellProcess(rc *RunShellOpts, blockMeta
} else { } else {
shellProc, err = shellexec.StartRemoteShellProc(rc.TermSize, cmdStr, cmdOpts, conn) shellProc, err = shellexec.StartRemoteShellProc(rc.TermSize, cmdStr, cmdOpts, conn)
if err != nil { if err != nil {
conn.WithLock(func() { conn.SetWshError(err)
conn.WshError = err.Error()
})
conn.WshEnabled.Store(false) conn.WshEnabled.Store(false)
log.Printf("error starting remote shell proc with wsh: %v", err) log.Printf("error starting remote shell proc with wsh: %v", err)
log.Print("attempting install without wsh") log.Print("attempting install without wsh")
@ -759,6 +758,13 @@ func getOrCreateBlockController(tabId string, blockId string, controllerName str
return bc return bc
} }
func formatConnNameForLog(connName string) string {
if connName == "" {
return "local"
}
return connName
}
func ResyncController(ctx context.Context, tabId string, blockId string, rtOpts *waveobj.RuntimeOpts, force bool) error { func ResyncController(ctx context.Context, tabId string, blockId string, rtOpts *waveobj.RuntimeOpts, force bool) error {
if tabId == "" || blockId == "" { if tabId == "" || blockId == "" {
return fmt.Errorf("invalid tabId or blockId passed to ResyncController") return fmt.Errorf("invalid tabId or blockId passed to ResyncController")
@ -769,6 +775,7 @@ func ResyncController(ctx context.Context, tabId string, blockId string, rtOpts
} }
if force { if force {
StopBlockController(blockId) StopBlockController(blockId)
time.Sleep(100 * time.Millisecond) // TODO see if we can remove this (the "process finished with exit code" message comes out after we start reconnecting otherwise)
} }
connName := blockData.Meta.GetString(waveobj.MetaKey_Connection, "") connName := blockData.Meta.GetString(waveobj.MetaKey_Connection, "")
controllerName := blockData.Meta.GetString(waveobj.MetaKey_Controller, "") controllerName := blockData.Meta.GetString(waveobj.MetaKey_Controller, "")
@ -784,8 +791,10 @@ func ResyncController(ctx context.Context, tabId string, blockId string, rtOpts
if curBc != nil { if curBc != nil {
bcStatus := curBc.GetRuntimeStatus() bcStatus := curBc.GetRuntimeStatus()
if bcStatus.ShellProcStatus == Status_Running && bcStatus.ShellProcConnName != connName { if bcStatus.ShellProcStatus == Status_Running && bcStatus.ShellProcConnName != connName {
blocklogger.Infof(ctx, "\n[conndebug] stopping blockcontroller due to conn change %q => %q\n", formatConnNameForLog(bcStatus.ShellProcConnName), formatConnNameForLog(connName))
log.Printf("stopping blockcontroller %s due to conn change\n", blockId) log.Printf("stopping blockcontroller %s due to conn change\n", blockId)
StopBlockControllerAndSetStatus(blockId, Status_Init) StopBlockControllerAndSetStatus(blockId, Status_Init)
time.Sleep(100 * time.Millisecond) // TODO see if we can remove this (the "process finished with exit code" message comes out after we start reconnecting otherwise)
} }
} }
// now if there is a conn, ensure it is connected // now if there is a conn, ensure it is connected

View File

@ -0,0 +1,92 @@
// Copyright 2025, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
package blocklogger
import (
"context"
"encoding/base64"
"fmt"
"log"
"strings"
"github.com/wavetermdev/waveterm/pkg/wshrpc"
"github.com/wavetermdev/waveterm/pkg/wshrpc/wshclient"
)
// Buffer size for the output channel
const outputBufferSize = 1000
var outputChan chan wshrpc.CommandControllerAppendOutputData
func InitBlockLogger() {
outputChan = make(chan wshrpc.CommandControllerAppendOutputData, outputBufferSize)
// Start the output runner
go outputRunner()
}
func outputRunner() {
defer log.Printf("blocklogger: outputRunner exiting")
client := wshclient.GetBareRpcClient()
for data := range outputChan {
// Process each output request synchronously, waiting for response
wshclient.ControllerAppendOutputCommand(client, data, nil)
}
}
type logBlockIdContextKeyType struct{}
var logBlockIdContextKey = logBlockIdContextKeyType{}
type logBlockIdData struct {
BlockId string
Verbose bool
}
func ContextWithLogBlockId(ctx context.Context, blockId string, verbose bool) context.Context {
return context.WithValue(ctx, logBlockIdContextKey, &logBlockIdData{BlockId: blockId, Verbose: verbose})
}
func getLogBlockData(ctx context.Context) *logBlockIdData {
if ctx == nil {
return nil
}
dataPtr := ctx.Value(logBlockIdContextKey)
if dataPtr == nil {
return nil
}
return dataPtr.(*logBlockIdData)
}
func queueLogData(data wshrpc.CommandControllerAppendOutputData) {
select {
case outputChan <- data:
default:
}
}
func writeLogf(blockId string, format string, args []any) {
logStr := fmt.Sprintf(format, args...)
logStr = strings.ReplaceAll(logStr, "\n", "\r\n")
data := wshrpc.CommandControllerAppendOutputData{
BlockId: blockId,
Data64: base64.StdEncoding.EncodeToString([]byte(logStr)),
}
queueLogData(data)
}
func Infof(ctx context.Context, format string, args ...any) {
logData := getLogBlockData(ctx)
if logData == nil {
return
}
writeLogf(logData.BlockId, format, args)
}
func Debugf(ctx context.Context, format string, args ...interface{}) {
logData := getLogBlockData(ctx)
if logData == nil || !logData.Verbose {
return
}
writeLogf(logData.BlockId, format, args)
}

View File

@ -20,6 +20,7 @@ import (
"github.com/kevinburke/ssh_config" "github.com/kevinburke/ssh_config"
"github.com/skeema/knownhosts" "github.com/skeema/knownhosts"
"github.com/wavetermdev/waveterm/pkg/blocklogger"
"github.com/wavetermdev/waveterm/pkg/genconn" "github.com/wavetermdev/waveterm/pkg/genconn"
"github.com/wavetermdev/waveterm/pkg/panichandler" "github.com/wavetermdev/waveterm/pkg/panichandler"
"github.com/wavetermdev/waveterm/pkg/remote" "github.com/wavetermdev/waveterm/pkg/remote"
@ -56,16 +57,24 @@ type SSHConn struct {
WshEnabled *atomic.Bool WshEnabled *atomic.Bool
Opts *remote.SSHOpts Opts *remote.SSHOpts
Client *ssh.Client Client *ssh.Client
SockName string DomainSockName string // if "", then no domain socket
DomainSockListener net.Listener DomainSockListener net.Listener
ConnController *ssh.Session ConnController *ssh.Session
Error string Error string
WshError string WshError string
NoWshReason string
WshVersion string
HasWaiter *atomic.Bool HasWaiter *atomic.Bool
LastConnectTime int64 LastConnectTime int64
ActiveConnNum int ActiveConnNum int
} }
var ConnServerCmdTemplate = strings.TrimSpace(`
%s version || echo "not-installed"
read jwt_token
WAVETERM_JWT="$jwt_token" %s connserver
`)
func GetAllConnStatus() []wshrpc.ConnStatus { func GetAllConnStatus() []wshrpc.ConnStatus {
globalLock.Lock() globalLock.Lock()
defer globalLock.Unlock() defer globalLock.Unlock()
@ -96,15 +105,22 @@ func (conn *SSHConn) DeriveConnStatus() wshrpc.ConnStatus {
return wshrpc.ConnStatus{ return wshrpc.ConnStatus{
Status: conn.Status, Status: conn.Status,
Connected: conn.Status == Status_Connected, Connected: conn.Status == Status_Connected,
WshEnabled: conn.WshEnabled.Load(),
Connection: conn.Opts.String(), Connection: conn.Opts.String(),
HasConnected: (conn.LastConnectTime > 0), HasConnected: (conn.LastConnectTime > 0),
ActiveConnNum: conn.ActiveConnNum, ActiveConnNum: conn.ActiveConnNum,
Error: conn.Error, Error: conn.Error,
WshEnabled: conn.WshEnabled.Load(),
WshError: conn.WshError, WshError: conn.WshError,
NoWshReason: conn.NoWshReason,
WshVersion: conn.WshVersion,
} }
} }
func (conn *SSHConn) Infof(ctx context.Context, format string, args ...any) {
log.Print(fmt.Sprintf("[conn:%s] ", conn.GetName()) + fmt.Sprintf(format, args...))
blocklogger.Infof(ctx, "[conndebug] "+format, args...)
}
func (conn *SSHConn) FireConnChangeEvent() { func (conn *SSHConn) FireConnChangeEvent() {
status := conn.DeriveConnStatus() status := conn.DeriveConnStatus()
event := wps.WaveEvent{ event := wps.WaveEvent{
@ -143,6 +159,7 @@ func (conn *SSHConn) close_nolock() {
if conn.DomainSockListener != nil { if conn.DomainSockListener != nil {
conn.DomainSockListener.Close() conn.DomainSockListener.Close()
conn.DomainSockListener = nil conn.DomainSockListener = nil
conn.DomainSockName = ""
} }
if conn.ConnController != nil { if conn.ConnController != nil {
conn.ConnController.Close() conn.ConnController.Close()
@ -157,7 +174,7 @@ func (conn *SSHConn) close_nolock() {
func (conn *SSHConn) GetDomainSocketName() string { func (conn *SSHConn) GetDomainSocketName() string {
conn.Lock.Lock() conn.Lock.Lock()
defer conn.Lock.Unlock() defer conn.Lock.Unlock()
return conn.SockName return conn.DomainSockName
} }
func (conn *SSHConn) GetStatus() string { func (conn *SSHConn) GetStatus() string {
@ -171,14 +188,10 @@ func (conn *SSHConn) GetName() string {
return conn.Opts.String() return conn.Opts.String()
} }
func (conn *SSHConn) OpenDomainSocketListener() error { func (conn *SSHConn) OpenDomainSocketListener(ctx context.Context) error {
var allowed bool conn.Infof(ctx, "running OpenDomainSocketListener...\n")
conn.WithLock(func() { allowed := WithLockRtn(conn, func() bool {
if conn.Status != Status_Connecting { return conn.Status == Status_Connecting
allowed = false
} else {
allowed = true
}
}) })
if !allowed { if !allowed {
return fmt.Errorf("cannot open domain socket for %q when status is %q", conn.GetName(), conn.GetStatus()) return fmt.Errorf("cannot open domain socket for %q when status is %q", conn.GetName(), conn.GetStatus())
@ -189,39 +202,57 @@ func (conn *SSHConn) OpenDomainSocketListener() error {
return fmt.Errorf("error generating random string: %w", err) return fmt.Errorf("error generating random string: %w", err)
} }
sockName := fmt.Sprintf("/tmp/waveterm-%s.sock", randStr) sockName := fmt.Sprintf("/tmp/waveterm-%s.sock", randStr)
log.Printf("remote domain socket %s %q\n", conn.GetName(), conn.GetDomainSocketName()) conn.Infof(ctx, "generated domain socket name %s\n", sockName)
listener, err := client.ListenUnix(sockName) listener, err := client.ListenUnix(sockName)
if err != nil { if err != nil {
return fmt.Errorf("unable to request connection domain socket: %v", err) return fmt.Errorf("unable to request connection domain socket: %v", err)
} }
conn.WithLock(func() { conn.WithLock(func() {
conn.SockName = sockName conn.DomainSockName = sockName
conn.DomainSockListener = listener conn.DomainSockListener = listener
}) })
conn.Infof(ctx, "successfully connected domain socket\n")
go func() { go func() {
defer func() { defer func() {
panichandler.PanicHandler("conncontroller:OpenDomainSocketListener", recover()) panichandler.PanicHandler("conncontroller:OpenDomainSocketListener", recover())
}() }()
defer conn.WithLock(func() { defer conn.WithLock(func() {
conn.DomainSockListener = nil conn.DomainSockListener = nil
conn.SockName = "" conn.DomainSockName = ""
}) })
wshutil.RunWshRpcOverListener(listener) wshutil.RunWshRpcOverListener(listener)
}() }()
return nil return nil
} }
func (conn *SSHConn) StartConnServer() error { // expects the output of `wsh version` which looks like `wsh v0.10.4` or "not-installed"
var allowed bool // returns (up-to-date, semver, error)
conn.WithLock(func() { // if not up to date, or error, version might be ""
if conn.Status != Status_Connecting { func isWshVersionUpToDate(wshVersionLine string) (bool, string, error) {
allowed = false wshVersionLine = strings.TrimSpace(wshVersionLine)
} else { if wshVersionLine == "not-installed" {
allowed = true return false, "", nil
} }
parts := strings.Fields(wshVersionLine)
if len(parts) != 2 {
return false, "", fmt.Errorf("unexpected version format: %s", wshVersionLine)
}
clientVersion := parts[1]
expectedVersion := fmt.Sprintf("v%s", wavebase.WaveVersion)
if semver.Compare(clientVersion, expectedVersion) < 0 {
return false, clientVersion, nil
}
return true, clientVersion, nil
}
// returns (needsInstall, clientVersion, error)
func (conn *SSHConn) StartConnServer(ctx context.Context) (bool, string, error) {
conn.Infof(ctx, "running StartConnServer...\n")
allowed := WithLockRtn(conn, func() bool {
return conn.Status == Status_Connecting
}) })
if !allowed { if !allowed {
return fmt.Errorf("cannot start conn server for %q when status is %q", conn.GetName(), conn.GetStatus()) return false, "", fmt.Errorf("cannot start conn server for %q when status is %q", conn.GetName(), conn.GetStatus())
} }
client := conn.GetClient() client := conn.GetClient()
wshPath := remote.GetWshPath(client) wshPath := remote.GetWshPath(client)
@ -232,29 +263,49 @@ func (conn *SSHConn) StartConnServer() error {
sockName := conn.GetDomainSocketName() sockName := conn.GetDomainSocketName()
jwtToken, err := wshutil.MakeClientJWTToken(rpcCtx, sockName) jwtToken, err := wshutil.MakeClientJWTToken(rpcCtx, sockName)
if err != nil { if err != nil {
return fmt.Errorf("unable to create jwt token for conn controller: %w", err) return false, "", fmt.Errorf("unable to create jwt token for conn controller: %w", err)
} }
sshSession, err := client.NewSession() sshSession, err := client.NewSession()
if err != nil { if err != nil {
return fmt.Errorf("unable to create ssh session for conn controller: %w", err) return false, "", fmt.Errorf("unable to create ssh session for conn controller: %w", err)
} }
pipeRead, pipeWrite := io.Pipe() pipeRead, pipeWrite := io.Pipe()
sshSession.Stdout = pipeWrite sshSession.Stdout = pipeWrite
sshSession.Stderr = pipeWrite sshSession.Stderr = pipeWrite
shellPath, err := remote.DetectShell(client) stdinPipe, err := sshSession.StdinPipe()
if err != nil { if err != nil {
return err return false, "", fmt.Errorf("unable to get stdin pipe: %w", err)
}
var cmdStr string
if remote.IsPowershell(shellPath) {
cmdStr = fmt.Sprintf("$env:%s=\"%s\"; %s connserver", wshutil.WaveJwtTokenVarName, jwtToken, wshPath)
} else {
cmdStr = fmt.Sprintf("%s=\"%s\" %s connserver", wshutil.WaveJwtTokenVarName, jwtToken, wshPath)
} }
cmdStr := fmt.Sprintf(ConnServerCmdTemplate, wshPath, wshPath)
log.Printf("starting conn controller: %s\n", cmdStr) log.Printf("starting conn controller: %s\n", cmdStr)
err = sshSession.Start(cmdStr) shWrappedCmdStr := fmt.Sprintf("sh -c %s", genconn.HardQuote(cmdStr))
err = sshSession.Start(shWrappedCmdStr)
if err != nil { if err != nil {
return fmt.Errorf("unable to start conn controller: %w", err) return false, "", fmt.Errorf("unable to start conn controller command: %w", err)
}
linesChan := wshutil.StreamToLinesChan(pipeRead)
versionLine, err := wshutil.ReadLineWithTimeout(linesChan, 2*time.Second)
if err != nil {
sshSession.Close()
return false, "", fmt.Errorf("error reading wsh version: %w", err)
}
conn.Infof(ctx, "got connserver version: %s\n", strings.TrimSpace(versionLine))
isUpToDate, clientVersion, err := isWshVersionUpToDate(versionLine)
if err != nil {
sshSession.Close()
return false, "", fmt.Errorf("error checking wsh version: %w", err)
}
conn.Infof(ctx, "connserver update to date: %v\n", isUpToDate)
if !isUpToDate {
sshSession.Close()
return true, clientVersion, nil
}
// write the jwt
conn.Infof(ctx, "writing jwt token to connserver\n")
_, err = fmt.Fprintf(stdinPipe, "%s\n", jwtToken)
if err != nil {
sshSession.Close()
return false, clientVersion, fmt.Errorf("failed to write JWT token: %w", err)
} }
conn.WithLock(func() { conn.WithLock(func() {
conn.ConnController = sshSession conn.ConnController = sshSession
@ -265,35 +316,46 @@ func (conn *SSHConn) StartConnServer() error {
panichandler.PanicHandler("conncontroller:sshSession.Wait", recover()) panichandler.PanicHandler("conncontroller:sshSession.Wait", recover())
}() }()
// wait for termination, clear the controller // wait for termination, clear the controller
var waitErr error
defer conn.WithLock(func() { defer conn.WithLock(func() {
if conn.ConnController != nil {
conn.WshEnabled.Store(false)
conn.NoWshReason = "connserver terminated"
if waitErr != nil {
conn.WshError = fmt.Sprintf("connserver terminated unexpectedly with error: %v", waitErr)
}
}
conn.ConnController = nil conn.ConnController = nil
}) })
waitErr := sshSession.Wait() waitErr = sshSession.Wait()
log.Printf("conn controller (%q) terminated: %v", conn.GetName(), waitErr) log.Printf("conn controller (%q) terminated: %v", conn.GetName(), waitErr)
}() }()
go func() { go func() {
defer func() { defer func() {
panichandler.PanicHandler("conncontroller:sshSession-output", recover()) panichandler.PanicHandler("conncontroller:sshSession-output", recover())
}() }()
readErr := wshutil.StreamToLines(pipeRead, func(line []byte) { for output := range linesChan {
lineStr := string(line) if output.Error != nil {
if !strings.HasSuffix(lineStr, "\n") { log.Printf("[conncontroller:%s:output] error: %v\n", conn.GetName(), output.Error)
lineStr += "\n" continue
} }
log.Printf("[conncontroller:%s:output] %s", conn.GetName(), lineStr) line := output.Line
}) if !strings.HasSuffix(line, "\n") {
if readErr != nil && readErr != io.EOF { line += "\n"
log.Printf("[conncontroller:%s] error reading output: %v\n", conn.GetName(), readErr) }
log.Printf("[conncontroller:%s:output] %s", conn.GetName(), line)
} }
}() }()
conn.Infof(ctx, "connserver started, waiting for route to be registered\n")
regCtx, cancelFn := context.WithTimeout(context.Background(), 5*time.Second) regCtx, cancelFn := context.WithTimeout(context.Background(), 5*time.Second)
defer cancelFn() defer cancelFn()
err = wshutil.DefaultRouter.WaitForRegister(regCtx, wshutil.MakeConnectionRouteId(rpcCtx.Conn)) err = wshutil.DefaultRouter.WaitForRegister(regCtx, wshutil.MakeConnectionRouteId(rpcCtx.Conn))
if err != nil { if err != nil {
return fmt.Errorf("timeout waiting for connserver to register") return false, clientVersion, fmt.Errorf("timeout waiting for connserver to register")
} }
time.Sleep(300 * time.Millisecond) // TODO remove this sleep (but we need to wait until connserver is "ready") time.Sleep(300 * time.Millisecond) // TODO remove this sleep (but we need to wait until connserver is "ready")
return nil conn.Infof(ctx, "connserver is registered and ready\n")
return false, clientVersion, nil
} }
type WshInstallOpts struct { type WshInstallOpts struct {
@ -307,78 +369,78 @@ func (wise *WshInstallSkipError) Error() string {
return "skipping wsh installation" return "skipping wsh installation"
} }
func (conn *SSHConn) CheckAndInstallWsh(ctx context.Context, clientDisplayName string, opts *WshInstallOpts) error { var queryTextTemplate = strings.TrimSpace(`
if opts == nil { Wave requires Wave Shell Extensions to be
opts = &WshInstallOpts{} installed on %q
to ensure a seamless experience.
Would you like to install them?
`)
// returns (allowed, error)
func (conn *SSHConn) getPermissionToInstallWsh(ctx context.Context, clientDisplayName string) (bool, error) {
conn.Infof(ctx, "running getPermissionToInstallWsh...\n")
queryText := fmt.Sprintf(queryTextTemplate, clientDisplayName)
title := "Install Wave Shell Extensions"
request := &userinput.UserInputRequest{
ResponseType: "confirm",
QueryText: queryText,
Title: title,
Markdown: true,
CheckBoxMsg: "Automatically install for all connections",
OkLabel: "Install wsh",
CancelLabel: "No wsh",
} }
conn.Infof(ctx, "requesting user confirmation...\n")
response, err := userinput.GetUserInput(ctx, request)
if err != nil {
conn.Infof(ctx, "error getting user input: %v\n", err)
return false, err
}
conn.Infof(ctx, "user response to allowing wsh: %v\n", response.Confirm)
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)
if err != nil {
log.Printf("warning: error writing to connections file: %v", err)
}
if !response.Confirm {
return false, nil
}
if response.CheckboxStat {
conn.Infof(ctx, "writing conn:askbeforewshinstall=false to settings.json\n")
meta := waveobj.MetaMapType{
wconfig.ConfigKey_ConnAskBeforeWshInstall: false,
}
setConfigErr := wconfig.SetBaseConfigValue(meta)
if setConfigErr != nil {
// this is not a critical error, just log and continue
log.Printf("warning: error writing to base config file: %v", err)
}
}
return true, nil
}
func (conn *SSHConn) InstallWsh(ctx context.Context) error {
conn.Infof(ctx, "running installWsh...\n")
client := conn.GetClient() client := conn.GetClient()
if client == nil { if client == nil {
return fmt.Errorf("client is nil") conn.Infof(ctx, "ERROR ssh client is not connected, cannot install\n")
return fmt.Errorf("ssh client is not connected, cannot install")
} }
// check that correct wsh extensions are installed
expectedVersion := fmt.Sprintf("v%s", wavebase.WaveVersion)
clientVersion, err := remote.GetWshVersion(client)
if err == nil && !opts.Force && semver.Compare(clientVersion, expectedVersion) >= 0 {
return nil
}
var queryText string
var title string
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"+
"Would you like to install them?", clientDisplayName)
title = "Install Wave Shell Extensions"
} else {
// don't ask for upgrading the version
opts.NoUserPrompt = true
}
if !opts.NoUserPrompt {
request := &userinput.UserInputRequest{
ResponseType: "confirm",
QueryText: queryText,
Title: title,
Markdown: true,
CheckBoxMsg: "Automatically install for all connections",
OkLabel: "Install wsh",
CancelLabel: "No wsh",
}
response, err := userinput.GetUserInput(ctx, request)
if err != nil {
return err
}
if !response.Confirm {
meta := make(map[string]any)
meta["conn:wshenabled"] = false
err = wconfig.SetConnectionsConfigValue(conn.GetName(), meta)
if err != nil {
log.Printf("warning: error writing to connections file: %v", err)
}
return &WshInstallSkipError{}
}
if response.CheckboxStat {
meta := waveobj.MetaMapType{
wconfig.ConfigKey_ConnAskBeforeWshInstall: false,
}
err := wconfig.SetBaseConfigValue(meta)
if err != nil {
return fmt.Errorf("error setting conn:askbeforewshinstall value: %w", err)
}
}
}
log.Printf("attempting to install wsh to `%s`", clientDisplayName)
clientOs, clientArch, err := remote.GetClientPlatform(ctx, genconn.MakeSSHShellClient(client)) clientOs, clientArch, err := remote.GetClientPlatform(ctx, genconn.MakeSSHShellClient(client))
if err != nil { if err != nil {
conn.Infof(ctx, "ERROR detecting client platform: %v\n", err)
return err return err
} }
conn.Infof(ctx, "detected remote platform os:%s arch:%s\n", clientOs, clientArch)
err = remote.CpWshToRemote(ctx, client, clientOs, clientArch) err = remote.CpWshToRemote(ctx, client, clientOs, clientArch)
if err != nil { if err != nil {
return fmt.Errorf("error installing wsh to remote: %w", err) conn.Infof(ctx, "ERROR copying wsh binary to remote: %v\n", err)
return fmt.Errorf("error copying wsh binary to remote: %w", err)
} }
log.Printf("successfully installed wsh on %s\n", conn.GetName()) conn.Infof(ctx, "successfully installed wsh\n")
return nil return nil
} }
@ -422,6 +484,7 @@ func (conn *SSHConn) WaitForConnect(ctx context.Context) error {
// does not return an error since that error is stored inside of SSHConn // does not return an error since that error is stored inside of SSHConn
func (conn *SSHConn) Connect(ctx context.Context, connFlags *wshrpc.ConnKeywords) error { func (conn *SSHConn) Connect(ctx context.Context, connFlags *wshrpc.ConnKeywords) error {
blocklogger.Infof(ctx, "\n")
var connectAllowed bool var connectAllowed bool
conn.WithLock(func() { conn.WithLock(func() {
if conn.Status == Status_Connecting || conn.Status == Status_Connected { if conn.Status == Status_Connecting || conn.Status == Status_Connected {
@ -432,14 +495,16 @@ func (conn *SSHConn) Connect(ctx context.Context, connFlags *wshrpc.ConnKeywords
connectAllowed = true connectAllowed = true
} }
}) })
log.Printf("Connect %s\n", conn.GetName())
if !connectAllowed { if !connectAllowed {
conn.Infof(ctx, "cannot connect to when status is %q\n", conn.GetStatus())
return fmt.Errorf("cannot connect to %q when status is %q", conn.GetName(), conn.GetStatus()) return fmt.Errorf("cannot connect to %q when status is %q", conn.GetName(), conn.GetStatus())
} }
conn.Infof(ctx, "trying to connect to %q...\n", conn.GetName())
conn.FireConnChangeEvent() conn.FireConnChangeEvent()
err := conn.connectInternal(ctx, connFlags) err := conn.connectInternal(ctx, connFlags)
conn.WithLock(func() { conn.WithLock(func() {
if err != nil { if err != nil {
conn.Infof(ctx, "ERROR %v\n\n", err)
conn.Status = Status_Error conn.Status = Status_Error
conn.Error = err.Error() conn.Error = err.Error()
conn.close_nolock() conn.close_nolock()
@ -447,6 +512,7 @@ func (conn *SSHConn) Connect(ctx context.Context, connFlags *wshrpc.ConnKeywords
Conn: map[string]int{"ssh:connecterror": 1}, Conn: map[string]int{"ssh:connecterror": 1},
}, "ssh-connconnect") }, "ssh-connconnect")
} else { } else {
conn.Infof(ctx, "successfully connected (wsh:%v)\n\n", conn.WshEnabled.Load())
conn.Status = Status_Connected conn.Status = Status_Connected
conn.LastConnectTime = time.Now().UnixMilli() conn.LastConnectTime = time.Now().UnixMilli()
if conn.ActiveConnNum == 0 { if conn.ActiveConnNum == 0 {
@ -465,7 +531,7 @@ func (conn *SSHConn) Connect(ctx context.Context, connFlags *wshrpc.ConnKeywords
// logic for saving connection and potential flags (we only save once a connection has been made successfully) // logic for saving connection and potential flags (we only save once a connection has been made successfully)
// at the moment, identity files is the only saved flag // at the moment, identity files is the only saved flag
var identityFiles []string var identityFiles []string
existingConfig := wconfig.ReadFullConfig() existingConfig := wconfig.GetWatcher().GetFullConfig()
existingConnection, ok := existingConfig.Connections[conn.GetName()] existingConnection, ok := existingConfig.Connections[conn.GetName()]
if ok { if ok {
identityFiles = existingConnection.SshIdentityFile identityFiles = existingConnection.SshIdentityFile
@ -499,79 +565,143 @@ func (conn *SSHConn) WithLock(fn func()) {
fn() fn()
} }
func (conn *SSHConn) connectInternal(ctx context.Context, connFlags *wshrpc.ConnKeywords) error { func WithLockRtn[T any](conn *SSHConn, fn func() T) T {
client, _, err := remote.ConnectToClient(ctx, conn.Opts, nil, 0, connFlags) conn.Lock.Lock()
if err != nil { defer conn.Lock.Unlock()
log.Printf("error: failed to connect to client %s: %s\n", conn.GetName(), err) return fn()
return err }
}
fmtAddr := knownhosts.Normalize(fmt.Sprintf("%s@%s", client.User(), client.RemoteAddr().String())) // returns (enable-wsh, ask-before-install)
clientDisplayName := fmt.Sprintf("%s (%s)", conn.GetName(), fmtAddr) func (conn *SSHConn) getConnWshSettings() (bool, bool) {
conn.WithLock(func() { config := wconfig.GetWatcher().GetFullConfig()
conn.Client = client
})
config := wconfig.ReadFullConfig()
enableWsh := config.Settings.ConnWshEnabled enableWsh := config.Settings.ConnWshEnabled
askBeforeInstall := config.Settings.ConnAskBeforeWshInstall askBeforeInstall := wconfig.DefaultBoolPtr(config.Settings.ConnAskBeforeWshInstall, true)
connSettings, ok := config.Connections[conn.GetName()] connSettings, ok := config.Connections[conn.GetName()]
if ok { if ok {
if connSettings.ConnWshEnabled != nil { if connSettings.ConnWshEnabled != nil {
enableWsh = *connSettings.ConnWshEnabled enableWsh = *connSettings.ConnWshEnabled
} }
if connSettings.ConnAskBeforeWshInstall != nil { // if the connection object exists, and conn:askbeforewshinstall is not set, the user must have allowed it
// TODO: in v0.12+ this should be removed. we'll explicitly write a "false" into the connection object on successful connection
if connSettings.ConnAskBeforeWshInstall == nil {
askBeforeInstall = false
} else {
askBeforeInstall = *connSettings.ConnAskBeforeWshInstall askBeforeInstall = *connSettings.ConnAskBeforeWshInstall
} }
} }
if enableWsh { return enableWsh, askBeforeInstall
installErr := conn.CheckAndInstallWsh(ctx, clientDisplayName, &WshInstallOpts{NoUserPrompt: !askBeforeInstall}) }
if errors.Is(installErr, &WshInstallSkipError{}) {
// skips are not true errors
conn.WithLock(func() {
conn.WshEnabled.Store(false)
})
} else if installErr != nil {
log.Printf("error: unable to install wsh shell extensions for %s: %v\n", conn.GetName(), err)
log.Print("attempting to run with nowsh instead")
conn.WithLock(func() {
conn.WshError = installErr.Error()
})
conn.WshEnabled.Store(false)
} else {
conn.WshEnabled.Store(true)
}
if conn.WshEnabled.Load() { type WshCheckResult struct {
dsErr := conn.OpenDomainSocketListener() WshEnabled bool
var csErr error ClientVersion string
if dsErr != nil { NoWshReason string
log.Printf("error: unable to open domain socket listener for %s: %v\n", conn.GetName(), dsErr) WshError error
} else { }
csErr = conn.StartConnServer()
if csErr != nil { // returns (wsh-enabled, clientVersion, text-reason, wshError)
log.Printf("error: unable to start conn server for %s: %v\n", conn.GetName(), csErr) func (conn *SSHConn) tryEnableWsh(ctx context.Context, clientDisplayName string) WshCheckResult {
} conn.Infof(ctx, "running tryEnableWsh...\n")
} enableWsh, askBeforeInstall := conn.getConnWshSettings()
if dsErr != nil || csErr != nil { conn.Infof(ctx, "wsh settings enable:%v ask:%v\n", enableWsh, askBeforeInstall)
log.Print("attempting to run with nowsh instead") if !enableWsh {
var errmsgs []string return WshCheckResult{NoWshReason: "conn:wshenabled set to false"}
if dsErr != nil {
errmsgs = append(errmsgs, fmt.Sprintf("domain socket error: %s", dsErr.Error()))
}
if csErr != nil {
errmsgs = append(errmsgs, fmt.Sprintf("conn server error: %s", csErr.Error()))
}
combinedErr := fmt.Errorf("%s", strings.Join(errmsgs, " | "))
conn.WithLock(func() {
conn.WshError = combinedErr.Error()
})
conn.WshEnabled.Store(false)
}
}
} else {
conn.WshEnabled.Store(false)
} }
conn.HasWaiter.Store(true) if askBeforeInstall {
allowInstall, err := conn.getPermissionToInstallWsh(ctx, clientDisplayName)
if err != nil {
log.Printf("error getting permission to install wsh: %v\n", err)
return WshCheckResult{NoWshReason: "error getting user permission to install", WshError: err}
}
if !allowInstall {
return WshCheckResult{NoWshReason: "user selected not to install wsh extensions"}
}
}
err := conn.OpenDomainSocketListener(ctx)
if err != nil {
conn.Infof(ctx, "ERROR opening domain socket listener: %v\n", err)
err = fmt.Errorf("error opening domain socket listener: %w", err)
return WshCheckResult{NoWshReason: "error opening domain socket", WshError: err}
}
needsInstall, clientVersion, err := conn.StartConnServer(ctx)
if err != nil {
conn.Infof(ctx, "ERROR starting conn server: %v\n", err)
err = fmt.Errorf("error starting conn server: %w", err)
return WshCheckResult{NoWshReason: "error starting connserver", WshError: err}
}
if needsInstall {
conn.Infof(ctx, "connserver needs to be (re)installed\n")
err = conn.InstallWsh(ctx)
if err != nil {
conn.Infof(ctx, "ERROR installing wsh: %v\n", err)
err = fmt.Errorf("error installing wsh: %w", err)
return WshCheckResult{NoWshReason: "error installing wsh/connserver", WshError: err}
}
needsInstall, clientVersion, err = conn.StartConnServer(ctx)
if err != nil {
conn.Infof(ctx, "ERROR starting conn server (after install): %v\n", err)
err = fmt.Errorf("error starting conn server (after install): %w", err)
return WshCheckResult{NoWshReason: "error starting connserver", WshError: err}
}
if needsInstall {
conn.Infof(ctx, "conn server not installed correctly (after install)\n")
err = fmt.Errorf("conn server not installed correctly (after install)")
return WshCheckResult{NoWshReason: "connserver not installed properly", WshError: err}
}
return WshCheckResult{WshEnabled: true, ClientVersion: clientVersion}
} else {
return WshCheckResult{WshEnabled: true, ClientVersion: clientVersion}
}
}
func (conn *SSHConn) persistWshInstalled(ctx context.Context, result WshCheckResult) {
conn.WshEnabled.Store(result.WshEnabled)
conn.SetWshError(result.WshError)
conn.WithLock(func() {
conn.NoWshReason = result.NoWshReason
conn.WshVersion = result.ClientVersion
})
config := wconfig.GetWatcher().GetFullConfig()
connSettings, ok := config.Connections[conn.GetName()]
if ok && connSettings.ConnWshEnabled != nil {
return
}
meta := make(map[string]any)
meta["conn:wshenabled"] = result.WshEnabled
err := wconfig.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)
}
// doesn't return an error since none of this is required for connection to work
}
// returns (connect-error)
func (conn *SSHConn) connectInternal(ctx context.Context, connFlags *wshrpc.ConnKeywords) error {
conn.Infof(ctx, "connectInternal %s\n", conn.GetName())
client, _, err := remote.ConnectToClient(ctx, conn.Opts, nil, 0, connFlags)
if err != nil {
conn.Infof(ctx, "ERROR ConnectToClient: %s\n", remote.SimpleMessageFromPossibleConnectionError(err))
log.Printf("error: failed to connect to client %s: %s\n", conn.GetName(), err)
return err
}
conn.WithLock(func() {
conn.Client = client
})
go conn.waitForDisconnect() go conn.waitForDisconnect()
fmtAddr := knownhosts.Normalize(fmt.Sprintf("%s@%s", client.User(), client.RemoteAddr().String()))
conn.Infof(ctx, "normalized knownhosts address: %s\n", fmtAddr)
clientDisplayName := fmt.Sprintf("%s (%s)", conn.GetName(), fmtAddr)
wshResult := conn.tryEnableWsh(ctx, clientDisplayName)
if !wshResult.WshEnabled {
if wshResult.WshError != nil {
conn.Infof(ctx, "ERROR enabling wsh: %v\n", wshResult.WshError)
conn.Infof(ctx, "will connect with wsh disabled\n")
} else {
conn.Infof(ctx, "wsh not enabled: %s\n", wshResult.NoWshReason)
}
}
conn.persistWshInstalled(ctx, wshResult)
return nil return nil
} }
@ -597,6 +727,22 @@ func (conn *SSHConn) waitForDisconnect() {
}) })
} }
func (conn *SSHConn) SetWshError(err error) {
conn.WithLock(func() {
if err == nil {
conn.WshError = ""
} else {
conn.WshError = err.Error()
}
})
}
func (conn *SSHConn) ClearWshError() {
conn.WithLock(func() {
conn.WshError = ""
})
}
func getConnInternal(opts *remote.SSHOpts) *SSHConn { func getConnInternal(opts *remote.SSHOpts) *SSHConn {
globalLock.Lock() globalLock.Lock()
defer globalLock.Unlock() defer globalLock.Unlock()
@ -743,7 +889,7 @@ func GetConnectionsList() ([]string, error) {
func GetConnectionsFromInternalConfig() []string { func GetConnectionsFromInternalConfig() []string {
var internalNames []string var internalNames []string
config := wconfig.ReadFullConfig() config := wconfig.GetWatcher().GetFullConfig()
for internalName := range config.Connections { for internalName := range config.Connections {
if strings.HasPrefix(internalName, "wsl://") { if strings.HasPrefix(internalName, "wsl://") {
// don't add wsl conns to this list // don't add wsl conns to this list

View File

@ -20,7 +20,6 @@ import (
"github.com/wavetermdev/waveterm/pkg/util/shellutil" "github.com/wavetermdev/waveterm/pkg/util/shellutil"
"github.com/wavetermdev/waveterm/pkg/wavebase" "github.com/wavetermdev/waveterm/pkg/wavebase"
"golang.org/x/crypto/ssh" "golang.org/x/crypto/ssh"
"golang.org/x/mod/semver"
) )
var userHostRe = regexp.MustCompile(`^([a-zA-Z0-9][a-zA-Z0-9._@\\-]*@)?([a-zA-Z0-9][a-zA-Z0-9.-]*)(?::([0-9]+))?$`) var userHostRe = regexp.MustCompile(`^([a-zA-Z0-9][a-zA-Z0-9._@\\-]*@)?([a-zA-Z0-9][a-zA-Z0-9.-]*)(?::([0-9]+))?$`)
@ -55,32 +54,6 @@ func DetectShell(client *ssh.Client) (string, error) {
return fmt.Sprintf(`"%s"`, strings.TrimSpace(string(out))), nil return fmt.Sprintf(`"%s"`, strings.TrimSpace(string(out))), nil
} }
// returns a valid semver version string
func GetWshVersion(client *ssh.Client) (string, error) {
wshPath := GetWshPath(client)
session, err := client.NewSession()
if err != nil {
return "", err
}
out, err := session.Output(wshPath + " version")
if err != nil {
return "", err
}
// output is expected to be in the form of "wsh v0.10.4"
// should strip off the "wsh" prefix, and return a semver object
fields := strings.Fields(strings.TrimSpace(string(out)))
if len(fields) != 2 {
return "", fmt.Errorf("unexpected output from wsh version: %s", out)
}
wshVersion := strings.TrimSpace(fields[1])
if !semver.IsValid(wshVersion) {
return "", fmt.Errorf("invalid semver version: %s", wshVersion)
}
return wshVersion, nil
}
func GetWshPath(client *ssh.Client) string { func GetWshPath(client *ssh.Client) string {
defaultPath := wavebase.RemoteFullWshBinPath defaultPath := wavebase.RemoteFullWshBinPath
session, err := client.NewSession() session, err := client.NewSession()

View File

@ -23,6 +23,7 @@ import (
"github.com/kevinburke/ssh_config" "github.com/kevinburke/ssh_config"
"github.com/skeema/knownhosts" "github.com/skeema/knownhosts"
"github.com/wavetermdev/waveterm/pkg/blocklogger"
"github.com/wavetermdev/waveterm/pkg/panichandler" "github.com/wavetermdev/waveterm/pkg/panichandler"
"github.com/wavetermdev/waveterm/pkg/trimquotes" "github.com/wavetermdev/waveterm/pkg/trimquotes"
"github.com/wavetermdev/waveterm/pkg/userinput" "github.com/wavetermdev/waveterm/pkg/userinput"
@ -72,9 +73,19 @@ type ConnectionError struct {
func (ce ConnectionError) Error() string { func (ce ConnectionError) Error() string {
if ce.CurrentClient == nil { if ce.CurrentClient == nil {
return fmt.Sprintf("Connecting to %+#v, Error: %v", ce.NextOpts, ce.Err) return fmt.Sprintf("Connecting to %s, Error: %v", ce.NextOpts, ce.Err)
} }
return fmt.Sprintf("Connecting from %v to %+#v (jump number %d), Error: %v", ce.CurrentClient, ce.NextOpts, ce.JumpNum, ce.Err) return fmt.Sprintf("Connecting from %v to %s (jump number %d), Error: %v", ce.CurrentClient, ce.NextOpts, ce.JumpNum, ce.Err)
}
func SimpleMessageFromPossibleConnectionError(err error) string {
if err == nil {
return ""
}
if ce, ok := err.(ConnectionError); ok {
return ce.Err.Error()
}
return err.Error()
} }
// This exists to trick the ssh library into continuing to try // This exists to trick the ssh library into continuing to try
@ -142,6 +153,7 @@ func createPublicKeyCallback(connCtx context.Context, sshKeywords *wshrpc.ConnKe
return nil, ConnectionError{ConnectionDebugInfo: debugInfo, Err: fmt.Errorf("no identity files remaining")} return nil, ConnectionError{ConnectionDebugInfo: debugInfo, Err: fmt.Errorf("no identity files remaining")}
} }
identityFile := (*identityFilesPtr)[0] identityFile := (*identityFilesPtr)[0]
blocklogger.Infof(connCtx, "[conndebug] trying keyfile %q...\n", identityFile)
*identityFilesPtr = (*identityFilesPtr)[1:] *identityFilesPtr = (*identityFilesPtr)[1:]
privateKey, ok := existingKeys[identityFile] privateKey, ok := existingKeys[identityFile]
if !ok { if !ok {
@ -208,6 +220,7 @@ func createPublicKeyCallback(connCtx context.Context, sshKeywords *wshrpc.ConnKe
func createInteractivePasswordCallbackPrompt(connCtx context.Context, remoteDisplayName string, debugInfo *ConnectionDebugInfo) func() (secret string, err error) { func createInteractivePasswordCallbackPrompt(connCtx context.Context, remoteDisplayName string, debugInfo *ConnectionDebugInfo) func() (secret string, err error) {
return func() (secret string, err error) { return func() (secret string, err error) {
blocklogger.Infof(connCtx, "[conndebug] Password Authentication requested from connection %s...\n", remoteDisplayName)
ctx, cancelFn := context.WithTimeout(connCtx, 60*time.Second) ctx, cancelFn := context.WithTimeout(connCtx, 60*time.Second)
defer cancelFn() defer cancelFn()
queryText := fmt.Sprintf( queryText := fmt.Sprintf(
@ -222,8 +235,10 @@ func createInteractivePasswordCallbackPrompt(connCtx context.Context, remoteDisp
} }
response, err := userinput.GetUserInput(ctx, request) response, err := userinput.GetUserInput(ctx, request)
if err != nil { if err != nil {
blocklogger.Infof(connCtx, "[conndebug] ERROR Password Authentication failed: %v\n", SimpleMessageFromPossibleConnectionError(err))
return "", ConnectionError{ConnectionDebugInfo: debugInfo, Err: err} return "", ConnectionError{ConnectionDebugInfo: debugInfo, Err: err}
} }
blocklogger.Infof(connCtx, "[conndebug] got password from user, sending to ssh\n")
return response.Text, nil return response.Text, nil
} }
} }
@ -557,7 +572,10 @@ func createClientConfig(connCtx context.Context, sshKeywords *wshrpc.ConnKeyword
chosenUser := utilfn.SafeDeref(sshKeywords.SshUser) chosenUser := utilfn.SafeDeref(sshKeywords.SshUser)
chosenHostName := utilfn.SafeDeref(sshKeywords.SshHostName) chosenHostName := utilfn.SafeDeref(sshKeywords.SshHostName)
chosenPort := utilfn.SafeDeref(sshKeywords.SshPort) chosenPort := utilfn.SafeDeref(sshKeywords.SshPort)
remoteName := chosenUser + xknownhosts.Normalize(chosenHostName+":"+chosenPort) remoteName := xknownhosts.Normalize(chosenHostName + ":" + chosenPort)
if chosenUser != "" {
remoteName = chosenUser + "@" + remoteName
}
var authSockSigners []ssh.Signer var authSockSigners []ssh.Signer
var agentClient agent.ExtendedAgent var agentClient agent.ExtendedAgent
@ -619,24 +637,31 @@ func connectInternal(ctx context.Context, networkAddr string, clientConfig *ssh.
var err error var err error
if currentClient == nil { if currentClient == nil {
d := net.Dialer{Timeout: clientConfig.Timeout} d := net.Dialer{Timeout: clientConfig.Timeout}
blocklogger.Infof(ctx, "[conndebug] ssh dial %s\n", networkAddr)
clientConn, err = d.DialContext(ctx, "tcp", networkAddr) clientConn, err = d.DialContext(ctx, "tcp", networkAddr)
if err != nil { if err != nil {
blocklogger.Infof(ctx, "[conndebug] ERROR dial error: %v\n", err)
return nil, err return nil, err
} }
} else { } else {
blocklogger.Infof(ctx, "[conndebug] ssh dial (from client) %s\n", networkAddr)
clientConn, err = currentClient.DialContext(ctx, "tcp", networkAddr) clientConn, err = currentClient.DialContext(ctx, "tcp", networkAddr)
if err != nil { if err != nil {
blocklogger.Infof(ctx, "[conndebug] ERROR dial error: %v\n", err)
return nil, err return nil, err
} }
} }
c, chans, reqs, err := ssh.NewClientConn(clientConn, networkAddr, clientConfig) c, chans, reqs, err := ssh.NewClientConn(clientConn, networkAddr, clientConfig)
if err != nil { if err != nil {
blocklogger.Infof(ctx, "[conndebug] ERROR ssh auth/negotiation: %s\n", SimpleMessageFromPossibleConnectionError(err))
return nil, err return nil, err
} }
blocklogger.Infof(ctx, "[conndebug] successful ssh connection to %s\n", networkAddr)
return ssh.NewClient(c, chans, reqs), nil return ssh.NewClient(c, chans, reqs), nil
} }
func ConnectToClient(connCtx context.Context, opts *SSHOpts, currentClient *ssh.Client, jumpNum int32, connFlags *wshrpc.ConnKeywords) (*ssh.Client, int32, error) { func ConnectToClient(connCtx context.Context, opts *SSHOpts, currentClient *ssh.Client, jumpNum int32, connFlags *wshrpc.ConnKeywords) (*ssh.Client, int32, error) {
blocklogger.Infof(connCtx, "[conndebug] ConnectToClient %s (jump:%d)...\n", opts.String(), jumpNum)
debugInfo := &ConnectionDebugInfo{ debugInfo := &ConnectionDebugInfo{
CurrentClient: currentClient, CurrentClient: currentClient,
NextOpts: opts, NextOpts: opts,
@ -660,7 +685,7 @@ func ConnectToClient(connCtx context.Context, opts *SSHOpts, currentClient *ssh.
} }
rawName := opts.String() rawName := opts.String()
fullConfig := wconfig.ReadFullConfig() fullConfig := wconfig.GetWatcher().GetFullConfig()
internalSshConfigKeywords, ok := fullConfig.Connections[rawName] internalSshConfigKeywords, ok := fullConfig.Connections[rawName]
if !ok { if !ok {
internalSshConfigKeywords = wshrpc.ConnKeywords{} internalSshConfigKeywords = wshrpc.ConnKeywords{}

View File

@ -95,6 +95,7 @@ const (
MetaKey_TermVDomToolbarBlockId = "term:vdomtoolbarblockid" MetaKey_TermVDomToolbarBlockId = "term:vdomtoolbarblockid"
MetaKey_TermTransparency = "term:transparency" MetaKey_TermTransparency = "term:transparency"
MetaKey_TermAllowBracketedPaste = "term:allowbracketedpaste" MetaKey_TermAllowBracketedPaste = "term:allowbracketedpaste"
MetaKey_TermConnDebug = "term:conndebug"
MetaKey_WebZoom = "web:zoom" MetaKey_WebZoom = "web:zoom"
MetaKey_WebHideNav = "web:hidenav" MetaKey_WebHideNav = "web:hidenav"

View File

@ -96,6 +96,7 @@ type MetaTSType struct {
TermVDomToolbarBlockId string `json:"term:vdomtoolbarblockid,omitempty"` TermVDomToolbarBlockId string `json:"term:vdomtoolbarblockid,omitempty"`
TermTransparency *float64 `json:"term:transparency,omitempty"` // default 0.5 TermTransparency *float64 `json:"term:transparency,omitempty"` // default 0.5
TermAllowBracketedPaste *bool `json:"term:allowbracketedpaste,omitempty"` TermAllowBracketedPaste *bool `json:"term:allowbracketedpaste,omitempty"`
TermConnDebug string `json:"term:conndebug,omitempty"` // null, info, debug
WebZoom float64 `json:"web:zoom,omitempty"` WebZoom float64 `json:"web:zoom,omitempty"`
WebHideNav *bool `json:"web:hidenav,omitempty"` WebHideNav *bool `json:"web:hidenav,omitempty"`

View File

@ -115,9 +115,9 @@ type SettingsType struct {
TelemetryClear bool `json:"telemetry:*,omitempty"` TelemetryClear bool `json:"telemetry:*,omitempty"`
TelemetryEnabled bool `json:"telemetry:enabled,omitempty"` TelemetryEnabled bool `json:"telemetry:enabled,omitempty"`
ConnClear bool `json:"conn:*,omitempty"` ConnClear bool `json:"conn:*,omitempty"`
ConnAskBeforeWshInstall bool `json:"conn:askbeforewshinstall,omitempty"` ConnAskBeforeWshInstall *bool `json:"conn:askbeforewshinstall,omitempty"`
ConnWshEnabled bool `json:"conn:wshenabled,omitempty"` ConnWshEnabled bool `json:"conn:wshenabled,omitempty"`
} }
type ConfigError struct { type ConfigError struct {
@ -136,6 +136,13 @@ type FullConfigType struct {
ConfigErrors []ConfigError `json:"configerrors" configfile:"-"` ConfigErrors []ConfigError `json:"configerrors" configfile:"-"`
} }
func DefaultBoolPtr(arg *bool, def bool) bool {
if arg == nil {
return def
}
return *arg
}
func goBackWS(barr []byte, offset int) int { func goBackWS(barr []byte, offset int) int {
if offset >= len(barr) { if offset >= len(barr) {
offset = offset - 1 offset = offset - 1
@ -307,6 +314,8 @@ func readConfigPart(partName string, simpleMerge bool) (waveobj.MetaMapType, []C
return mergeMetaMap(rtn, homeConfigs, simpleMerge), allErrs return mergeMetaMap(rtn, homeConfigs, simpleMerge), allErrs
} }
// this function should only be called by the wconfig code.
// in golang code, the best way to get the current config is via the watcher -- wconfig.GetWatcher().GetFullConfig()
func ReadFullConfig() FullConfigType { func ReadFullConfig() FullConfigType {
var fullConfig FullConfigType var fullConfig FullConfigType
configRType := reflect.TypeOf(fullConfig) configRType := reflect.TypeOf(fullConfig)

View File

@ -50,7 +50,7 @@ func ConnDisconnectCommand(w *wshutil.WshRpc, data string, opts *wshrpc.RpcOpts)
} }
// command "connensure", wshserver.ConnEnsureCommand // command "connensure", wshserver.ConnEnsureCommand
func ConnEnsureCommand(w *wshutil.WshRpc, data string, opts *wshrpc.RpcOpts) error { func ConnEnsureCommand(w *wshutil.WshRpc, data wshrpc.ConnExtData, opts *wshrpc.RpcOpts) error {
_, err := sendRpcRequestCallHelper[any](w, "connensure", data, opts) _, err := sendRpcRequestCallHelper[any](w, "connensure", data, opts)
return err return err
} }
@ -62,7 +62,7 @@ func ConnListCommand(w *wshutil.WshRpc, opts *wshrpc.RpcOpts) ([]string, error)
} }
// command "connreinstallwsh", wshserver.ConnReinstallWshCommand // command "connreinstallwsh", wshserver.ConnReinstallWshCommand
func ConnReinstallWshCommand(w *wshutil.WshRpc, data string, opts *wshrpc.RpcOpts) error { func ConnReinstallWshCommand(w *wshutil.WshRpc, data wshrpc.ConnExtData, opts *wshrpc.RpcOpts) error {
_, err := sendRpcRequestCallHelper[any](w, "connreinstallwsh", data, opts) _, err := sendRpcRequestCallHelper[any](w, "connreinstallwsh", data, opts)
return err return err
} }
@ -73,6 +73,12 @@ func ConnStatusCommand(w *wshutil.WshRpc, opts *wshrpc.RpcOpts) ([]wshrpc.ConnSt
return resp, err return resp, err
} }
// command "controllerappendoutput", wshserver.ControllerAppendOutputCommand
func ControllerAppendOutputCommand(w *wshutil.WshRpc, data wshrpc.CommandControllerAppendOutputData, opts *wshrpc.RpcOpts) error {
_, err := sendRpcRequestCallHelper[any](w, "controllerappendoutput", data, opts)
return err
}
// command "controllerinput", wshserver.ControllerInputCommand // command "controllerinput", wshserver.ControllerInputCommand
func ControllerInputCommand(w *wshutil.WshRpc, data wshrpc.CommandBlockInputData, opts *wshrpc.RpcOpts) error { func ControllerInputCommand(w *wshutil.WshRpc, data wshrpc.CommandBlockInputData, opts *wshrpc.RpcOpts) error {
_, err := sendRpcRequestCallHelper[any](w, "controllerinput", data, opts) _, err := sendRpcRequestCallHelper[any](w, "controllerinput", data, opts)

View File

@ -118,6 +118,7 @@ type WshRpcInterface interface {
ControllerInputCommand(ctx context.Context, data CommandBlockInputData) error ControllerInputCommand(ctx context.Context, data CommandBlockInputData) error
ControllerStopCommand(ctx context.Context, blockId string) error ControllerStopCommand(ctx context.Context, blockId string) error
ControllerResyncCommand(ctx context.Context, data CommandControllerResyncData) error ControllerResyncCommand(ctx context.Context, data CommandControllerResyncData) error
ControllerAppendOutputCommand(ctx context.Context, data CommandControllerAppendOutputData) error
ResolveIdsCommand(ctx context.Context, data CommandResolveIdsData) (CommandResolveIdsRtnData, error) ResolveIdsCommand(ctx context.Context, data CommandResolveIdsData) (CommandResolveIdsRtnData, error)
CreateBlockCommand(ctx context.Context, data CommandCreateBlockData) (waveobj.ORef, error) CreateBlockCommand(ctx context.Context, data CommandCreateBlockData) (waveobj.ORef, error)
CreateSubBlockCommand(ctx context.Context, data CommandCreateSubBlockData) (waveobj.ORef, error) CreateSubBlockCommand(ctx context.Context, data CommandCreateSubBlockData) (waveobj.ORef, error)
@ -154,8 +155,8 @@ type WshRpcInterface interface {
// connection functions // connection functions
ConnStatusCommand(ctx context.Context) ([]ConnStatus, error) ConnStatusCommand(ctx context.Context) ([]ConnStatus, error)
WslStatusCommand(ctx context.Context) ([]ConnStatus, error) WslStatusCommand(ctx context.Context) ([]ConnStatus, error)
ConnEnsureCommand(ctx context.Context, connName string) error ConnEnsureCommand(ctx context.Context, data ConnExtData) error
ConnReinstallWshCommand(ctx context.Context, connName string) error ConnReinstallWshCommand(ctx context.Context, data ConnExtData) error
ConnConnectCommand(ctx context.Context, connRequest ConnRequest) error ConnConnectCommand(ctx context.Context, connRequest ConnRequest) error
ConnDisconnectCommand(ctx context.Context, connName string) error ConnDisconnectCommand(ctx context.Context, connName string) error
ConnListCommand(ctx context.Context) ([]string, error) ConnListCommand(ctx context.Context) ([]string, error)
@ -311,6 +312,11 @@ type CommandControllerResyncData struct {
RtOpts *waveobj.RuntimeOpts `json:"rtopts,omitempty"` RtOpts *waveobj.RuntimeOpts `json:"rtopts,omitempty"`
} }
type CommandControllerAppendOutputData struct {
BlockId string `json:"blockid"`
Data64 string `json:"data64"`
}
type CommandBlockInputData struct { type CommandBlockInputData struct {
BlockId string `json:"blockid" wshcontext:"BlockId"` BlockId string `json:"blockid" wshcontext:"BlockId"`
InputData64 string `json:"inputdata64,omitempty"` InputData64 string `json:"inputdata64,omitempty"`
@ -459,9 +465,10 @@ type CommandRemoteWriteFileData struct {
} }
type ConnKeywords struct { type ConnKeywords struct {
ConnWshEnabled *bool `json:"conn:wshenabled,omitempty"` ConnWshEnabled *bool `json:"conn:wshenabled,omitempty"`
ConnAskBeforeWshInstall *bool `json:"conn:askbeforewshinstall,omitempty"` ConnAskBeforeWshInstall *bool `json:"conn:askbeforewshinstall,omitempty"`
ConnOverrideConfig bool `json:"conn:overrideconfig,omitempty"` ConnOverrideConfig bool `json:"conn:overrideconfig,omitempty"`
ConnWshPath string `json:"conn:wshpath,omitempty"`
DisplayHidden *bool `json:"display:hidden,omitempty"` DisplayHidden *bool `json:"display:hidden,omitempty"`
DisplayOrder float32 `json:"display:order,omitempty"` DisplayOrder float32 `json:"display:order,omitempty"`
@ -488,8 +495,9 @@ type ConnKeywords struct {
} }
type ConnRequest struct { type ConnRequest struct {
Host string `json:"host"` Host string `json:"host"`
Keywords ConnKeywords `json:"keywords,omitempty"` Keywords ConnKeywords `json:"keywords,omitempty"`
LogBlockId string `json:"logblockid,omitempty"`
} }
const ( const (
@ -534,6 +542,8 @@ type ConnStatus struct {
ActiveConnNum int `json:"activeconnnum"` ActiveConnNum int `json:"activeconnnum"`
Error string `json:"error,omitempty"` Error string `json:"error,omitempty"`
WshError string `json:"wsherror,omitempty"` WshError string `json:"wsherror,omitempty"`
NoWshReason string `json:"nowshreason,omitempty"`
WshVersion string `json:"wshversion,omitempty"`
} }
type WebSelectorOpts struct { type WebSelectorOpts struct {
@ -646,3 +656,8 @@ type ActivityUpdate struct {
WshCmds map[string]int `json:"wshcmds,omitempty"` WshCmds map[string]int `json:"wshcmds,omitempty"`
Conn map[string]int `json:"conn,omitempty"` Conn map[string]int `json:"conn,omitempty"`
} }
type ConnExtData struct {
ConnName string `json:"connname"`
LogBlockId string `json:"logblockid,omitempty"`
}

View File

@ -19,6 +19,7 @@ import (
"github.com/skratchdot/open-golang/open" "github.com/skratchdot/open-golang/open"
"github.com/wavetermdev/waveterm/pkg/blockcontroller" "github.com/wavetermdev/waveterm/pkg/blockcontroller"
"github.com/wavetermdev/waveterm/pkg/blocklogger"
"github.com/wavetermdev/waveterm/pkg/filestore" "github.com/wavetermdev/waveterm/pkg/filestore"
"github.com/wavetermdev/waveterm/pkg/panichandler" "github.com/wavetermdev/waveterm/pkg/panichandler"
"github.com/wavetermdev/waveterm/pkg/remote" "github.com/wavetermdev/waveterm/pkg/remote"
@ -237,6 +238,7 @@ func (ws *WshServer) ControllerStopCommand(ctx context.Context, blockId string)
} }
func (ws *WshServer) ControllerResyncCommand(ctx context.Context, data wshrpc.CommandControllerResyncData) error { func (ws *WshServer) ControllerResyncCommand(ctx context.Context, data wshrpc.CommandControllerResyncData) error {
ctx = termCtxWithLogBlockId(ctx, data.BlockId)
return blockcontroller.ResyncController(ctx, data.TabId, data.BlockId, data.RtOpts, data.ForceRestart) return blockcontroller.ResyncController(ctx, data.TabId, data.BlockId, data.RtOpts, data.ForceRestart)
} }
@ -260,6 +262,19 @@ func (ws *WshServer) ControllerInputCommand(ctx context.Context, data wshrpc.Com
return bc.SendInput(inputUnion) return bc.SendInput(inputUnion)
} }
func (ws *WshServer) ControllerAppendOutputCommand(ctx context.Context, data wshrpc.CommandControllerAppendOutputData) error {
outputBuf := make([]byte, base64.StdEncoding.DecodedLen(len(data.Data64)))
nw, err := base64.StdEncoding.Decode(outputBuf, []byte(data.Data64))
if err != nil {
return fmt.Errorf("error decoding output data: %w", err)
}
err = blockcontroller.HandleAppendBlockFile(data.BlockId, blockcontroller.BlockFile_Term, outputBuf[:nw])
if err != nil {
return fmt.Errorf("error appending to block file: %w", err)
}
return nil
}
func (ws *WshServer) FileCreateCommand(ctx context.Context, data wshrpc.CommandFileCreateData) error { func (ws *WshServer) FileCreateCommand(ctx context.Context, data wshrpc.CommandFileCreateData) error {
var fileOpts filestore.FileOptsType var fileOpts filestore.FileOptsType
if data.Opts != nil { if data.Opts != nil {
@ -596,12 +611,28 @@ func (ws *WshServer) WslStatusCommand(ctx context.Context) ([]wshrpc.ConnStatus,
return rtn, nil return rtn, nil
} }
func (ws *WshServer) ConnEnsureCommand(ctx context.Context, connName string) error { func termCtxWithLogBlockId(ctx context.Context, logBlockId string) context.Context {
if strings.HasPrefix(connName, "wsl://") { if logBlockId == "" {
distroName := strings.TrimPrefix(connName, "wsl://") return ctx
}
block, err := wstore.DBMustGet[*waveobj.Block](ctx, logBlockId)
if err != nil {
return ctx
}
connDebug := block.Meta.GetString(waveobj.MetaKey_TermConnDebug, "")
if connDebug == "" {
return ctx
}
return blocklogger.ContextWithLogBlockId(ctx, logBlockId, connDebug == "debug")
}
func (ws *WshServer) ConnEnsureCommand(ctx context.Context, data wshrpc.ConnExtData) error {
ctx = termCtxWithLogBlockId(ctx, data.LogBlockId)
if strings.HasPrefix(data.ConnName, "wsl://") {
distroName := strings.TrimPrefix(data.ConnName, "wsl://")
return wsl.EnsureConnection(ctx, distroName) return wsl.EnsureConnection(ctx, distroName)
} }
return conncontroller.EnsureConnection(ctx, connName) return conncontroller.EnsureConnection(ctx, data.ConnName)
} }
func (ws *WshServer) ConnDisconnectCommand(ctx context.Context, connName string) error { func (ws *WshServer) ConnDisconnectCommand(ctx context.Context, connName string) error {
@ -625,6 +656,7 @@ func (ws *WshServer) ConnDisconnectCommand(ctx context.Context, connName string)
} }
func (ws *WshServer) ConnConnectCommand(ctx context.Context, connRequest wshrpc.ConnRequest) error { func (ws *WshServer) ConnConnectCommand(ctx context.Context, connRequest wshrpc.ConnRequest) error {
ctx = termCtxWithLogBlockId(ctx, connRequest.LogBlockId)
connName := connRequest.Host connName := connRequest.Host
if strings.HasPrefix(connName, "wsl://") { if strings.HasPrefix(connName, "wsl://") {
distroName := strings.TrimPrefix(connName, "wsl://") distroName := strings.TrimPrefix(connName, "wsl://")
@ -645,7 +677,9 @@ func (ws *WshServer) ConnConnectCommand(ctx context.Context, connRequest wshrpc.
return conn.Connect(ctx, &connRequest.Keywords) return conn.Connect(ctx, &connRequest.Keywords)
} }
func (ws *WshServer) ConnReinstallWshCommand(ctx context.Context, connName string) error { func (ws *WshServer) ConnReinstallWshCommand(ctx context.Context, data wshrpc.ConnExtData) error {
ctx = termCtxWithLogBlockId(ctx, data.LogBlockId)
connName := data.ConnName
if strings.HasPrefix(connName, "wsl://") { if strings.HasPrefix(connName, "wsl://") {
distroName := strings.TrimPrefix(connName, "wsl://") distroName := strings.TrimPrefix(connName, "wsl://")
conn := wsl.GetWslConn(ctx, distroName, false) conn := wsl.GetWslConn(ctx, distroName, false)
@ -662,7 +696,7 @@ func (ws *WshServer) ConnReinstallWshCommand(ctx context.Context, connName strin
if conn == nil { if conn == nil {
return fmt.Errorf("connection not found: %s", connName) return fmt.Errorf("connection not found: %s", connName)
} }
return conn.CheckAndInstallWsh(ctx, connName, &conncontroller.WshInstallOpts{Force: true, NoUserPrompt: true}) return conn.InstallWsh(ctx)
} }
func (ws *WshServer) ConnListCommand(ctx context.Context) ([]string, error) { func (ws *WshServer) ConnListCommand(ctx context.Context) ([]string, error) {
@ -708,9 +742,7 @@ func (ws *WshServer) DismissWshFailCommand(ctx context.Context, connName string)
if conn == nil { if conn == nil {
return fmt.Errorf("connection %s not found", connName) return fmt.Errorf("connection %s not found", connName)
} }
conn.WithLock(func() { conn.ClearWshError()
conn.WshError = ""
})
conn.FireConnChangeEvent() conn.FireConnChangeEvent()
return nil return nil
} }

View File

@ -5,8 +5,10 @@ package wshutil
import ( import (
"bytes" "bytes"
"context"
"fmt" "fmt"
"io" "io"
"time"
) )
// special I/O wrappers for wshrpc // special I/O wrappers for wshrpc
@ -55,6 +57,38 @@ func StreamToLines(input io.Reader, lineFn func([]byte)) error {
} }
} }
type LineOutput struct {
Line string
Error error
}
// starts a goroutine to drive the channel
func StreamToLinesChan(input io.Reader) chan LineOutput {
ch := make(chan LineOutput)
go func() {
defer close(ch)
err := StreamToLines(input, func(line []byte) {
ch <- LineOutput{Line: string(line)}
})
if err != nil && err != io.EOF {
ch <- LineOutput{Error: err}
}
}()
return ch
}
func ReadLineWithTimeout(ch chan LineOutput, timeout time.Duration) (string, error) {
select {
case output := <-ch:
if output.Error != nil {
return "", output.Error
}
return output.Line, nil
case <-time.After(timeout):
return "", context.DeadlineExceeded
}
}
func AdaptStreamToMsgCh(input io.Reader, output chan []byte) error { func AdaptStreamToMsgCh(input io.Reader, output chan []byte) error {
return StreamToLines(input, func(line []byte) { return StreamToLines(input, func(line []byte) {
output <- line output <- line

View File

@ -446,8 +446,9 @@ func (conn *WslConn) connectInternal(ctx context.Context) error {
if err != nil { if err != nil {
return err return err
} }
config := wconfig.ReadFullConfig() config := wconfig.GetWatcher().GetFullConfig()
installErr := conn.CheckAndInstallWsh(ctx, conn.GetName(), &WshInstallOpts{NoUserPrompt: !config.Settings.ConnAskBeforeWshInstall}) wshAsk := wconfig.DefaultBoolPtr(config.Settings.ConnAskBeforeWshInstall, true)
installErr := conn.CheckAndInstallWsh(ctx, conn.GetName(), &WshInstallOpts{NoUserPrompt: !wshAsk})
if installErr != nil { if installErr != nil {
return fmt.Errorf("conncontroller %s wsh install error: %v", conn.GetName(), installErr) return fmt.Errorf("conncontroller %s wsh install error: %v", conn.GetName(), installErr)
} }