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
Original file line number Diff line number Diff line change
Expand Up @@ -93,8 +93,12 @@ const SSH_SENTENCES: [RegExp, (alias: string) => string][] = [
[/(Operation|Connection) timed out/, (alias) => `The SSH connection to ${alias} timed out. Check that it is on and reachable from this network.`],
];

function sshFailureSentence(stderr: string, alias: string): string | undefined {
return SSH_SENTENCES.find(([pattern]) => pattern.test(stderr))?.[1](alias);
/** A direct session cannot prompt, so a key with a passphrase only works through ssh-agent. */
const DIRECT_PERMISSION_HINT = 'Whiteboard cannot ask for a password or passphrase here: if your key has one, add it to ssh-agent with "ssh-add", then retry.';

function sshFailureSentence(stderr: string, alias: string, direct = false): string | undefined {
const sentence = SSH_SENTENCES.find(([pattern]) => pattern.test(stderr))?.[1](alias);
return direct && sentence && /Permission denied/.test(stderr) ? `${sentence} ${DIRECT_PERMISSION_HINT}` : sentence;
}

const SSH_DROPS: [RegExp, (alias: string) => string][] = [
Expand Down Expand Up @@ -599,7 +603,7 @@ export class ReviewRemoteHost {
detail: sshDropSentence(last, this.alias) ?? `The SSH connection to ${this.alias} ended: ${last || exited}`,
});
}
const sentence = sshFailureSentence(this.masterStderr, this.alias);
const sentence = sshFailureSentence(this.masterStderr, this.alias, this.direct);
if (sentence) this.options.log(`${this.alias}: ${exited} ${firstLines(this.masterStderr)}`);
this.fail({
state: classifySshFailure(this.masterStderr, this.promptCancelled),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,10 @@
*--------------------------------------------------------------------------------------------*/

import { createHash } from "node:crypto";
import { existsSync } from "node:fs";
import { chmod, lstat, mkdir } from "node:fs/promises";
import { homedir, tmpdir } from "node:os";
import { join } from "node:path";
import { join, win32 } from "node:path";

import { validateSshAlias } from "../../common/reviewSshAlias.js";

Expand All @@ -32,6 +33,13 @@ export function reviewSshSharesConnections(env: NodeJS.ProcessEnv = process.env,
return platform !== "win32" && !(env.VSCODE_DEV && env[REVIEW_SSH_DIRECT_ENV] === "1");
}

/** On Windows, the inbox Win32-OpenSSH rather than an MSYS ssh, such as Git for Windows', earlier on PATH. */
export function reviewSshBinary(env: NodeJS.ProcessEnv = process.env, platform = process.platform, exists = existsSync): string {
if (platform !== "win32") return "ssh";
const inbox = win32.join(env.SystemRoot ?? "C:\\Windows", "System32", "OpenSSH", "ssh.exe");
return exists(inbox) ? inbox : "ssh";
}

/** Connection sharing needs Unix sockets and a uid; Windows has neither. */
function currentUid(): number {
if (!process.getuid) throw new Error("SSH connection sharing is not supported on this platform.");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,8 @@ async function expand(pattern: string, includeDir: string): Promise<string[]> {
const path = pattern.startsWith("~/") ? join(homedir(), pattern.slice(2)) : isAbsolute(pattern) ? pattern : join(includeDir, pattern);
const files: string[] = [];
try {
for await (const file of glob(path)) files.push(file);
// A glob pattern escapes with backslashes; Windows paths take forward slashes too.
for await (const file of glob(process.platform === "win32" ? path.replaceAll("\\", "/") : path)) files.push(file);
} catch {
return [];
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ import { installRemote } from "./remote/reviewRemoteInstaller.js";
import { readRemoteInstallScript } from "./remote/reviewRemoteInstallScript.js";
import { createSshAskpass } from "./remote/reviewSshAskpass.js";
import {
reviewSshBinary,
reviewSshConfigPath,
reviewSshControlDirectory,
reviewSshSharesConnections,
Expand Down Expand Up @@ -357,11 +358,19 @@ export class ReviewDesktopHost extends Disposable {
private startRemoteHosts(
shellEnvironment: () => Promise<NodeJS.ProcessEnv>,
): void {
// SSH hosts need ControlMaster sockets, which Windows OpenSSH lacks.
if (this.disposed || process.platform === "win32") return;
if (this.disposed) return;
let version: Promise<string> | undefined;
const ssh = reviewSshBinary();
const manager = new ReviewRemoteHosts({
spawn: (args, options) => spawn("ssh", args, options),
// A detached ssh gets a console window on Windows; direct sessions never prompt there.
spawn: (args, options) =>
spawn(
ssh,
args,
process.platform === "win32"
? { ...options, detached: false, windowsHide: true }
: options,
),
controlDirectory: reviewSshSharesConnections()
? reviewSshControlDirectory()
: undefined,
Expand Down
20 changes: 16 additions & 4 deletions docs/remote-hosts.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,20 @@ config apply to Whiteboard's connections too.
- An OpenSSH server that allows TCP forwarding. See
[TCP forwarding](#tcp-forwarding).

Your laptop needs macOS or Linux with the OpenSSH client. A Windows laptop is
not supported yet.
Your laptop needs the OpenSSH client. On Windows, that is the built-in
OpenSSH Client, with two limits:

- Whiteboard cannot show SSH prompts there. Use a key without a passphrase, or
load it into the `ssh-agent` service:

```powershell
Get-Service ssh-agent | Set-Service -StartupType Automatic -PassThru | Start-Service
ssh-add
```

- Run `ssh <alias>` once in a terminal to verify and accept the host's key.

Each command Desktop runs on a Windows laptop opens its own SSH connection.

## Add a host

Expand All @@ -50,8 +62,8 @@ remote is deleted unless you check
The hosts are kept in the setting `review.remote.hosts`, a list of aliases.
Only you can change it: a repository or a remote never adds a host.

If `ssh` asks for a password, a passphrase or a host key confirmation,
Whiteboard shows the prompt in its window. Your answer goes to `ssh` and is
On macOS and Linux, if `ssh` asks for a password, a passphrase or a host key
confirmation, Whiteboard shows the prompt in its window. Your answer goes to `ssh` and is
never stored.

## How Desktop installs Whiteboard
Expand Down
Loading