SSH without using WSH (#1355)

This commit is contained in:
Sylvie Crowe 2024-11-27 16:52:00 -08:00 committed by GitHub
parent c37d292224
commit 24103213aa
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
25 changed files with 474 additions and 251 deletions

View File

@ -27,10 +27,8 @@ func GenerateWshClient() error {
"github.com/wavetermdev/waveterm/pkg/wshutil", "github.com/wavetermdev/waveterm/pkg/wshutil",
"github.com/wavetermdev/waveterm/pkg/wshrpc", "github.com/wavetermdev/waveterm/pkg/wshrpc",
"github.com/wavetermdev/waveterm/pkg/waveobj", "github.com/wavetermdev/waveterm/pkg/waveobj",
"github.com/wavetermdev/waveterm/pkg/wconfig",
"github.com/wavetermdev/waveterm/pkg/wps", "github.com/wavetermdev/waveterm/pkg/wps",
"github.com/wavetermdev/waveterm/pkg/vdom", "github.com/wavetermdev/waveterm/pkg/vdom",
"github.com/wavetermdev/waveterm/pkg/telemetry",
}) })
wshDeclMap := wshrpc.GenerateWshCommandDeclMap() wshDeclMap := wshrpc.GenerateWshCommandDeclMap()
for _, key := range utilfn.GetOrderedMapKeys(wshDeclMap) { for _, key := range utilfn.GetOrderedMapKeys(wshDeclMap) {

View File

@ -113,7 +113,7 @@ func telemetryLoop() {
} }
func panicTelemetryHandler() { func panicTelemetryHandler() {
activity := telemetry.ActivityUpdate{NumPanics: 1} activity := wshrpc.ActivityUpdate{NumPanics: 1}
err := telemetry.UpdateActivity(context.Background(), activity) err := telemetry.UpdateActivity(context.Background(), activity)
if err != nil { if err != nil {
log.Printf("error updating activity (panicTelemetryHandler): %v\n", err) log.Printf("error updating activity (panicTelemetryHandler): %v\n", err)
@ -137,7 +137,7 @@ func sendTelemetryWrapper() {
} }
func beforeSendActivityUpdate(ctx context.Context) { func beforeSendActivityUpdate(ctx context.Context) {
activity := telemetry.ActivityUpdate{} activity := wshrpc.ActivityUpdate{}
activity.NumTabs, _ = wstore.DBGetCount[*waveobj.Tab](ctx) activity.NumTabs, _ = wstore.DBGetCount[*waveobj.Tab](ctx)
activity.NumBlocks, _ = wstore.DBGetCount[*waveobj.Block](ctx) activity.NumBlocks, _ = wstore.DBGetCount[*waveobj.Block](ctx)
activity.Blocks, _ = wstore.DBGetBlockViewCounts(ctx) activity.Blocks, _ = wstore.DBGetBlockViewCounts(ctx)
@ -153,7 +153,7 @@ func beforeSendActivityUpdate(ctx context.Context) {
func startupActivityUpdate() { func startupActivityUpdate() {
ctx, cancelFn := context.WithTimeout(context.Background(), 5*time.Second) ctx, cancelFn := context.WithTimeout(context.Background(), 5*time.Second)
defer cancelFn() defer cancelFn()
activity := telemetry.ActivityUpdate{Startup: 1} activity := wshrpc.ActivityUpdate{Startup: 1}
err := telemetry.UpdateActivity(ctx, activity) // set at least one record into activity (don't use go routine wrap here) err := telemetry.UpdateActivity(ctx, activity) // set at least one record into activity (don't use go routine wrap here)
if err != nil { if err != nil {
log.Printf("error updating startup activity: %v\n", err) log.Printf("error updating startup activity: %v\n", err)
@ -163,7 +163,7 @@ func startupActivityUpdate() {
func shutdownActivityUpdate() { func shutdownActivityUpdate() {
ctx, cancelFn := context.WithTimeout(context.Background(), 1*time.Second) ctx, cancelFn := context.WithTimeout(context.Background(), 1*time.Second)
defer cancelFn() defer cancelFn()
activity := telemetry.ActivityUpdate{Shutdown: 1} activity := wshrpc.ActivityUpdate{Shutdown: 1}
err := telemetry.UpdateActivity(ctx, activity) // do NOT use the go routine wrap here (this needs to be synchronous) err := telemetry.UpdateActivity(ctx, activity) // do NOT use the go routine wrap here (this needs to be synchronous)
if err != nil { if err != nil {
log.Printf("error updating shutdown activity: %v\n", err) log.Printf("error updating shutdown activity: %v\n", err)

View File

@ -167,7 +167,7 @@ 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, connName, &wshrpc.RpcOpts{Timeout: 60000}) err := wshclient.ConnConnectCommand(RpcClient, wshrpc.ConnRequest{Host: connName}, &wshrpc.RpcOpts{Timeout: 60000})
if err != nil { if err != nil {
return fmt.Errorf("connecting connection: %w", err) return fmt.Errorf("connecting connection: %w", err)
} }

View File

@ -7,7 +7,6 @@ import (
"fmt" "fmt"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/wavetermdev/waveterm/pkg/wconfig"
"github.com/wavetermdev/waveterm/pkg/wshrpc" "github.com/wavetermdev/waveterm/pkg/wshrpc"
"github.com/wavetermdev/waveterm/pkg/wshrpc/wshclient" "github.com/wavetermdev/waveterm/pkg/wshrpc/wshclient"
) )
@ -34,7 +33,7 @@ func setConfigRun(cmd *cobra.Command, args []string) (rtnErr error) {
if err != nil { if err != nil {
return err return err
} }
commandData := wconfig.MetaSettingsType{MetaMapType: meta} commandData := wshrpc.MetaSettingsType{MetaMapType: meta}
err = wshclient.SetConfigCommand(RpcClient, commandData, &wshrpc.RpcOpts{Timeout: 2000}) err = wshclient.SetConfigCommand(RpcClient, commandData, &wshrpc.RpcOpts{Timeout: 2000})
if err != nil { if err != nil {
return fmt.Errorf("setting config: %w", err) return fmt.Errorf("setting config: %w", err)

View File

@ -12,6 +12,8 @@ import (
"github.com/wavetermdev/waveterm/pkg/wshrpc/wshclient" "github.com/wavetermdev/waveterm/pkg/wshrpc/wshclient"
) )
var identityFiles []string
var sshCmd = &cobra.Command{ var sshCmd = &cobra.Command{
Use: "ssh", Use: "ssh",
Short: "connect this terminal to a remote host", Short: "connect this terminal to a remote host",
@ -21,6 +23,7 @@ var sshCmd = &cobra.Command{
} }
func init() { func init() {
sshCmd.Flags().StringArrayVarP(&identityFiles, "identityfile", "i", []string{}, "add an identity file for publickey authentication")
rootCmd.AddCommand(sshCmd) rootCmd.AddCommand(sshCmd)
} }
@ -34,6 +37,16 @@ func sshRun(cmd *cobra.Command, args []string) (rtnErr error) {
if blockId == "" { if blockId == "" {
return fmt.Errorf("cannot determine blockid (not in JWT)") return fmt.Errorf("cannot determine blockid (not in JWT)")
} }
// first, make a connection independent of the block
connOpts := wshrpc.ConnRequest{
Host: sshArg,
Keywords: wshrpc.ConnKeywords{
SshIdentityFile: identityFiles,
},
}
wshclient.ConnConnectCommand(RpcClient, connOpts, nil)
// now, with that made, it will be straightforward to connect
data := wshrpc.CommandSetMetaData{ data := wshrpc.CommandSetMetaData{
ORef: waveobj.MakeORef(waveobj.OType_Block, blockId), ORef: waveobj.MakeORef(waveobj.OType_Block, blockId),
Meta: map[string]any{ Meta: map[string]any{

View File

@ -341,7 +341,7 @@ const ConnStatusOverlay = React.memo(
}, [width, connStatus, setShowError]); }, [width, connStatus, setShowError]);
const handleTryReconnect = React.useCallback(() => { const handleTryReconnect = React.useCallback(() => {
const prtn = RpcApi.ConnConnectCommand(TabRpcClient, connName, { timeout: 60000 }); const prtn = RpcApi.ConnConnectCommand(TabRpcClient, { host: connName }, { timeout: 60000 });
prtn.catch((e) => console.log("error reconnecting", connName, e)); prtn.catch((e) => console.log("error reconnecting", connName, e));
}, [connName]); }, [connName]);
@ -673,7 +673,11 @@ const ChangeConnectionBlockModal = React.memo(
label: `Reconnect to ${connStatus.connection}`, label: `Reconnect to ${connStatus.connection}`,
value: "", value: "",
onSelect: async (_: string) => { onSelect: async (_: string) => {
const prtn = RpcApi.ConnConnectCommand(TabRpcClient, connStatus.connection, { timeout: 60000 }); const prtn = RpcApi.ConnConnectCommand(
TabRpcClient,
{ host: connStatus.connection },
{ timeout: 60000 }
);
prtn.catch((e) => console.log("error reconnecting", connStatus.connection, e)); prtn.catch((e) => console.log("error reconnecting", connStatus.connection, e));
}, },
}; };

View File

@ -45,11 +45,17 @@
.userinput-checkbox-container { .userinput-checkbox-container {
display: flex; display: flex;
align-items: center; flex-direction: column;
gap: 6px; gap: 6px;
.userinput-checkbox { .userinput-checkbox-row {
accent-color: var(--accent-color); display: flex;
align-items: center;
gap: 6px;
.userinput-checkbox {
accent-color: var(--accent-color);
}
} }
} }
} }

View File

@ -15,7 +15,7 @@ const UserInputModal = (userInputRequest: UserInputRequest) => {
const [countdown, setCountdown] = useState(Math.floor(userInputRequest.timeoutms / 1000)); const [countdown, setCountdown] = useState(Math.floor(userInputRequest.timeoutms / 1000));
const checkboxRef = useRef<HTMLInputElement>(); const checkboxRef = useRef<HTMLInputElement>();
const handleSendCancel = useCallback(() => { const handleSendErrResponse = useCallback(() => {
UserInputService.SendUserInputResponse({ UserInputService.SendUserInputResponse({
type: "userinputresp", type: "userinputresp",
requestid: userInputRequest.requestid, requestid: userInputRequest.requestid,
@ -29,20 +29,24 @@ const UserInputModal = (userInputRequest: UserInputRequest) => {
type: "userinputresp", type: "userinputresp",
requestid: userInputRequest.requestid, requestid: userInputRequest.requestid,
text: responseText, text: responseText,
checkboxstat: checkboxRef.current?.checked ?? false, checkboxstat: checkboxRef?.current?.checked ?? false,
}); });
modalsModel.popModal(); modalsModel.popModal();
}, [responseText, userInputRequest]); }, [responseText, userInputRequest]);
console.log("bar");
const handleSendConfirm = useCallback(() => { const handleSendConfirm = useCallback(
UserInputService.SendUserInputResponse({ (response: boolean) => {
type: "userinputresp", UserInputService.SendUserInputResponse({
requestid: userInputRequest.requestid, type: "userinputresp",
confirm: true, requestid: userInputRequest.requestid,
checkboxstat: checkboxRef.current?.checked ?? false, confirm: response,
}); checkboxstat: checkboxRef?.current?.checked ?? false,
modalsModel.popModal(); });
}, [userInputRequest]); modalsModel.popModal();
},
[userInputRequest]
);
const handleSubmit = useCallback(() => { const handleSubmit = useCallback(() => {
switch (userInputRequest.responsetype) { switch (userInputRequest.responsetype) {
@ -50,15 +54,16 @@ const UserInputModal = (userInputRequest: UserInputRequest) => {
handleSendText(); handleSendText();
break; break;
case "confirm": case "confirm":
handleSendConfirm(); handleSendConfirm(true);
break; break;
} }
}, [handleSendConfirm, handleSendText, userInputRequest.responsetype]); }, [handleSendConfirm, handleSendText, userInputRequest.responsetype]);
console.log("baz");
const handleKeyDown = useCallback( const handleKeyDown = useCallback(
(waveEvent: WaveKeyboardEvent): boolean => { (waveEvent: WaveKeyboardEvent): boolean => {
if (keyutil.checkKeyPressed(waveEvent, "Escape")) { if (keyutil.checkKeyPressed(waveEvent, "Escape")) {
handleSendCancel(); handleSendErrResponse();
return; return;
} }
if (keyutil.checkKeyPressed(waveEvent, "Enter")) { if (keyutil.checkKeyPressed(waveEvent, "Enter")) {
@ -66,7 +71,7 @@ const UserInputModal = (userInputRequest: UserInputRequest) => {
return true; return true;
} }
}, },
[handleSendCancel, handleSubmit] [handleSendErrResponse, handleSubmit]
); );
const queryText = useMemo(() => { const queryText = useMemo(() => {
@ -75,6 +80,7 @@ const UserInputModal = (userInputRequest: UserInputRequest) => {
} }
return <span className="userinput-text">{userInputRequest.querytext}</span>; return <span className="userinput-text">{userInputRequest.querytext}</span>;
}, [userInputRequest.markdown, userInputRequest.querytext]); }, [userInputRequest.markdown, userInputRequest.querytext]);
console.log("foobarbaz");
const inputBox = useMemo(() => { const inputBox = useMemo(() => {
if (userInputRequest.responsetype === "confirm") { if (userInputRequest.responsetype === "confirm") {
@ -92,6 +98,7 @@ const UserInputModal = (userInputRequest: UserInputRequest) => {
/> />
); );
}, [userInputRequest.responsetype, userInputRequest.publictext, responseText, handleKeyDown, setResponseText]); }, [userInputRequest.responsetype, userInputRequest.publictext, responseText, handleKeyDown, setResponseText]);
console.log("mem1");
const optionalCheckbox = useMemo(() => { const optionalCheckbox = useMemo(() => {
if (userInputRequest.checkboxmsg == "") { if (userInputRequest.checkboxmsg == "") {
@ -99,22 +106,25 @@ const UserInputModal = (userInputRequest: UserInputRequest) => {
} }
return ( return (
<div className="userinput-checkbox-container"> <div className="userinput-checkbox-container">
<input <div className="userinput-checkbox-row">
type="checkbox" <input
id={`uicheckbox-${userInputRequest.requestid}`} type="checkbox"
className="userinput-checkbox" id={`uicheckbox-${userInputRequest.requestid}`}
ref={checkboxRef} className="userinput-checkbox"
/> ref={checkboxRef}
<label htmlFor={`uicheckbox-${userInputRequest.requestid}}`}>{userInputRequest.checkboxmsg}</label> />
<label htmlFor={`uicheckbox-${userInputRequest.requestid}}`}>{userInputRequest.checkboxmsg}</label>
</div>
</div> </div>
); );
}, []); }, []);
console.log("mem2");
useEffect(() => { useEffect(() => {
let timeout: ReturnType<typeof setTimeout>; let timeout: ReturnType<typeof setTimeout>;
if (countdown <= 0) { if (countdown <= 0) {
timeout = setTimeout(() => { timeout = setTimeout(() => {
handleSendCancel(); handleSendErrResponse();
}, 300); }, 300);
} else { } else {
timeout = setTimeout(() => { timeout = setTimeout(() => {
@ -123,9 +133,28 @@ const UserInputModal = (userInputRequest: UserInputRequest) => {
} }
return () => clearTimeout(timeout); return () => clearTimeout(timeout);
}, [countdown]); }, [countdown]);
console.log("count");
const handleNegativeResponse = useCallback(() => {
switch (userInputRequest.responsetype) {
case "text":
handleSendErrResponse();
break;
case "confirm":
handleSendConfirm(false);
break;
}
}, [userInputRequest.responsetype, handleSendErrResponse, handleSendConfirm]);
console.log("before end");
return ( return (
<Modal onOk={() => handleSubmit()} onCancel={() => handleSendCancel()} onClose={() => handleSendCancel()}> <Modal
onOk={() => handleSubmit()}
onCancel={() => handleNegativeResponse()}
onClose={() => handleSendErrResponse()}
okLabel={userInputRequest.oklabel}
cancelLabel={userInputRequest.cancellabel}
>
<div className="userinput-header">{userInputRequest.title + ` (${countdown}s)`}</div> <div className="userinput-header">{userInputRequest.title + ` (${countdown}s)`}</div>
<div className="userinput-body"> <div className="userinput-body">
{queryText} {queryText}

View File

@ -28,7 +28,7 @@ class RpcApiType {
} }
// command "connconnect" [call] // command "connconnect" [call]
ConnConnectCommand(client: WshClient, data: string, opts?: RpcOpts): Promise<void> { ConnConnectCommand(client: WshClient, data: ConnRequest, opts?: RpcOpts): Promise<void> {
return client.wshRpcCall("connconnect", data, opts); return client.wshRpcCall("connconnect", data, opts);
} }

View File

@ -5,7 +5,7 @@
declare global { declare global {
// telemetry.ActivityDisplayType // wshrpc.ActivityDisplayType
type ActivityDisplayType = { type ActivityDisplayType = {
width: number; width: number;
height: number; height: number;
@ -13,7 +13,7 @@ declare global {
internal?: boolean; internal?: boolean;
}; };
// telemetry.ActivityUpdate // wshrpc.ActivityUpdate
type ActivityUpdate = { type ActivityUpdate = {
fgminutes?: number; fgminutes?: number;
activeminutes?: number; activeminutes?: number;
@ -275,9 +275,36 @@ declare global {
err: string; err: string;
}; };
// wshrpc.ConnKeywords
type ConnKeywords = {
wshenabled?: boolean;
askbeforewshinstall?: boolean;
"ssh:user"?: string;
"ssh:hostname"?: string;
"ssh:port"?: string;
"ssh:identityfile"?: string[];
"ssh:batchmode"?: boolean;
"ssh:pubkeyauthentication"?: boolean;
"ssh:passwordauthentication"?: boolean;
"ssh:kbdinteractiveauthentication"?: boolean;
"ssh:preferredauthentications"?: string[];
"ssh:addkeystoagent"?: boolean;
"ssh:identityagent"?: string;
"ssh:proxyjump"?: string[];
"ssh:userknownhostsfile"?: string[];
"ssh:globalknownhostsfile"?: string[];
};
// wshrpc.ConnRequest
type ConnRequest = {
host: string;
keywords?: ConnKeywords;
};
// wshrpc.ConnStatus // wshrpc.ConnStatus
type ConnStatus = { type ConnStatus = {
status: string; status: string;
wshenabled: boolean;
connection: string; connection: string;
connected: boolean; connected: boolean;
hasconnected: boolean; hasconnected: boolean;
@ -337,9 +364,11 @@ declare global {
type FullConfigType = { type FullConfigType = {
settings: SettingsType; settings: SettingsType;
mimetypes: {[key: string]: MimeTypeConfigType}; mimetypes: {[key: string]: MimeTypeConfigType};
defaultwidgets: {[key: string]: WidgetConfigType};
widgets: {[key: string]: WidgetConfigType}; widgets: {[key: string]: WidgetConfigType};
presets: {[key: string]: MetaType}; presets: {[key: string]: MetaType};
termthemes: {[key: string]: TermThemeType}; termthemes: {[key: string]: TermThemeType};
connections: {[key: string]: ConnKeywords};
configerrors: ConfigError[]; configerrors: ConfigError[];
}; };
@ -608,6 +637,7 @@ declare global {
"telemetry:enabled"?: boolean; "telemetry:enabled"?: boolean;
"conn:*"?: boolean; "conn:*"?: boolean;
"conn:askbeforewshinstall"?: boolean; "conn:askbeforewshinstall"?: boolean;
"conn:wshenabled"?: boolean;
}; };
// waveobj.StickerClickOptsType // waveobj.StickerClickOptsType
@ -701,6 +731,8 @@ declare global {
timeoutms: number; timeoutms: number;
checkboxmsg: string; checkboxmsg: string;
publictext: boolean; publictext: boolean;
oklabel?: string;
cancellabel?: string;
}; };
// userinput.UserInputResponse // userinput.UserInputResponse

View File

@ -296,7 +296,7 @@ func (bc *BlockController) DoRunShellCommand(rc *RunShellOpts, blockMeta waveobj
if err != nil { if err != nil {
return err return err
} }
conn := conncontroller.GetConn(credentialCtx, opts, false) conn := conncontroller.GetConn(credentialCtx, opts, false, &wshrpc.ConnKeywords{})
connStatus := conn.DeriveConnStatus() connStatus := conn.DeriveConnStatus()
if connStatus.Status != conncontroller.Status_Connected { if connStatus.Status != conncontroller.Status_Connected {
return fmt.Errorf("not connected, cannot start shellproc") return fmt.Errorf("not connected, cannot start shellproc")
@ -538,7 +538,7 @@ func CheckConnStatus(blockId string) error {
if err != nil { if err != nil {
return fmt.Errorf("error parsing connection name: %w", err) return fmt.Errorf("error parsing connection name: %w", err)
} }
conn := conncontroller.GetConn(context.Background(), opts, false) conn := conncontroller.GetConn(context.Background(), opts, false, &wshrpc.ConnKeywords{})
connStatus := conn.DeriveConnStatus() connStatus := conn.DeriveConnStatus()
if connStatus.Status != conncontroller.Status_Connected { if connStatus.Status != conncontroller.Status_Connected {
return fmt.Errorf("not connected: %s", connStatus.Status) return fmt.Errorf("not connected: %s", connStatus.Status)

View File

@ -52,6 +52,7 @@ var activeConnCounter = &atomic.Int32{}
type SSHConn struct { type SSHConn struct {
Lock *sync.Mutex Lock *sync.Mutex
Status string Status string
WshEnabled *atomic.Bool
Opts *remote.SSHOpts Opts *remote.SSHOpts
Client *ssh.Client Client *ssh.Client
SockName string SockName string
@ -290,6 +291,12 @@ type WshInstallOpts struct {
NoUserPrompt bool NoUserPrompt bool
} }
type WshInstallSkipError struct{}
func (wise *WshInstallSkipError) Error() string {
return "skipping wsh installation"
}
func (conn *SSHConn) CheckAndInstallWsh(ctx context.Context, clientDisplayName string, opts *WshInstallOpts) error { func (conn *SSHConn) CheckAndInstallWsh(ctx context.Context, clientDisplayName string, opts *WshInstallOpts) error {
if opts == nil { if opts == nil {
opts = &WshInstallOpts{} opts = &WshInstallOpts{}
@ -325,12 +332,23 @@ func (conn *SSHConn) CheckAndInstallWsh(ctx context.Context, clientDisplayName s
QueryText: queryText, QueryText: queryText,
Title: title, Title: title,
Markdown: true, Markdown: true,
CheckBoxMsg: "Don't show me this again", CheckBoxMsg: "Automatically install for all connections",
OkLabel: "Install wsh",
CancelLabel: "No wsh",
} }
response, err := userinput.GetUserInput(ctx, request) response, err := userinput.GetUserInput(ctx, request)
if err != nil || !response.Confirm { if err != nil {
return err return err
} }
if !response.Confirm {
meta := make(map[string]any)
meta["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 { if response.CheckboxStat {
meta := waveobj.MetaMapType{ meta := waveobj.MetaMapType{
wconfig.ConfigKey_ConnAskBeforeWshInstall: false, wconfig.ConfigKey_ConnAskBeforeWshInstall: false,
@ -371,7 +389,7 @@ func (conn *SSHConn) Reconnect(ctx context.Context) error {
if err != nil { if err != nil {
return err return err
} }
return conn.Connect(ctx) return conn.Connect(ctx, &wshrpc.ConnKeywords{})
} }
func (conn *SSHConn) WaitForConnect(ctx context.Context) error { func (conn *SSHConn) WaitForConnect(ctx context.Context) error {
@ -399,7 +417,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) error { func (conn *SSHConn) Connect(ctx context.Context, connFlags *wshrpc.ConnKeywords) error {
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 {
@ -415,13 +433,13 @@ func (conn *SSHConn) Connect(ctx context.Context) error {
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.FireConnChangeEvent() conn.FireConnChangeEvent()
err := conn.connectInternal(ctx) err := conn.connectInternal(ctx, connFlags)
conn.WithLock(func() { conn.WithLock(func() {
if err != nil { if err != nil {
conn.Status = Status_Error conn.Status = Status_Error
conn.Error = err.Error() conn.Error = err.Error()
conn.close_nolock() conn.close_nolock()
telemetry.GoUpdateActivityWrap(telemetry.ActivityUpdate{ telemetry.GoUpdateActivityWrap(wshrpc.ActivityUpdate{
Conn: map[string]int{"ssh:connecterror": 1}, Conn: map[string]int{"ssh:connecterror": 1},
}, "ssh-connconnect") }, "ssh-connconnect")
} else { } else {
@ -430,7 +448,7 @@ func (conn *SSHConn) Connect(ctx context.Context) error {
if conn.ActiveConnNum == 0 { if conn.ActiveConnNum == 0 {
conn.ActiveConnNum = int(activeConnCounter.Add(1)) conn.ActiveConnNum = int(activeConnCounter.Add(1))
} }
telemetry.GoUpdateActivityWrap(telemetry.ActivityUpdate{ telemetry.GoUpdateActivityWrap(wshrpc.ActivityUpdate{
Conn: map[string]int{"ssh:connect": 1}, Conn: map[string]int{"ssh:connect": 1},
}, "ssh-connconnect") }, "ssh-connconnect")
} }
@ -445,8 +463,8 @@ func (conn *SSHConn) WithLock(fn func()) {
fn() fn()
} }
func (conn *SSHConn) connectInternal(ctx context.Context) error { func (conn *SSHConn) connectInternal(ctx context.Context, connFlags *wshrpc.ConnKeywords) error {
client, _, err := remote.ConnectToClient(ctx, conn.Opts, nil, 0) client, _, err := remote.ConnectToClient(ctx, conn.Opts, nil, 0, connFlags)
if err != nil { if err != nil {
log.Printf("error: failed to connect to client %s: %s\n", conn.GetName(), err) log.Printf("error: failed to connect to client %s: %s\n", conn.GetName(), err)
return err return err
@ -462,15 +480,40 @@ func (conn *SSHConn) connectInternal(ctx context.Context) error {
return err return err
} }
config := wconfig.ReadFullConfig() config := wconfig.ReadFullConfig()
installErr := conn.CheckAndInstallWsh(ctx, clientDisplayName, &WshInstallOpts{NoUserPrompt: !config.Settings.ConnAskBeforeWshInstall}) enableWsh := config.Settings.ConnWshEnabled
if installErr != nil { askBeforeInstall := config.Settings.ConnAskBeforeWshInstall
log.Printf("error: unable to install wsh shell extensions for %s: %v\n", conn.GetName(), err) connSettings, ok := config.Connections[conn.GetName()]
return fmt.Errorf("conncontroller %s wsh install error: %v", conn.GetName(), installErr) if ok {
if connSettings.WshEnabled != nil {
enableWsh = *connSettings.WshEnabled
}
if connSettings.AskBeforeWshInstall != nil {
askBeforeInstall = *connSettings.AskBeforeWshInstall
}
} }
csErr := conn.StartConnServer() if enableWsh {
if csErr != nil { installErr := conn.CheckAndInstallWsh(ctx, clientDisplayName, &WshInstallOpts{NoUserPrompt: !askBeforeInstall})
log.Printf("error: unable to start conn server for %s: %v\n", conn.GetName(), csErr) if errors.Is(installErr, &WshInstallSkipError{}) {
return fmt.Errorf("conncontroller %s start wsh connserver error: %v", conn.GetName(), csErr) // 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)
return fmt.Errorf("conncontroller %s wsh install error: %v", conn.GetName(), installErr)
} else {
conn.WshEnabled.Store(true)
}
if conn.WshEnabled.Load() {
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)
}
}
} else {
conn.WshEnabled.Store(false)
} }
conn.HasWaiter.Store(true) conn.HasWaiter.Store(true)
go conn.waitForDisconnect() go conn.waitForDisconnect()
@ -504,16 +547,16 @@ func getConnInternal(opts *remote.SSHOpts) *SSHConn {
defer globalLock.Unlock() defer globalLock.Unlock()
rtn := clientControllerMap[*opts] rtn := clientControllerMap[*opts]
if rtn == nil { if rtn == nil {
rtn = &SSHConn{Lock: &sync.Mutex{}, Status: Status_Init, Opts: opts, HasWaiter: &atomic.Bool{}} rtn = &SSHConn{Lock: &sync.Mutex{}, Status: Status_Init, WshEnabled: &atomic.Bool{}, Opts: opts, HasWaiter: &atomic.Bool{}}
clientControllerMap[*opts] = rtn clientControllerMap[*opts] = rtn
} }
return rtn return rtn
} }
func GetConn(ctx context.Context, opts *remote.SSHOpts, shouldConnect bool) *SSHConn { func GetConn(ctx context.Context, opts *remote.SSHOpts, shouldConnect bool, connFlags *wshrpc.ConnKeywords) *SSHConn {
conn := getConnInternal(opts) conn := getConnInternal(opts)
if conn.Client == nil && shouldConnect { if conn.Client == nil && shouldConnect {
conn.Connect(ctx) conn.Connect(ctx, connFlags)
} }
return conn return conn
} }
@ -527,7 +570,7 @@ func EnsureConnection(ctx context.Context, connName string) error {
if err != nil { if err != nil {
return fmt.Errorf("error parsing connection name: %w", err) return fmt.Errorf("error parsing connection name: %w", err)
} }
conn := GetConn(ctx, connOpts, false) conn := GetConn(ctx, connOpts, false, &wshrpc.ConnKeywords{})
if conn == nil { if conn == nil {
return fmt.Errorf("connection not found: %s", connName) return fmt.Errorf("connection not found: %s", connName)
} }
@ -538,7 +581,7 @@ func EnsureConnection(ctx context.Context, connName string) error {
case Status_Connecting: case Status_Connecting:
return conn.WaitForConnect(ctx) return conn.WaitForConnect(ctx)
case Status_Init, Status_Disconnected: case Status_Init, Status_Disconnected:
return conn.Connect(ctx) return conn.Connect(ctx, &wshrpc.ConnKeywords{})
case Status_Error: case Status_Error:
return fmt.Errorf("connection error: %s", connStatus.Error) return fmt.Errorf("connection error: %s", connStatus.Error)
default: default:

View File

@ -17,7 +17,6 @@ import (
"os/exec" "os/exec"
"os/user" "os/user"
"path/filepath" "path/filepath"
"strconv"
"strings" "strings"
"sync" "sync"
"time" "time"
@ -28,6 +27,7 @@ import (
"github.com/wavetermdev/waveterm/pkg/userinput" "github.com/wavetermdev/waveterm/pkg/userinput"
"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"
"github.com/wavetermdev/waveterm/pkg/wshrpc"
"golang.org/x/crypto/ssh" "golang.org/x/crypto/ssh"
"golang.org/x/crypto/ssh/agent" "golang.org/x/crypto/ssh/agent"
xknownhosts "golang.org/x/crypto/ssh/knownhosts" xknownhosts "golang.org/x/crypto/ssh/knownhosts"
@ -101,13 +101,13 @@ func createDummySigner() ([]ssh.Signer, error) {
// they were successes. An error in this function prevents any other // they were successes. An error in this function prevents any other
// keys from being attempted. But if there's an error because of a dummy // keys from being attempted. But if there's an error because of a dummy
// file, the library can still try again with a new key. // file, the library can still try again with a new key.
func createPublicKeyCallback(connCtx context.Context, sshKeywords *SshKeywords, authSockSignersExt []ssh.Signer, agentClient agent.ExtendedAgent, debugInfo *ConnectionDebugInfo) func() ([]ssh.Signer, error) { func createPublicKeyCallback(connCtx context.Context, sshKeywords *wshrpc.ConnKeywords, authSockSignersExt []ssh.Signer, agentClient agent.ExtendedAgent, debugInfo *ConnectionDebugInfo) func() ([]ssh.Signer, error) {
var identityFiles []string var identityFiles []string
existingKeys := make(map[string][]byte) existingKeys := make(map[string][]byte)
// checking the file early prevents us from needing to send a // checking the file early prevents us from needing to send a
// dummy signer if there's a problem with the signer // dummy signer if there's a problem with the signer
for _, identityFile := range sshKeywords.IdentityFile { for _, identityFile := range sshKeywords.SshIdentityFile {
filePath, err := wavebase.ExpandHomeDir(identityFile) filePath, err := wavebase.ExpandHomeDir(identityFile)
if err != nil { if err != nil {
continue continue
@ -151,7 +151,7 @@ func createPublicKeyCallback(connCtx context.Context, sshKeywords *SshKeywords,
if err == nil { if err == nil {
signer, err := ssh.NewSignerFromKey(unencryptedPrivateKey) signer, err := ssh.NewSignerFromKey(unencryptedPrivateKey)
if err == nil { if err == nil {
if sshKeywords.AddKeysToAgent && agentClient != nil { if sshKeywords.SshAddKeysToAgent && agentClient != nil {
agentClient.Add(agent.AddedKey{ agentClient.Add(agent.AddedKey{
PrivateKey: unencryptedPrivateKey, PrivateKey: unencryptedPrivateKey,
}) })
@ -165,7 +165,7 @@ func createPublicKeyCallback(connCtx context.Context, sshKeywords *SshKeywords,
} }
// batch mode deactivates user input // batch mode deactivates user input
if sshKeywords.BatchMode { if sshKeywords.SshBatchMode {
// skip this key and try with the next // skip this key and try with the next
return createDummySigner() return createDummySigner()
} }
@ -194,7 +194,7 @@ func createPublicKeyCallback(connCtx context.Context, sshKeywords *SshKeywords,
// skip this key and try with the next // skip this key and try with the next
return createDummySigner() return createDummySigner()
} }
if sshKeywords.AddKeysToAgent && agentClient != nil { if sshKeywords.SshAddKeysToAgent && agentClient != nil {
agentClient.Add(agent.AddedKey{ agentClient.Add(agent.AddedKey{
PrivateKey: unencryptedPrivateKey, PrivateKey: unencryptedPrivateKey,
}) })
@ -333,7 +333,14 @@ func createUnknownKeyVerifier(knownHostsFile string, hostname string, remote str
return func() (*userinput.UserInputResponse, error) { return func() (*userinput.UserInputResponse, error) {
ctx, cancelFn := context.WithTimeout(context.Background(), 60*time.Second) ctx, cancelFn := context.WithTimeout(context.Background(), 60*time.Second)
defer cancelFn() defer cancelFn()
return userinput.GetUserInput(ctx, request) resp, err := userinput.GetUserInput(ctx, request)
if err != nil {
return nil, err
}
if !resp.Confirm {
return nil, fmt.Errorf("user selected no")
}
return resp, nil
} }
} }
@ -357,7 +364,14 @@ func createMissingKnownHostsVerifier(knownHostsFile string, hostname string, rem
return func() (*userinput.UserInputResponse, error) { return func() (*userinput.UserInputResponse, error) {
ctx, cancelFn := context.WithTimeout(context.Background(), 60*time.Second) ctx, cancelFn := context.WithTimeout(context.Background(), 60*time.Second)
defer cancelFn() defer cancelFn()
return userinput.GetUserInput(ctx, request) resp, err := userinput.GetUserInput(ctx, request)
if err != nil {
return nil, err
}
if !resp.Confirm {
return nil, fmt.Errorf("user selected no")
}
return resp, nil
} }
} }
@ -370,9 +384,9 @@ func lineContainsMatch(line []byte, matches [][]byte) bool {
return false return false
} }
func createHostKeyCallback(sshKeywords *SshKeywords) (ssh.HostKeyCallback, HostKeyAlgorithms, error) { func createHostKeyCallback(sshKeywords *wshrpc.ConnKeywords) (ssh.HostKeyCallback, HostKeyAlgorithms, error) {
globalKnownHostsFiles := sshKeywords.GlobalKnownHostsFile globalKnownHostsFiles := sshKeywords.SshGlobalKnownHostsFile
userKnownHostsFiles := sshKeywords.UserKnownHostsFile userKnownHostsFiles := sshKeywords.SshUserKnownHostsFile
osUser, err := user.Current() osUser, err := user.Current()
if err != nil { if err != nil {
@ -536,12 +550,12 @@ func createHostKeyCallback(sshKeywords *SshKeywords) (ssh.HostKeyCallback, HostK
return waveHostKeyCallback, hostKeyAlgorithms, nil return waveHostKeyCallback, hostKeyAlgorithms, nil
} }
func createClientConfig(connCtx context.Context, sshKeywords *SshKeywords, debugInfo *ConnectionDebugInfo) (*ssh.ClientConfig, error) { func createClientConfig(connCtx context.Context, sshKeywords *wshrpc.ConnKeywords, debugInfo *ConnectionDebugInfo) (*ssh.ClientConfig, error) {
remoteName := sshKeywords.User + "@" + xknownhosts.Normalize(sshKeywords.HostName+":"+sshKeywords.Port) remoteName := sshKeywords.SshUser + "@" + xknownhosts.Normalize(sshKeywords.SshHostName+":"+sshKeywords.SshPort)
var authSockSigners []ssh.Signer var authSockSigners []ssh.Signer
var agentClient agent.ExtendedAgent var agentClient agent.ExtendedAgent
conn, err := net.Dial("unix", sshKeywords.IdentityAgent) conn, err := net.Dial("unix", sshKeywords.SshIdentityAgent)
if err != nil { if err != nil {
log.Printf("Failed to open Identity Agent Socket: %v", err) log.Printf("Failed to open Identity Agent Socket: %v", err)
} else { } else {
@ -555,20 +569,20 @@ func createClientConfig(connCtx context.Context, sshKeywords *SshKeywords, debug
// exclude gssapi-with-mic and hostbased until implemented // exclude gssapi-with-mic and hostbased until implemented
authMethodMap := map[string]ssh.AuthMethod{ authMethodMap := map[string]ssh.AuthMethod{
"publickey": ssh.RetryableAuthMethod(publicKeyCallback, len(sshKeywords.IdentityFile)+len(authSockSigners)), "publickey": ssh.RetryableAuthMethod(publicKeyCallback, len(sshKeywords.SshIdentityFile)+len(authSockSigners)),
"keyboard-interactive": ssh.RetryableAuthMethod(keyboardInteractive, 1), "keyboard-interactive": ssh.RetryableAuthMethod(keyboardInteractive, 1),
"password": ssh.RetryableAuthMethod(passwordCallback, 1), "password": ssh.RetryableAuthMethod(passwordCallback, 1),
} }
// note: batch mode turns off interactive input // note: batch mode turns off interactive input
authMethodActiveMap := map[string]bool{ authMethodActiveMap := map[string]bool{
"publickey": sshKeywords.PubkeyAuthentication, "publickey": sshKeywords.SshPubkeyAuthentication,
"keyboard-interactive": sshKeywords.KbdInteractiveAuthentication && !sshKeywords.BatchMode, "keyboard-interactive": sshKeywords.SshKbdInteractiveAuthentication && !sshKeywords.SshBatchMode,
"password": sshKeywords.PasswordAuthentication && !sshKeywords.BatchMode, "password": sshKeywords.SshPasswordAuthentication && !sshKeywords.SshBatchMode,
} }
var authMethods []ssh.AuthMethod var authMethods []ssh.AuthMethod
for _, authMethodName := range sshKeywords.PreferredAuthentications { for _, authMethodName := range sshKeywords.SshPreferredAuthentications {
authMethodActive, ok := authMethodActiveMap[authMethodName] authMethodActive, ok := authMethodActiveMap[authMethodName]
if !ok || !authMethodActive { if !ok || !authMethodActive {
continue continue
@ -585,9 +599,9 @@ func createClientConfig(connCtx context.Context, sshKeywords *SshKeywords, debug
return nil, err return nil, err
} }
networkAddr := sshKeywords.HostName + ":" + sshKeywords.Port networkAddr := sshKeywords.SshHostName + ":" + sshKeywords.SshPort
return &ssh.ClientConfig{ return &ssh.ClientConfig{
User: sshKeywords.User, User: sshKeywords.SshUser,
Auth: authMethods, Auth: authMethods,
HostKeyCallback: hostKeyCallback, HostKeyCallback: hostKeyCallback,
HostKeyAlgorithms: hostKeyAlgorithms(networkAddr), HostKeyAlgorithms: hostKeyAlgorithms(networkAddr),
@ -616,7 +630,7 @@ func connectInternal(ctx context.Context, networkAddr string, clientConfig *ssh.
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) (*ssh.Client, int32, error) { func ConnectToClient(connCtx context.Context, opts *SSHOpts, currentClient *ssh.Client, jumpNum int32, connFlags *wshrpc.ConnKeywords) (*ssh.Client, int32, error) {
debugInfo := &ConnectionDebugInfo{ debugInfo := &ConnectionDebugInfo{
CurrentClient: currentClient, CurrentClient: currentClient,
NextOpts: opts, NextOpts: opts,
@ -631,12 +645,16 @@ func ConnectToClient(connCtx context.Context, opts *SSHOpts, currentClient *ssh.
return nil, debugInfo.JumpNum, ConnectionError{ConnectionDebugInfo: debugInfo, Err: err} return nil, debugInfo.JumpNum, ConnectionError{ConnectionDebugInfo: debugInfo, Err: err}
} }
sshKeywords, err := combineSshKeywords(opts, sshConfigKeywords) connFlags.SshUser = opts.SSHUser
connFlags.SshHostName = opts.SSHHost
connFlags.SshPort = fmt.Sprintf("%d", opts.SSHPort)
sshKeywords, err := combineSshKeywords(connFlags, sshConfigKeywords)
if err != nil { if err != nil {
return nil, debugInfo.JumpNum, ConnectionError{ConnectionDebugInfo: debugInfo, Err: err} return nil, debugInfo.JumpNum, ConnectionError{ConnectionDebugInfo: debugInfo, Err: err}
} }
for _, proxyName := range sshKeywords.ProxyJump { for _, proxyName := range sshKeywords.SshProxyJump {
proxyOpts, err := ParseOpts(proxyName) proxyOpts, err := ParseOpts(proxyName)
if err != nil { if err != nil {
return nil, debugInfo.JumpNum, ConnectionError{ConnectionDebugInfo: debugInfo, Err: err} return nil, debugInfo.JumpNum, ConnectionError{ConnectionDebugInfo: debugInfo, Err: err}
@ -647,7 +665,8 @@ func ConnectToClient(connCtx context.Context, opts *SSHOpts, currentClient *ssh.
jumpNum += 1 jumpNum += 1
} }
debugInfo.CurrentClient, jumpNum, err = ConnectToClient(connCtx, proxyOpts, debugInfo.CurrentClient, jumpNum) // do not apply supplied keywords to proxies - ssh config must be used for that
debugInfo.CurrentClient, jumpNum, err = ConnectToClient(connCtx, proxyOpts, debugInfo.CurrentClient, jumpNum, &wshrpc.ConnKeywords{})
if err != nil { if err != nil {
// do not add a context on a recursive call // do not add a context on a recursive call
// (this can cause a recursive nested context that's arbitrarily deep) // (this can cause a recursive nested context that's arbitrarily deep)
@ -658,7 +677,7 @@ func ConnectToClient(connCtx context.Context, opts *SSHOpts, currentClient *ssh.
if err != nil { if err != nil {
return nil, debugInfo.JumpNum, ConnectionError{ConnectionDebugInfo: debugInfo, Err: err} return nil, debugInfo.JumpNum, ConnectionError{ConnectionDebugInfo: debugInfo, Err: err}
} }
networkAddr := sshKeywords.HostName + ":" + sshKeywords.Port networkAddr := sshKeywords.SshHostName + ":" + sshKeywords.SshPort
client, err := connectInternal(connCtx, networkAddr, clientConfig, debugInfo.CurrentClient) client, err := connectInternal(connCtx, networkAddr, clientConfig, debugInfo.CurrentClient)
if err != nil { if err != nil {
return client, debugInfo.JumpNum, ConnectionError{ConnectionDebugInfo: debugInfo, Err: err} return client, debugInfo.JumpNum, ConnectionError{ConnectionDebugInfo: debugInfo, Err: err}
@ -666,68 +685,51 @@ func ConnectToClient(connCtx context.Context, opts *SSHOpts, currentClient *ssh.
return client, debugInfo.JumpNum, nil return client, debugInfo.JumpNum, nil
} }
type SshKeywords struct { func combineSshKeywords(userProvidedOpts *wshrpc.ConnKeywords, configKeywords *wshrpc.ConnKeywords) (*wshrpc.ConnKeywords, error) {
User string sshKeywords := &wshrpc.ConnKeywords{}
HostName string
Port string
IdentityFile []string
BatchMode bool
PubkeyAuthentication bool
PasswordAuthentication bool
KbdInteractiveAuthentication bool
PreferredAuthentications []string
AddKeysToAgent bool
IdentityAgent string
ProxyJump []string
UserKnownHostsFile []string
GlobalKnownHostsFile []string
}
func combineSshKeywords(opts *SSHOpts, configKeywords *SshKeywords) (*SshKeywords, error) { if userProvidedOpts.SshUser != "" {
sshKeywords := &SshKeywords{} sshKeywords.SshUser = userProvidedOpts.SshUser
} else if configKeywords.SshUser != "" {
if opts.SSHUser != "" { sshKeywords.SshUser = configKeywords.SshUser
sshKeywords.User = opts.SSHUser
} else if configKeywords.User != "" {
sshKeywords.User = configKeywords.User
} else { } else {
user, err := user.Current() user, err := user.Current()
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to get user for ssh: %+v", err) return nil, fmt.Errorf("failed to get user for ssh: %+v", err)
} }
sshKeywords.User = user.Username sshKeywords.SshUser = user.Username
} }
// we have to check the host value because of the weird way // we have to check the host value because of the weird way
// we store the pattern as the hostname for imported remotes // we store the pattern as the hostname for imported remotes
if configKeywords.HostName != "" { if configKeywords.SshHostName != "" {
sshKeywords.HostName = configKeywords.HostName sshKeywords.SshHostName = configKeywords.SshHostName
} else { } else {
sshKeywords.HostName = opts.SSHHost sshKeywords.SshHostName = userProvidedOpts.SshHostName
} }
if opts.SSHPort != 0 && opts.SSHPort != 22 { if userProvidedOpts.SshPort != "0" && userProvidedOpts.SshPort != "22" {
sshKeywords.Port = strconv.Itoa(opts.SSHPort) sshKeywords.SshPort = userProvidedOpts.SshPort
} else if configKeywords.Port != "" && configKeywords.Port != "22" { } else if configKeywords.SshPort != "" && configKeywords.SshPort != "22" {
sshKeywords.Port = configKeywords.Port sshKeywords.SshPort = configKeywords.SshPort
} else { } else {
sshKeywords.Port = "22" sshKeywords.SshPort = "22"
} }
sshKeywords.IdentityFile = configKeywords.IdentityFile sshKeywords.SshIdentityFile = append(userProvidedOpts.SshIdentityFile, configKeywords.SshIdentityFile...)
// these are not officially supported in the waveterm frontend but can be configured // these are not officially supported in the waveterm frontend but can be configured
// in ssh config files // in ssh config files
sshKeywords.BatchMode = configKeywords.BatchMode sshKeywords.SshBatchMode = configKeywords.SshBatchMode
sshKeywords.PubkeyAuthentication = configKeywords.PubkeyAuthentication sshKeywords.SshPubkeyAuthentication = configKeywords.SshPubkeyAuthentication
sshKeywords.PasswordAuthentication = configKeywords.PasswordAuthentication sshKeywords.SshPasswordAuthentication = configKeywords.SshPasswordAuthentication
sshKeywords.KbdInteractiveAuthentication = configKeywords.KbdInteractiveAuthentication sshKeywords.SshKbdInteractiveAuthentication = configKeywords.SshKbdInteractiveAuthentication
sshKeywords.PreferredAuthentications = configKeywords.PreferredAuthentications sshKeywords.SshPreferredAuthentications = configKeywords.SshPreferredAuthentications
sshKeywords.AddKeysToAgent = configKeywords.AddKeysToAgent sshKeywords.SshAddKeysToAgent = configKeywords.SshAddKeysToAgent
sshKeywords.IdentityAgent = configKeywords.IdentityAgent sshKeywords.SshIdentityAgent = configKeywords.SshIdentityAgent
sshKeywords.ProxyJump = configKeywords.ProxyJump sshKeywords.SshProxyJump = configKeywords.SshProxyJump
sshKeywords.UserKnownHostsFile = configKeywords.UserKnownHostsFile sshKeywords.SshUserKnownHostsFile = configKeywords.SshUserKnownHostsFile
sshKeywords.GlobalKnownHostsFile = configKeywords.GlobalKnownHostsFile sshKeywords.SshGlobalKnownHostsFile = configKeywords.SshGlobalKnownHostsFile
return sshKeywords, nil return sshKeywords, nil
} }
@ -735,59 +737,60 @@ func combineSshKeywords(opts *SSHOpts, configKeywords *SshKeywords) (*SshKeyword
// note that a `var == "yes"` will default to false // note that a `var == "yes"` will default to false
// but `var != "no"` will default to true // but `var != "no"` will default to true
// when given unexpected strings // when given unexpected strings
func findSshConfigKeywords(hostPattern string) (*SshKeywords, error) { func findSshConfigKeywords(hostPattern string) (*wshrpc.ConnKeywords, error) {
WaveSshConfigUserSettings().ReloadConfigs() WaveSshConfigUserSettings().ReloadConfigs()
sshKeywords := &SshKeywords{} sshKeywords := &wshrpc.ConnKeywords{}
var err error var err error
//config := wconfig.ReadFullConfig()
userRaw, err := WaveSshConfigUserSettings().GetStrict(hostPattern, "User") userRaw, err := WaveSshConfigUserSettings().GetStrict(hostPattern, "User")
if err != nil { if err != nil {
return nil, err return nil, err
} }
sshKeywords.User = trimquotes.TryTrimQuotes(userRaw) sshKeywords.SshUser = trimquotes.TryTrimQuotes(userRaw)
hostNameRaw, err := WaveSshConfigUserSettings().GetStrict(hostPattern, "HostName") hostNameRaw, err := WaveSshConfigUserSettings().GetStrict(hostPattern, "HostName")
if err != nil { if err != nil {
return nil, err return nil, err
} }
sshKeywords.HostName = trimquotes.TryTrimQuotes(hostNameRaw) sshKeywords.SshHostName = trimquotes.TryTrimQuotes(hostNameRaw)
portRaw, err := WaveSshConfigUserSettings().GetStrict(hostPattern, "Port") portRaw, err := WaveSshConfigUserSettings().GetStrict(hostPattern, "Port")
if err != nil { if err != nil {
return nil, err return nil, err
} }
sshKeywords.Port = trimquotes.TryTrimQuotes(portRaw) sshKeywords.SshPort = trimquotes.TryTrimQuotes(portRaw)
identityFileRaw := WaveSshConfigUserSettings().GetAll(hostPattern, "IdentityFile") identityFileRaw := WaveSshConfigUserSettings().GetAll(hostPattern, "IdentityFile")
for i := 0; i < len(identityFileRaw); i++ { for i := 0; i < len(identityFileRaw); i++ {
identityFileRaw[i] = trimquotes.TryTrimQuotes(identityFileRaw[i]) identityFileRaw[i] = trimquotes.TryTrimQuotes(identityFileRaw[i])
} }
sshKeywords.IdentityFile = identityFileRaw sshKeywords.SshIdentityFile = identityFileRaw
batchModeRaw, err := WaveSshConfigUserSettings().GetStrict(hostPattern, "BatchMode") batchModeRaw, err := WaveSshConfigUserSettings().GetStrict(hostPattern, "BatchMode")
if err != nil { if err != nil {
return nil, err return nil, err
} }
sshKeywords.BatchMode = (strings.ToLower(trimquotes.TryTrimQuotes(batchModeRaw)) == "yes") sshKeywords.SshBatchMode = (strings.ToLower(trimquotes.TryTrimQuotes(batchModeRaw)) == "yes")
// we currently do not support host-bound or unbound but will use yes when they are selected // we currently do not support host-bound or unbound but will use yes when they are selected
pubkeyAuthenticationRaw, err := WaveSshConfigUserSettings().GetStrict(hostPattern, "PubkeyAuthentication") pubkeyAuthenticationRaw, err := WaveSshConfigUserSettings().GetStrict(hostPattern, "PubkeyAuthentication")
if err != nil { if err != nil {
return nil, err return nil, err
} }
sshKeywords.PubkeyAuthentication = (strings.ToLower(trimquotes.TryTrimQuotes(pubkeyAuthenticationRaw)) != "no") sshKeywords.SshPubkeyAuthentication = (strings.ToLower(trimquotes.TryTrimQuotes(pubkeyAuthenticationRaw)) != "no")
passwordAuthenticationRaw, err := WaveSshConfigUserSettings().GetStrict(hostPattern, "PasswordAuthentication") passwordAuthenticationRaw, err := WaveSshConfigUserSettings().GetStrict(hostPattern, "PasswordAuthentication")
if err != nil { if err != nil {
return nil, err return nil, err
} }
sshKeywords.PasswordAuthentication = (strings.ToLower(trimquotes.TryTrimQuotes(passwordAuthenticationRaw)) != "no") sshKeywords.SshPasswordAuthentication = (strings.ToLower(trimquotes.TryTrimQuotes(passwordAuthenticationRaw)) != "no")
kbdInteractiveAuthenticationRaw, err := WaveSshConfigUserSettings().GetStrict(hostPattern, "KbdInteractiveAuthentication") kbdInteractiveAuthenticationRaw, err := WaveSshConfigUserSettings().GetStrict(hostPattern, "KbdInteractiveAuthentication")
if err != nil { if err != nil {
return nil, err return nil, err
} }
sshKeywords.KbdInteractiveAuthentication = (strings.ToLower(trimquotes.TryTrimQuotes(kbdInteractiveAuthenticationRaw)) != "no") sshKeywords.SshKbdInteractiveAuthentication = (strings.ToLower(trimquotes.TryTrimQuotes(kbdInteractiveAuthenticationRaw)) != "no")
// these are parsed as a single string and must be separated // these are parsed as a single string and must be separated
// these are case sensitive in openssh so they are here too // these are case sensitive in openssh so they are here too
@ -795,12 +798,12 @@ func findSshConfigKeywords(hostPattern string) (*SshKeywords, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
sshKeywords.PreferredAuthentications = strings.Split(trimquotes.TryTrimQuotes(preferredAuthenticationsRaw), ",") sshKeywords.SshPreferredAuthentications = strings.Split(trimquotes.TryTrimQuotes(preferredAuthenticationsRaw), ",")
addKeysToAgentRaw, err := WaveSshConfigUserSettings().GetStrict(hostPattern, "AddKeysToAgent") addKeysToAgentRaw, err := WaveSshConfigUserSettings().GetStrict(hostPattern, "AddKeysToAgent")
if err != nil { if err != nil {
return nil, err return nil, err
} }
sshKeywords.AddKeysToAgent = (strings.ToLower(trimquotes.TryTrimQuotes(addKeysToAgentRaw)) == "yes") sshKeywords.SshAddKeysToAgent = (strings.ToLower(trimquotes.TryTrimQuotes(addKeysToAgentRaw)) == "yes")
identityAgentRaw, err := WaveSshConfigUserSettings().GetStrict(hostPattern, "IdentityAgent") identityAgentRaw, err := WaveSshConfigUserSettings().GetStrict(hostPattern, "IdentityAgent")
if err != nil { if err != nil {
@ -815,7 +818,7 @@ func findSshConfigKeywords(hostPattern string) (*SshKeywords, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
sshKeywords.IdentityAgent = agentPath sshKeywords.SshIdentityAgent = agentPath
} else { } else {
log.Printf("unable to find SSH_AUTH_SOCK: %v\n", err) log.Printf("unable to find SSH_AUTH_SOCK: %v\n", err)
} }
@ -824,7 +827,7 @@ func findSshConfigKeywords(hostPattern string) (*SshKeywords, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
sshKeywords.IdentityAgent = agentPath sshKeywords.SshIdentityAgent = agentPath
} }
proxyJumpRaw, err := WaveSshConfigUserSettings().GetStrict(hostPattern, "ProxyJump") proxyJumpRaw, err := WaveSshConfigUserSettings().GetStrict(hostPattern, "ProxyJump")
@ -837,12 +840,12 @@ func findSshConfigKeywords(hostPattern string) (*SshKeywords, error) {
if proxyJumpName == "" || strings.ToLower(proxyJumpName) == "none" { if proxyJumpName == "" || strings.ToLower(proxyJumpName) == "none" {
continue continue
} }
sshKeywords.ProxyJump = append(sshKeywords.ProxyJump, proxyJumpName) sshKeywords.SshProxyJump = append(sshKeywords.SshProxyJump, proxyJumpName)
} }
rawUserKnownHostsFile, _ := WaveSshConfigUserSettings().GetStrict(hostPattern, "UserKnownHostsFile") rawUserKnownHostsFile, _ := WaveSshConfigUserSettings().GetStrict(hostPattern, "UserKnownHostsFile")
sshKeywords.UserKnownHostsFile = strings.Fields(rawUserKnownHostsFile) // TODO - smarter splitting escaped spaces and quotes sshKeywords.SshUserKnownHostsFile = strings.Fields(rawUserKnownHostsFile) // TODO - smarter splitting escaped spaces and quotes
rawGlobalKnownHostsFile, _ := WaveSshConfigUserSettings().GetStrict(hostPattern, "GlobalKnownHostsFile") rawGlobalKnownHostsFile, _ := WaveSshConfigUserSettings().GetStrict(hostPattern, "GlobalKnownHostsFile")
sshKeywords.GlobalKnownHostsFile = strings.Fields(rawGlobalKnownHostsFile) // TODO - smarter splitting escaped spaces and quotes sshKeywords.SshGlobalKnownHostsFile = strings.Fields(rawGlobalKnownHostsFile) // TODO - smarter splitting escaped spaces and quotes
return sshKeywords, nil return sshKeywords, nil
} }

View File

@ -237,6 +237,47 @@ func StartWslShellProc(ctx context.Context, termSize waveobj.TermSize, cmdStr st
func StartRemoteShellProc(termSize waveobj.TermSize, cmdStr string, cmdOpts CommandOptsType, conn *conncontroller.SSHConn) (*ShellProc, error) { func StartRemoteShellProc(termSize waveobj.TermSize, cmdStr string, cmdOpts CommandOptsType, conn *conncontroller.SSHConn) (*ShellProc, error) {
client := conn.GetClient() 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 := SessionWrap{session, "", pipePty, 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 shellPath := cmdOpts.ShellPath
if shellPath == "" { if shellPath == "" {
remoteShellPath, err := remote.DetectShell(client) remoteShellPath, err := remote.DetectShell(client)

View File

@ -14,41 +14,12 @@ import (
"github.com/wavetermdev/waveterm/pkg/util/dbutil" "github.com/wavetermdev/waveterm/pkg/util/dbutil"
"github.com/wavetermdev/waveterm/pkg/wavebase" "github.com/wavetermdev/waveterm/pkg/wavebase"
"github.com/wavetermdev/waveterm/pkg/wconfig" "github.com/wavetermdev/waveterm/pkg/wconfig"
"github.com/wavetermdev/waveterm/pkg/wshrpc"
"github.com/wavetermdev/waveterm/pkg/wstore" "github.com/wavetermdev/waveterm/pkg/wstore"
) )
const MaxTzNameLen = 50 const MaxTzNameLen = 50
type ActivityDisplayType struct {
Width int `json:"width"`
Height int `json:"height"`
DPR float64 `json:"dpr"`
Internal bool `json:"internal,omitempty"`
}
type ActivityUpdate struct {
FgMinutes int `json:"fgminutes,omitempty"`
ActiveMinutes int `json:"activeminutes,omitempty"`
OpenMinutes int `json:"openminutes,omitempty"`
NumTabs int `json:"numtabs,omitempty"`
NewTab int `json:"newtab,omitempty"`
NumBlocks int `json:"numblocks,omitempty"`
NumWindows int `json:"numwindows,omitempty"`
NumSSHConn int `json:"numsshconn,omitempty"`
NumWSLConn int `json:"numwslconn,omitempty"`
NumMagnify int `json:"nummagnify,omitempty"`
NumPanics int `json:"numpanics,omitempty"`
Startup int `json:"startup,omitempty"`
Shutdown int `json:"shutdown,omitempty"`
SetTabTheme int `json:"settabtheme,omitempty"`
BuildTime string `json:"buildtime,omitempty"`
Displays []ActivityDisplayType `json:"displays,omitempty"`
Renderers map[string]int `json:"renderers,omitempty"`
Blocks map[string]int `json:"blocks,omitempty"`
WshCmds map[string]int `json:"wshcmds,omitempty"`
Conn map[string]int `json:"conn,omitempty"`
}
type ActivityType struct { type ActivityType struct {
Day string `json:"day"` Day string `json:"day"`
Uploaded bool `json:"-"` Uploaded bool `json:"-"`
@ -62,25 +33,25 @@ type ActivityType struct {
} }
type TelemetryData struct { type TelemetryData struct {
ActiveMinutes int `json:"activeminutes"` ActiveMinutes int `json:"activeminutes"`
FgMinutes int `json:"fgminutes"` FgMinutes int `json:"fgminutes"`
OpenMinutes int `json:"openminutes"` OpenMinutes int `json:"openminutes"`
NumTabs int `json:"numtabs"` NumTabs int `json:"numtabs"`
NumBlocks int `json:"numblocks,omitempty"` NumBlocks int `json:"numblocks,omitempty"`
NumWindows int `json:"numwindows,omitempty"` NumWindows int `json:"numwindows,omitempty"`
NumSSHConn int `json:"numsshconn,omitempty"` NumSSHConn int `json:"numsshconn,omitempty"`
NumWSLConn int `json:"numwslconn,omitempty"` NumWSLConn int `json:"numwslconn,omitempty"`
NumMagnify int `json:"nummagnify,omitempty"` NumMagnify int `json:"nummagnify,omitempty"`
NewTab int `json:"newtab"` NewTab int `json:"newtab"`
NumStartup int `json:"numstartup,omitempty"` NumStartup int `json:"numstartup,omitempty"`
NumShutdown int `json:"numshutdown,omitempty"` NumShutdown int `json:"numshutdown,omitempty"`
NumPanics int `json:"numpanics,omitempty"` NumPanics int `json:"numpanics,omitempty"`
SetTabTheme int `json:"settabtheme,omitempty"` SetTabTheme int `json:"settabtheme,omitempty"`
Displays []ActivityDisplayType `json:"displays,omitempty"` Displays []wshrpc.ActivityDisplayType `json:"displays,omitempty"`
Renderers map[string]int `json:"renderers,omitempty"` Renderers map[string]int `json:"renderers,omitempty"`
Blocks map[string]int `json:"blocks,omitempty"` Blocks map[string]int `json:"blocks,omitempty"`
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"`
} }
func (tdata TelemetryData) Value() (driver.Value, error) { func (tdata TelemetryData) Value() (driver.Value, error) {
@ -107,7 +78,7 @@ func AutoUpdateChannel() string {
} }
// Wraps UpdateCurrentActivity, spawns goroutine, and logs errors // Wraps UpdateCurrentActivity, spawns goroutine, and logs errors
func GoUpdateActivityWrap(update ActivityUpdate, debugStr string) { func GoUpdateActivityWrap(update wshrpc.ActivityUpdate, debugStr string) {
go func() { go func() {
defer panichandler.PanicHandlerNoTelemetry("GoUpdateActivityWrap") defer panichandler.PanicHandlerNoTelemetry("GoUpdateActivityWrap")
ctx, cancelFn := context.WithTimeout(context.Background(), 5*time.Second) ctx, cancelFn := context.WithTimeout(context.Background(), 5*time.Second)
@ -120,7 +91,7 @@ func GoUpdateActivityWrap(update ActivityUpdate, debugStr string) {
}() }()
} }
func UpdateActivity(ctx context.Context, update ActivityUpdate) error { func UpdateActivity(ctx context.Context, update wshrpc.ActivityUpdate) error {
now := time.Now() now := time.Now()
dayStr := daystr.GetCurDayStr() dayStr := daystr.GetCurDayStr()
txErr := wstore.WithTx(ctx, func(tx *wstore.TxWrap) error { txErr := wstore.WithTx(ctx, func(tx *wstore.TxWrap) error {

View File

@ -61,7 +61,7 @@ var contextRType = reflect.TypeOf((*context.Context)(nil)).Elem()
var errorRType = reflect.TypeOf((*error)(nil)).Elem() var errorRType = reflect.TypeOf((*error)(nil)).Elem()
var anyRType = reflect.TypeOf((*interface{})(nil)).Elem() var anyRType = reflect.TypeOf((*interface{})(nil)).Elem()
var metaRType = reflect.TypeOf((*waveobj.MetaMapType)(nil)).Elem() var metaRType = reflect.TypeOf((*waveobj.MetaMapType)(nil)).Elem()
var metaSettingsType = reflect.TypeOf((*wconfig.MetaSettingsType)(nil)).Elem() var metaSettingsType = reflect.TypeOf((*wshrpc.MetaSettingsType)(nil)).Elem()
var uiContextRType = reflect.TypeOf((*waveobj.UIContext)(nil)).Elem() var uiContextRType = reflect.TypeOf((*waveobj.UIContext)(nil)).Elem()
var waveObjRType = reflect.TypeOf((*waveobj.WaveObj)(nil)).Elem() var waveObjRType = reflect.TypeOf((*waveobj.WaveObj)(nil)).Elem()
var updatesRtnRType = reflect.TypeOf(waveobj.UpdatesRtnType{}) var updatesRtnRType = reflect.TypeOf(waveobj.UpdatesRtnType{})

View File

@ -25,6 +25,8 @@ type UserInputRequest struct {
TimeoutMs int `json:"timeoutms"` TimeoutMs int `json:"timeoutms"`
CheckBoxMsg string `json:"checkboxmsg"` CheckBoxMsg string `json:"checkboxmsg"`
PublicText bool `json:"publictext"` PublicText bool `json:"publictext"`
OkLabel string `json:"oklabel,omitempty"`
CancelLabel string `json:"cancellabel,omitempty"`
} }
type UserInputResponse struct { type UserInputResponse struct {

View File

@ -7,6 +7,7 @@
"autoupdate:installonquit": true, "autoupdate:installonquit": true,
"autoupdate:intervalms": 3600000, "autoupdate:intervalms": 3600000,
"conn:askbeforewshinstall": true, "conn:askbeforewshinstall": true,
"conn:wshenabled": true,
"editor:minimapenabled": true, "editor:minimapenabled": true,
"web:defaulturl": "https://github.com/wavetermdev/waveterm", "web:defaulturl": "https://github.com/wavetermdev/waveterm",
"web:defaultsearch": "https://www.google.com/search?q={query}", "web:defaultsearch": "https://www.google.com/search?q={query}",

View File

@ -71,5 +71,6 @@ const (
ConfigKey_ConnClear = "conn:*" ConfigKey_ConnClear = "conn:*"
ConfigKey_ConnAskBeforeWshInstall = "conn:askbeforewshinstall" ConfigKey_ConnAskBeforeWshInstall = "conn:askbeforewshinstall"
ConfigKey_ConnWshEnabled = "conn:wshenabled"
) )

View File

@ -19,9 +19,11 @@ import (
"github.com/wavetermdev/waveterm/pkg/wavebase" "github.com/wavetermdev/waveterm/pkg/wavebase"
"github.com/wavetermdev/waveterm/pkg/waveobj" "github.com/wavetermdev/waveterm/pkg/waveobj"
"github.com/wavetermdev/waveterm/pkg/wconfig/defaultconfig" "github.com/wavetermdev/waveterm/pkg/wconfig/defaultconfig"
"github.com/wavetermdev/waveterm/pkg/wshrpc"
) )
const SettingsFile = "settings.json" const SettingsFile = "settings.json"
const ConnectionsFile = "connections.json"
const AnySchema = ` const AnySchema = `
{ {
@ -30,25 +32,6 @@ const AnySchema = `
} }
` `
type MetaSettingsType struct {
waveobj.MetaMapType
}
func (m *MetaSettingsType) UnmarshalJSON(data []byte) error {
var metaMap waveobj.MetaMapType
decoder := json.NewDecoder(bytes.NewReader(data))
decoder.UseNumber()
if err := decoder.Decode(&metaMap); err != nil {
return err
}
*m = MetaSettingsType{MetaMapType: metaMap}
return nil
}
func (m MetaSettingsType) MarshalJSON() ([]byte, error) {
return json.Marshal(m.MetaMapType)
}
type SettingsType struct { type SettingsType struct {
AiClear bool `json:"ai:*,omitempty"` AiClear bool `json:"ai:*,omitempty"`
AiPreset string `json:"ai:preset,omitempty"` AiPreset string `json:"ai:preset,omitempty"`
@ -115,6 +98,7 @@ type SettingsType struct {
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"`
} }
type ConfigError struct { type ConfigError struct {
@ -123,12 +107,14 @@ type ConfigError struct {
} }
type FullConfigType struct { type FullConfigType struct {
Settings SettingsType `json:"settings" merge:"meta"` Settings SettingsType `json:"settings" merge:"meta"`
MimeTypes map[string]MimeTypeConfigType `json:"mimetypes"` MimeTypes map[string]MimeTypeConfigType `json:"mimetypes"`
Widgets map[string]WidgetConfigType `json:"widgets"` DefaultWidgets map[string]WidgetConfigType `json:"defaultwidgets"`
Presets map[string]waveobj.MetaMapType `json:"presets"` Widgets map[string]WidgetConfigType `json:"widgets"`
TermThemes map[string]TermThemeType `json:"termthemes"` Presets map[string]waveobj.MetaMapType `json:"presets"`
ConfigErrors []ConfigError `json:"configerrors" configfile:"-"` TermThemes map[string]TermThemeType `json:"termthemes"`
Connections map[string]wshrpc.ConnKeywords `json:"connections"`
ConfigErrors []ConfigError `json:"configerrors" configfile:"-"`
} }
func goBackWS(barr []byte, offset int) int { func goBackWS(barr []byte, offset int) int {
@ -401,12 +387,12 @@ func reindentJson(barr []byte, indentStr string) []byte {
if barr[0] != '{' && barr[0] != '[' { if barr[0] != '{' && barr[0] != '[' {
return barr return barr
} }
if bytes.Contains(barr, []byte("\n")) { if !bytes.Contains(barr, []byte("\n")) {
return barr return barr
} }
outputLines := bytes.Split(barr, []byte("\n")) outputLines := bytes.Split(barr, []byte("\n"))
for i, line := range outputLines { for i, line := range outputLines {
if i == 0 || i == len(outputLines)-1 { if i == 0 {
continue continue
} }
outputLines[i] = append([]byte(indentStr), line...) outputLines[i] = append([]byte(indentStr), line...)
@ -509,6 +495,25 @@ func SetBaseConfigValue(toMerge waveobj.MetaMapType) error {
return WriteWaveHomeConfigFile(SettingsFile, m) return WriteWaveHomeConfigFile(SettingsFile, m)
} }
func SetConnectionsConfigValue(connName string, toMerge waveobj.MetaMapType) error {
m, cerrs := ReadWaveHomeConfigFile(ConnectionsFile)
if len(cerrs) > 0 {
return fmt.Errorf("error reading config file: %v", cerrs[0])
}
if m == nil {
m = make(waveobj.MetaMapType)
}
connData := m.GetMap(connName)
if connData == nil {
connData = make(waveobj.MetaMapType)
}
for configKey, val := range toMerge {
connData[configKey] = val
}
m[connName] = connData
return WriteWaveHomeConfigFile(ConnectionsFile, m)
}
type WidgetConfigType struct { type WidgetConfigType struct {
DisplayOrder float64 `json:"display:order,omitempty"` DisplayOrder float64 `json:"display:order,omitempty"`
Icon string `json:"icon,omitempty"` Icon string `json:"icon,omitempty"`

View File

@ -16,6 +16,7 @@ import (
"github.com/wavetermdev/waveterm/pkg/telemetry" "github.com/wavetermdev/waveterm/pkg/telemetry"
"github.com/wavetermdev/waveterm/pkg/waveobj" "github.com/wavetermdev/waveterm/pkg/waveobj"
"github.com/wavetermdev/waveterm/pkg/wps" "github.com/wavetermdev/waveterm/pkg/wps"
"github.com/wavetermdev/waveterm/pkg/wshrpc"
"github.com/wavetermdev/waveterm/pkg/wstore" "github.com/wavetermdev/waveterm/pkg/wstore"
) )
@ -119,7 +120,7 @@ func CreateTab(ctx context.Context, windowId string, tabName string, activateTab
return "", fmt.Errorf("error setting active tab: %w", err) return "", fmt.Errorf("error setting active tab: %w", err)
} }
} }
telemetry.GoUpdateActivityWrap(telemetry.ActivityUpdate{NewTab: 1}, "createtab") telemetry.GoUpdateActivityWrap(wshrpc.ActivityUpdate{NewTab: 1}, "createtab")
return tab.OID, nil return tab.OID, nil
} }
@ -282,7 +283,7 @@ func CreateBlock(ctx context.Context, tabId string, blockDef *waveobj.BlockDef,
} }
tctx, cancelFn := context.WithTimeout(context.Background(), 2*time.Second) tctx, cancelFn := context.WithTimeout(context.Background(), 2*time.Second)
defer cancelFn() defer cancelFn()
telemetry.UpdateActivity(tctx, telemetry.ActivityUpdate{ telemetry.UpdateActivity(tctx, wshrpc.ActivityUpdate{
Renderers: map[string]int{blockView: 1}, Renderers: map[string]int{blockView: 1},
}) })
}() }()

View File

@ -9,14 +9,12 @@ import (
"github.com/wavetermdev/waveterm/pkg/wshutil" "github.com/wavetermdev/waveterm/pkg/wshutil"
"github.com/wavetermdev/waveterm/pkg/wshrpc" "github.com/wavetermdev/waveterm/pkg/wshrpc"
"github.com/wavetermdev/waveterm/pkg/waveobj" "github.com/wavetermdev/waveterm/pkg/waveobj"
"github.com/wavetermdev/waveterm/pkg/wconfig"
"github.com/wavetermdev/waveterm/pkg/wps" "github.com/wavetermdev/waveterm/pkg/wps"
"github.com/wavetermdev/waveterm/pkg/vdom" "github.com/wavetermdev/waveterm/pkg/vdom"
"github.com/wavetermdev/waveterm/pkg/telemetry"
) )
// command "activity", wshserver.ActivityCommand // command "activity", wshserver.ActivityCommand
func ActivityCommand(w *wshutil.WshRpc, data telemetry.ActivityUpdate, opts *wshrpc.RpcOpts) error { func ActivityCommand(w *wshutil.WshRpc, data wshrpc.ActivityUpdate, opts *wshrpc.RpcOpts) error {
_, err := sendRpcRequestCallHelper[any](w, "activity", data, opts) _, err := sendRpcRequestCallHelper[any](w, "activity", data, opts)
return err return err
} }
@ -40,7 +38,7 @@ func BlockInfoCommand(w *wshutil.WshRpc, data string, opts *wshrpc.RpcOpts) (*ws
} }
// command "connconnect", wshserver.ConnConnectCommand // command "connconnect", wshserver.ConnConnectCommand
func ConnConnectCommand(w *wshutil.WshRpc, data string, opts *wshrpc.RpcOpts) error { func ConnConnectCommand(w *wshutil.WshRpc, data wshrpc.ConnRequest, opts *wshrpc.RpcOpts) error {
_, err := sendRpcRequestCallHelper[any](w, "connconnect", data, opts) _, err := sendRpcRequestCallHelper[any](w, "connconnect", data, opts)
return err return err
} }
@ -290,7 +288,7 @@ func RouteUnannounceCommand(w *wshutil.WshRpc, opts *wshrpc.RpcOpts) error {
} }
// command "setconfig", wshserver.SetConfigCommand // command "setconfig", wshserver.SetConfigCommand
func SetConfigCommand(w *wshutil.WshRpc, data wconfig.MetaSettingsType, opts *wshrpc.RpcOpts) error { func SetConfigCommand(w *wshutil.WshRpc, data wshrpc.MetaSettingsType, opts *wshrpc.RpcOpts) error {
_, err := sendRpcRequestCallHelper[any](w, "setconfig", data, opts) _, err := sendRpcRequestCallHelper[any](w, "setconfig", data, opts)
return err return err
} }

View File

@ -5,17 +5,17 @@
package wshrpc package wshrpc
import ( import (
"bytes"
"context" "context"
"encoding/json"
"log" "log"
"os" "os"
"reflect" "reflect"
"github.com/wavetermdev/waveterm/pkg/filestore" "github.com/wavetermdev/waveterm/pkg/filestore"
"github.com/wavetermdev/waveterm/pkg/ijson" "github.com/wavetermdev/waveterm/pkg/ijson"
"github.com/wavetermdev/waveterm/pkg/telemetry"
"github.com/wavetermdev/waveterm/pkg/vdom" "github.com/wavetermdev/waveterm/pkg/vdom"
"github.com/wavetermdev/waveterm/pkg/waveobj" "github.com/wavetermdev/waveterm/pkg/waveobj"
"github.com/wavetermdev/waveterm/pkg/wconfig"
"github.com/wavetermdev/waveterm/pkg/wps" "github.com/wavetermdev/waveterm/pkg/wps"
) )
@ -133,11 +133,11 @@ type WshRpcInterface interface {
StreamWaveAiCommand(ctx context.Context, request OpenAiStreamRequest) chan RespOrErrorUnion[OpenAIPacketType] StreamWaveAiCommand(ctx context.Context, request OpenAiStreamRequest) chan RespOrErrorUnion[OpenAIPacketType]
StreamCpuDataCommand(ctx context.Context, request CpuDataRequest) chan RespOrErrorUnion[TimeSeriesData] StreamCpuDataCommand(ctx context.Context, request CpuDataRequest) chan RespOrErrorUnion[TimeSeriesData]
TestCommand(ctx context.Context, data string) error TestCommand(ctx context.Context, data string) error
SetConfigCommand(ctx context.Context, data wconfig.MetaSettingsType) error SetConfigCommand(ctx context.Context, data MetaSettingsType) error
BlockInfoCommand(ctx context.Context, blockId string) (*BlockInfoData, error) BlockInfoCommand(ctx context.Context, blockId string) (*BlockInfoData, error)
WaveInfoCommand(ctx context.Context) (*WaveInfoData, error) WaveInfoCommand(ctx context.Context) (*WaveInfoData, error)
WshActivityCommand(ct context.Context, data map[string]int) error WshActivityCommand(ct context.Context, data map[string]int) error
ActivityCommand(ctx context.Context, data telemetry.ActivityUpdate) error ActivityCommand(ctx context.Context, data ActivityUpdate) error
GetVarCommand(ctx context.Context, data CommandVarData) (*CommandVarResponseData, error) GetVarCommand(ctx context.Context, data CommandVarData) (*CommandVarResponseData, error)
SetVarCommand(ctx context.Context, data CommandVarData) error SetVarCommand(ctx context.Context, data CommandVarData) error
@ -146,7 +146,7 @@ type WshRpcInterface interface {
WslStatusCommand(ctx context.Context) ([]ConnStatus, error) WslStatusCommand(ctx context.Context) ([]ConnStatus, error)
ConnEnsureCommand(ctx context.Context, connName string) error ConnEnsureCommand(ctx context.Context, connName string) error
ConnReinstallWshCommand(ctx context.Context, connName string) error ConnReinstallWshCommand(ctx context.Context, connName string) error
ConnConnectCommand(ctx context.Context, connName string) 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)
WslListCommand(ctx context.Context) ([]string, error) WslListCommand(ctx context.Context) ([]string, error)
@ -440,6 +440,31 @@ type CommandRemoteWriteFileData struct {
CreateMode os.FileMode `json:"createmode,omitempty"` CreateMode os.FileMode `json:"createmode,omitempty"`
} }
type ConnKeywords struct {
WshEnabled *bool `json:"wshenabled,omitempty"`
AskBeforeWshInstall *bool `json:"askbeforewshinstall,omitempty"`
SshUser string `json:"ssh:user,omitempty"`
SshHostName string `json:"ssh:hostname,omitempty"`
SshPort string `json:"ssh:port,omitempty"`
SshIdentityFile []string `json:"ssh:identityfile,omitempty"`
SshBatchMode bool `json:"ssh:batchmode,omitempty"`
SshPubkeyAuthentication bool `json:"ssh:pubkeyauthentication,omitempty"`
SshPasswordAuthentication bool `json:"ssh:passwordauthentication,omitempty"`
SshKbdInteractiveAuthentication bool `json:"ssh:kbdinteractiveauthentication,omitempty"`
SshPreferredAuthentications []string `json:"ssh:preferredauthentications,omitempty"`
SshAddKeysToAgent bool `json:"ssh:addkeystoagent,omitempty"`
SshIdentityAgent string `json:"ssh:identityagent,omitempty"`
SshProxyJump []string `json:"ssh:proxyjump,omitempty"`
SshUserKnownHostsFile []string `json:"ssh:userknownhostsfile,omitempty"`
SshGlobalKnownHostsFile []string `json:"ssh:globalknownhostsfile,omitempty"`
}
type ConnRequest struct {
Host string `json:"host"`
Keywords ConnKeywords `json:"keywords,omitempty"`
}
const ( const (
TimeSeries_Cpu = "cpu" TimeSeries_Cpu = "cpu"
) )
@ -449,8 +474,28 @@ type TimeSeriesData struct {
Values map[string]float64 `json:"values"` Values map[string]float64 `json:"values"`
} }
type MetaSettingsType struct {
waveobj.MetaMapType
}
func (m *MetaSettingsType) UnmarshalJSON(data []byte) error {
var metaMap waveobj.MetaMapType
decoder := json.NewDecoder(bytes.NewReader(data))
decoder.UseNumber()
if err := decoder.Decode(&metaMap); err != nil {
return err
}
*m = MetaSettingsType{MetaMapType: metaMap}
return nil
}
func (m MetaSettingsType) MarshalJSON() ([]byte, error) {
return json.Marshal(m.MetaMapType)
}
type ConnStatus struct { type ConnStatus struct {
Status string `json:"status"` Status string `json:"status"`
WshEnabled bool `json:"wshenabled"`
Connection string `json:"connection"` Connection string `json:"connection"`
Connected bool `json:"connected"` Connected bool `json:"connected"`
HasConnected bool `json:"hasconnected"` // true if it has *ever* connected successfully HasConnected bool `json:"hasconnected"` // true if it has *ever* connected successfully
@ -522,3 +567,33 @@ type CommandVarResponseData struct {
Val string `json:"val"` Val string `json:"val"`
Exists bool `json:"exists"` Exists bool `json:"exists"`
} }
type ActivityDisplayType struct {
Width int `json:"width"`
Height int `json:"height"`
DPR float64 `json:"dpr"`
Internal bool `json:"internal,omitempty"`
}
type ActivityUpdate struct {
FgMinutes int `json:"fgminutes,omitempty"`
ActiveMinutes int `json:"activeminutes,omitempty"`
OpenMinutes int `json:"openminutes,omitempty"`
NumTabs int `json:"numtabs,omitempty"`
NewTab int `json:"newtab,omitempty"`
NumBlocks int `json:"numblocks,omitempty"`
NumWindows int `json:"numwindows,omitempty"`
NumSSHConn int `json:"numsshconn,omitempty"`
NumWSLConn int `json:"numwslconn,omitempty"`
NumMagnify int `json:"nummagnify,omitempty"`
NumPanics int `json:"numpanics,omitempty"`
Startup int `json:"startup,omitempty"`
Shutdown int `json:"shutdown,omitempty"`
SetTabTheme int `json:"settabtheme,omitempty"`
BuildTime string `json:"buildtime,omitempty"`
Displays []ActivityDisplayType `json:"displays,omitempty"`
Renderers map[string]int `json:"renderers,omitempty"`
Blocks map[string]int `json:"blocks,omitempty"`
WshCmds map[string]int `json:"wshcmds,omitempty"`
Conn map[string]int `json:"conn,omitempty"`
}

View File

@ -587,7 +587,7 @@ func (ws *WshServer) EventReadHistoryCommand(ctx context.Context, data wshrpc.Co
return events, nil return events, nil
} }
func (ws *WshServer) SetConfigCommand(ctx context.Context, data wconfig.MetaSettingsType) error { func (ws *WshServer) SetConfigCommand(ctx context.Context, data wshrpc.MetaSettingsType) error {
log.Printf("SETCONFIG: %v\n", data) log.Printf("SETCONFIG: %v\n", data)
return wconfig.SetBaseConfigValue(data.MetaMapType) return wconfig.SetBaseConfigValue(data.MetaMapType)
} }
@ -623,14 +623,15 @@ func (ws *WshServer) ConnDisconnectCommand(ctx context.Context, connName string)
if err != nil { if err != nil {
return fmt.Errorf("error parsing connection name: %w", err) return fmt.Errorf("error parsing connection name: %w", err)
} }
conn := conncontroller.GetConn(ctx, connOpts, false) conn := conncontroller.GetConn(ctx, connOpts, false, &wshrpc.ConnKeywords{})
if conn == nil { if conn == nil {
return fmt.Errorf("connection not found: %s", connName) return fmt.Errorf("connection not found: %s", connName)
} }
return conn.Close() return conn.Close()
} }
func (ws *WshServer) ConnConnectCommand(ctx context.Context, connName string) error { func (ws *WshServer) ConnConnectCommand(ctx context.Context, connRequest wshrpc.ConnRequest) error {
connName := connRequest.Host
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)
@ -643,11 +644,11 @@ func (ws *WshServer) ConnConnectCommand(ctx context.Context, connName string) er
if err != nil { if err != nil {
return fmt.Errorf("error parsing connection name: %w", err) return fmt.Errorf("error parsing connection name: %w", err)
} }
conn := conncontroller.GetConn(ctx, connOpts, false) conn := conncontroller.GetConn(ctx, connOpts, false, &connRequest.Keywords)
if conn == nil { if conn == nil {
return fmt.Errorf("connection not found: %s", connName) return fmt.Errorf("connection not found: %s", connName)
} }
return conn.Connect(ctx) return conn.Connect(ctx, &connRequest.Keywords)
} }
func (ws *WshServer) ConnReinstallWshCommand(ctx context.Context, connName string) error { func (ws *WshServer) ConnReinstallWshCommand(ctx context.Context, connName string) error {
@ -663,7 +664,7 @@ func (ws *WshServer) ConnReinstallWshCommand(ctx context.Context, connName strin
if err != nil { if err != nil {
return fmt.Errorf("error parsing connection name: %w", err) return fmt.Errorf("error parsing connection name: %w", err)
} }
conn := conncontroller.GetConn(ctx, connOpts, false) conn := conncontroller.GetConn(ctx, connOpts, false, &wshrpc.ConnKeywords{})
if conn == nil { if conn == nil {
return fmt.Errorf("connection not found: %s", connName) return fmt.Errorf("connection not found: %s", connName)
} }
@ -753,14 +754,14 @@ func (ws *WshServer) WshActivityCommand(ctx context.Context, data map[string]int
delete(data, key) delete(data, key)
} }
} }
activityUpdate := telemetry.ActivityUpdate{ activityUpdate := wshrpc.ActivityUpdate{
WshCmds: data, WshCmds: data,
} }
telemetry.GoUpdateActivityWrap(activityUpdate, "wsh-activity") telemetry.GoUpdateActivityWrap(activityUpdate, "wsh-activity")
return nil return nil
} }
func (ws *WshServer) ActivityCommand(ctx context.Context, activity telemetry.ActivityUpdate) error { func (ws *WshServer) ActivityCommand(ctx context.Context, activity wshrpc.ActivityUpdate) error {
telemetry.GoUpdateActivityWrap(activity, "wshrpc-activity") telemetry.GoUpdateActivityWrap(activity, "wshrpc-activity")
return nil return nil
} }

View File

@ -394,7 +394,7 @@ func (conn *WslConn) Connect(ctx context.Context) error {
conn.Status = Status_Error conn.Status = Status_Error
conn.Error = err.Error() conn.Error = err.Error()
conn.close_nolock() conn.close_nolock()
telemetry.GoUpdateActivityWrap(telemetry.ActivityUpdate{ telemetry.GoUpdateActivityWrap(wshrpc.ActivityUpdate{
Conn: map[string]int{"wsl:connecterror": 1}, Conn: map[string]int{"wsl:connecterror": 1},
}, "wsl-connconnect") }, "wsl-connconnect")
} else { } else {
@ -403,7 +403,7 @@ func (conn *WslConn) Connect(ctx context.Context) error {
if conn.ActiveConnNum == 0 { if conn.ActiveConnNum == 0 {
conn.ActiveConnNum = int(activeConnCounter.Add(1)) conn.ActiveConnNum = int(activeConnCounter.Add(1))
} }
telemetry.GoUpdateActivityWrap(telemetry.ActivityUpdate{ telemetry.GoUpdateActivityWrap(wshrpc.ActivityUpdate{
Conn: map[string]int{"wsl:connect": 1}, Conn: map[string]int{"wsl:connect": 1},
}, "wsl-connconnect") }, "wsl-connconnect")
} }