Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 23 additions & 2 deletions docs/docs/connections.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,18 @@ The easiest way to access connections is to click the <i className="fa-sharp fa-

## What are wsh Shell Extensions?

`wsh` is a small program that helps manage waveterm regardless of which machine you are currently connected to. In order to not interrupt the normal flow of the remote session, we install it on your remote machine at `~/.waveterm/bin/wsh`. Then, when wave connects to your connection (and only when wave connects to your connection), `~/.waveterm/bin` is added to your `PATH` for that individual session. For more info on what `wsh` is capable of, see [wsh command](/wsh). And if you wish to view the source code of `wsh`, you can find it [here](https://github.com/wavetermdev/waveterm/tree/main/cmd/wsh).
`wsh` is a small program that helps manage waveterm regardless of which machine you are currently connected to. It is always included on your host machine, but you also have the option to install it when connecting to a remote machine. If it is installed on the remote machine, it is installed at `~/.waveterm/bin/wsh`. Then, when wave connects to your connection (and only when wave connects to your connection), `~/.waveterm/bin` is added to your `PATH` for that individual session. For more info on what `wsh` is capable of, see [wsh command](/wsh). And if you wish to view the source code of `wsh`, you can find it [here](https://github.com/wavetermdev/waveterm/tree/main/cmd/wsh).

With `wsh` installed, you have the ability to view certain widgets from the remote machine as if it were your host. In addition, `wsh` can be used to influence the widgets across various machines. As a very simple example, you can close a widget on the host machine by using the `wsh` command in a terminal window on a remote machine. For more information on what you can accomplish with `wsh`, take a look [here](/wsh).

## Add a New Connection to the Dropdown

The SSH values that are loaded into the dropdown by default are obtained by parsing your `~/.ssh/config` and `/etc/ssh/ssh_config` files. Adding a new connection is as simple as adding a new `Host` to one of these files, typically the `~/.ssh/config` file.
The SSH values that are loaded into the dropdown by default are obtained by parsing the internal `config/connections.json` file in addition to your `~/.ssh/config` and `/etc/ssh/ssh_config` files. Adding a new connection can be added in a couple ways:

- adding a new `Host` to one of your ssh config files, typically the `~/.ssh/config` file
- adding a new entry in the internal `config/connections.json` file
- manually typing your connection into the connection box (if this successfully connects, the connection will be added to the internal `config/connections.json` file)
- use `wsh ssh [user]@[host]` in your terminal (if this successfully connects, the connection will be added to the internal `config/connections.json` file)

WSL values are added by searching the installed WSL distributions as they appear in the Windows Registry.

Expand Down Expand Up @@ -55,6 +62,20 @@ Host myhost

You would then be able to access this connection with `myhost` or `username@myhost`. And if you wanted to manually specify a port such as port 2222, you could do that by either adding `Port 2222` to the config file or connecting to `username@myhost:2222`.

## Internal SSH Configuration

In addition to the regular ssh config file, wave also has its own config file to manage separate variables. These include
| Keyword | Description |
|---------|-------------|
| conn:wshenabled | This boolean allows wsh to be used for your connection, if it is set to `false`, `wsh` will never be used for that connection. It defaults to `true`.|
| conn:askbeforewshinstall | This boolean is used to prompt the user before installing wsh. If it is set to false, `wsh` will automatically be installed instead without prompting. It defaults to `true`.|
| display:hidden | This boolean hides the connection from the dropdown list. It defaults to `false` |
| display:order | This float determines the order of connections in the connection dropdown. It defaults to `0`.|
| term:fontsize | This int can be used to override the terminal font size for blocks using this connection. The block metadata takes priority over this setting. It defaults to null which means the global setting will be used instead. |
| term:fontfamily | This string can be used to specify a terminal font family for blocks using this connection. The block metadata takes priority over this setting. It defaults to null which means the global setting will be used instead. |
| term:theme | This string can be used to specify a terminal theme for blocks using this connection. The block metadata takes priority over this setting. It defaults to null which means the global setting will be used instead. |
| ssh:identityfile | A list of strings containing the paths to identity files that will be used. If a `wsh ssh` command using the `-i` flag is successful, the identity file will automatically be added here. |

## Managing Connections with the CLI

The `wsh` command gives some commands specifically for interacting with the connections. You can view these [here](/wsh#conn).
2 changes: 1 addition & 1 deletion docs/docs/wsh.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -298,7 +298,7 @@ This will delete the block with the specified id.
wsh ssh [user@host]
```

This will use Wave's internal ssh implementation to connect to the specified remote machine.
This will use Wave's internal ssh implementation to connect to the specified remote machine. The `-i` flag can be used to specify a path to an identity file.

---

Expand Down
1 change: 1 addition & 0 deletions frontend/app/block/block.scss
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,7 @@
flex: 1 2 auto;
overflow: hidden;
padding-right: 4px;
@include mixins.ellipsis()
}

.connecting-svg {
Expand Down
65 changes: 61 additions & 4 deletions frontend/app/block/blockframe.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -185,8 +185,11 @@ const BlockFrame_Header = ({
const manageConnection = util.useAtomValueSafe(viewModel?.manageConnection);
const dragHandleRef = preview ? null : nodeModel.dragHandleRef;
const connName = blockData?.meta?.connection;
const allSettings = jotai.useAtomValue(atoms.fullConfigAtom);
const wshEnabled = allSettings?.connections?.[connName]?.["conn:wshenabled"] ?? true;
const connStatus = util.useAtomValueSafe(getConnStatusAtom(connName));
const wshEnabled =
(connName &&
(connStatus?.status == "connecting" || (connStatus?.wshenabled && connStatus?.status == "connected"))) ??
true;

React.useEffect(() => {
if (!magnified || preview || prevMagifiedState.current) {
Expand Down Expand Up @@ -342,6 +345,8 @@ const ConnStatusOverlay = React.memo(
const [overlayRefCallback, _, domRect] = useDimensionsWithCallbackRef(30);
const width = domRect?.width;
const [showError, setShowError] = React.useState(false);
const fullConfig = jotai.useAtomValue(atoms.fullConfigAtom);
const [showWshError, setShowWshError] = React.useState(false);

React.useEffect(() => {
if (width) {
Expand All @@ -356,12 +361,40 @@ const ConnStatusOverlay = React.memo(
prtn.catch((e) => console.log("error reconnecting", connName, e));
}, [connName]);

const handleDisableWsh = React.useCallback(async () => {
// using unknown is a hack. we need proper types for the
// connection config on the frontend
const metamaptype: unknown = {
"conn:wshenabled": false,
};
const data: ConnConfigRequest = {
host: connName,
metamaptype: metamaptype,
};
try {
await RpcApi.SetConnectionsConfigCommand(TabRpcClient, data);
} catch (e) {
console.log("problem setting connection config: ", e);
}
}, [connName]);

const handleRemoveWshError = React.useCallback(async () => {
try {
await RpcApi.DismissWshFailCommand(TabRpcClient, connName);
} catch (e) {
console.log("unable to dismiss wsh error: ", e);
}
}, [connName]);

let statusText = `Disconnected from "${connName}"`;
let showReconnect = true;
if (connStatus.status == "connecting") {
statusText = `Connecting to "${connName}"...`;
showReconnect = false;
}
if (connStatus.status == "connected") {
showReconnect = false;
}
let reconDisplay = null;
let reconClassName = "outlined grey";
if (width && width < 350) {
Expand All @@ -373,18 +406,37 @@ const ConnStatusOverlay = React.memo(
}
const showIcon = connStatus.status != "connecting";

if (isLayoutMode || connStatus.status == "connected" || connModalOpen) {
const wshConfigEnabled = fullConfig?.connections?.[connName]?.["conn:wshenabled"] ?? true;
React.useEffect(() => {
const showWshErrorTemp =
connStatus.status == "connected" &&
connStatus.wsherror &&
connStatus.wsherror != "" &&
wshConfigEnabled;

setShowWshError(showWshErrorTemp);
}, [connStatus, wshConfigEnabled]);

if (!showWshError && (isLayoutMode || connStatus.status == "connected" || connModalOpen)) {
return null;
}

return (
<div className="connstatus-overlay" ref={overlayRefCallback}>
<div className="connstatus-content">
<div className={clsx("connstatus-status-icon-wrapper", { "has-error": showError })}>
<div className={clsx("connstatus-status-icon-wrapper", { "has-error": showError || showWshError })}>
{showIcon && <i className="fa-solid fa-triangle-exclamation"></i>}
<div className="connstatus-status">
<div className="connstatus-status-text">{statusText}</div>
{showError ? <div className="connstatus-error">error: {connStatus.error}</div> : null}
{showWshError ? (
<div className="connstatus-error">unable to use wsh: {connStatus.wsherror}</div>
) : null}
{showWshError && (
<Button className={reconClassName} onClick={handleDisableWsh}>
always disable wsh
</Button>
)}
</div>
</div>
{showReconnect ? (
Expand All @@ -394,6 +446,11 @@ const ConnStatusOverlay = React.memo(
</Button>
</div>
) : null}
{showWshError ? (
<div className="connstatus-actions">
<Button className={`fa-xmark fa-solid ${reconClassName}`} onClick={handleRemoveWshError} />
</div>
) : null}
</div>
</div>
);
Expand Down
10 changes: 10 additions & 0 deletions frontend/app/store/wshclientapi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,11 @@ class RpcApiType {
return client.wshRpcCall("deletesubblock", data, opts);
}

// command "dismisswshfail" [call]
DismissWshFailCommand(client: WshClient, data: string, opts?: RpcOpts): Promise<void> {
return client.wshRpcCall("dismisswshfail", data, opts);
}

// command "dispose" [call]
DisposeCommand(client: WshClient, data: CommandDisposeData, opts?: RpcOpts): Promise<void> {
return client.wshRpcCall("dispose", data, opts);
Expand Down Expand Up @@ -262,6 +267,11 @@ class RpcApiType {
return client.wshRpcCall("setconfig", data, opts);
}

// command "setconnectionsconfig" [call]
SetConnectionsConfigCommand(client: WshClient, data: ConnConfigRequest, opts?: RpcOpts): Promise<void> {
return client.wshRpcCall("setconnectionsconfig", data, opts);
}

// command "setmeta" [call]
SetMetaCommand(client: WshClient, data: CommandSetMetaData, opts?: RpcOpts): Promise<void> {
return client.wshRpcCall("setmeta", data, opts);
Expand Down
7 changes: 7 additions & 0 deletions frontend/types/gotypes.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -278,6 +278,12 @@ declare global {
err: string;
};

// wshrpc.ConnConfigRequest
type ConnConfigRequest = {
host: string;
metamaptype: MetaType;
};

// wshrpc.ConnKeywords
type ConnKeywords = {
"conn:wshenabled"?: boolean;
Expand Down Expand Up @@ -319,6 +325,7 @@ declare global {
hasconnected: boolean;
activeconnnum: number;
error?: string;
wsherror?: string;
};

// wshrpc.CpuDataRequest
Expand Down
21 changes: 20 additions & 1 deletion pkg/blockcontroller/blockcontroller.go
Original file line number Diff line number Diff line change
Expand Up @@ -354,7 +354,26 @@ func (bc *BlockController) DoRunShellCommand(rc *RunShellOpts, blockMeta waveobj
}
cmdOpts.Env[wshutil.WaveJwtTokenVarName] = jwtStr
}
shellProc, err = shellexec.StartRemoteShellProc(rc.TermSize, cmdStr, cmdOpts, conn)
if !conn.WshEnabled.Load() {
shellProc, err = shellexec.StartRemoteShellProcNoWsh(rc.TermSize, cmdStr, cmdOpts, conn)
if err != nil {
return err
}
} else {
shellProc, err = shellexec.StartRemoteShellProc(rc.TermSize, cmdStr, cmdOpts, conn)
if err != nil {
conn.WithLock(func() {
conn.WshError = err.Error()
})
conn.WshEnabled.Store(false)
log.Printf("error starting remote shell proc with wsh: %v", err)
log.Print("attempting install without wsh")
shellProc, err = shellexec.StartRemoteShellProcNoWsh(rc.TermSize, cmdStr, cmdOpts, conn)
if err != nil {
return err
}
}
}
if err != nil {
return err
}
Expand Down
16 changes: 14 additions & 2 deletions pkg/remote/conncontroller/conncontroller.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ type SSHConn struct {
DomainSockListener net.Listener
ConnController *ssh.Session
Error string
WshError string
HasWaiter *atomic.Bool
LastConnectTime int64
ActiveConnNum int
Expand Down Expand Up @@ -94,10 +95,12 @@ func (conn *SSHConn) DeriveConnStatus() wshrpc.ConnStatus {
return wshrpc.ConnStatus{
Status: conn.Status,
Connected: conn.Status == Status_Connected,
WshEnabled: conn.WshEnabled.Load(),
Connection: conn.Opts.String(),
HasConnected: (conn.LastConnectTime > 0),
ActiveConnNum: conn.ActiveConnNum,
Error: conn.Error,
WshError: conn.WshError,
}
}

Expand Down Expand Up @@ -532,7 +535,11 @@ func (conn *SSHConn) connectInternal(ctx context.Context, connFlags *wshrpc.Conn
})
} else if installErr != nil {
log.Printf("error: unable to install wsh shell extensions for %s: %v\n", conn.GetName(), err)
return fmt.Errorf("conncontroller %s wsh install error: %v", conn.GetName(), installErr)
log.Print("attempting to run with nowsh instead")
conn.WithLock(func() {
conn.WshError = installErr.Error()
})
conn.WshEnabled.Store(false)
} else {
conn.WshEnabled.Store(true)
}
Expand All @@ -541,7 +548,12 @@ func (conn *SSHConn) connectInternal(ctx context.Context, connFlags *wshrpc.Conn
csErr := conn.StartConnServer()
if csErr != nil {
log.Printf("error: unable to start conn server for %s: %v\n", conn.GetName(), csErr)
return fmt.Errorf("conncontroller %s start wsh connserver error: %v", conn.GetName(), csErr)
log.Print("attempting to run with nowsh instead")
conn.WithLock(func() {
conn.WshError = csErr.Error()
})
conn.WshEnabled.Store(false)
//return fmt.Errorf("conncontroller %s start wsh connserver error: %v", conn.GetName(), csErr)
}
}
} else {
Expand Down
75 changes: 38 additions & 37 deletions pkg/shellexec/shellexec.go
Original file line number Diff line number Diff line change
Expand Up @@ -236,49 +236,50 @@ func StartWslShellProc(ctx context.Context, termSize waveobj.TermSize, cmdStr st
return &ShellProc{Cmd: cmdWrap, ConnName: conn.GetName(), CloseOnce: &sync.Once{}, DoneCh: make(chan any)}, nil
}

func StartRemoteShellProc(termSize waveobj.TermSize, cmdStr string, cmdOpts CommandOptsType, conn *conncontroller.SSHConn) (*ShellProc, error) {
func StartRemoteShellProcNoWsh(termSize waveobj.TermSize, cmdStr string, cmdOpts CommandOptsType, conn *conncontroller.SSHConn) (*ShellProc, error) {
client := conn.GetClient()
if !conn.WshEnabled.Load() {
// no wsh code
session, err := client.NewSession()
if err != nil {
return nil, err
}
session, err := client.NewSession()
if err != nil {
return nil, err
}

remoteStdinRead, remoteStdinWriteOurs, err := os.Pipe()
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
}
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
pipePty := &PipePty{
remoteStdinWrite: remoteStdinWriteOurs,
remoteStdoutRead: remoteStdoutReadOurs,
}
if termSize.Rows == 0 || termSize.Cols == 0 {
termSize.Rows = shellutil.DefaultTermRows
termSize.Cols = shellutil.DefaultTermCols
}
if termSize.Rows <= 0 || termSize.Cols <= 0 {
return nil, fmt.Errorf("invalid term size: %v", termSize)
}
session.Stdin = remoteStdinRead
session.Stdout = remoteStdoutWrite
session.Stderr = remoteStdoutWrite

session.RequestPty("xterm-256color", termSize.Rows, termSize.Cols, nil)
sessionWrap := MakeSessionWrap(session, "", pipePty)
err = session.Shell()
if err != nil {
pipePty.Close()
return nil, err
}
return &ShellProc{Cmd: sessionWrap, ConnName: conn.GetName(), CloseOnce: &sync.Once{}, DoneCh: make(chan any)}, nil
session.RequestPty("xterm-256color", termSize.Rows, termSize.Cols, nil)
sessionWrap := MakeSessionWrap(session, "", pipePty)
err = session.Shell()
if err != nil {
pipePty.Close()
return nil, err
}
return &ShellProc{Cmd: sessionWrap, ConnName: conn.GetName(), CloseOnce: &sync.Once{}, DoneCh: make(chan any)}, nil
}

func StartRemoteShellProc(termSize waveobj.TermSize, cmdStr string, cmdOpts CommandOptsType, conn *conncontroller.SSHConn) (*ShellProc, error) {
client := conn.GetClient()
shellPath := cmdOpts.ShellPath
if shellPath == "" {
remoteShellPath, err := remote.DetectShell(client)
Expand Down
Loading