Skip to content
Open
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
31 changes: 28 additions & 3 deletions src/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import {
import { runDiagnosticCli } from "./command/diagnosticFlow";
import * as cliExec from "./core/cliExec";
import { CertificateError } from "./error/certificateError";
import { toError } from "./error/errorUtils";
import { raceWithAbort, toError } from "./error/errorUtils";
import { type FeatureSet, featureSetForVersion } from "./featureSet";
import {
AuthTelemetry,
Expand Down Expand Up @@ -45,6 +45,7 @@ import {
} from "./remote/sshOverrides";
import { resolveCliAuth } from "./settings/cli";
import { appendVsCodeLogs } from "./supportBundle/appendVsCodeLogs";
import { getRemoteEditorLogGlobs } from "./supportBundle/workspaceFiles";
import { runExportTelemetryCommand } from "./telemetry/export/command";
import { toRemoteAuthority } from "./util/authority";
import { openInBrowser, toSafeHost } from "./util/uri";
Expand Down Expand Up @@ -99,8 +100,11 @@ interface LoginArgs {
type WorkspaceResolution =
| {
readonly status: "selected";
readonly agentName?: string;
readonly client: CoderApi;
readonly workspaceId: string;
/** Active authority, only when it identifies this workspace. */
readonly remoteAuthority?: string;
}
| { readonly status: "cancelled" }
| {
Expand Down Expand Up @@ -150,6 +154,7 @@ export class Commands {
// are logged into (for convenience; otherwise the recents menu can be a pain
// if you use multiple deployments).
public workspace?: Workspace;
public agent?: WorkspaceAgent;
public workspaceLogPath?: string;
public remoteWorkspaceClient?: CoderApi;

Expand Down Expand Up @@ -424,7 +429,7 @@ export class Commands {
return;
}

const { client, workspaceId } = resolved;
const { agentName, client, workspaceId, remoteAuthority } = resolved;

const outputUri = await this.promptSupportBundlePath();
if (!outputUri) {
Expand All @@ -435,13 +440,30 @@ export class Commands {
const result = await withCancellableProgress(
async ({ signal, progress }) => {
progress.report({ message: "Resolving CLI..." });
// Independent of the CLI and never rejects, so resolve the globs
// concurrently and discard them when the CLI lacks support.
const remoteLogGlobs = getRemoteEditorLogGlobs({
appRoot: vscode.env.appRoot,
remoteAuthority,
logger: this.logger,
});
const env = await this.resolveCliEnv(client);
if (!env.featureSet.supportBundle) {
throw new SupportBundleUnsupportedCliError();
}

// The resolution APIs take no signal, so race to keep cancel honest.
const workspaceFiles = env.featureSet.supportBundleWorkspaceFiles
Comment thread
EhabY marked this conversation as resolved.
? await raceWithAbort(remoteLogGlobs, signal)
: [];

progress.report({ message: "Collecting diagnostics..." });
await cliExec.supportBundle(env, workspaceId, outputUri.fsPath, signal);
await cliExec.supportBundle(env, workspaceId, {
outputPath: outputUri.fsPath,
agentName,
workspaceFiles,
signal,
});

progress.report({ message: "Adding VS Code logs..." });
await appendVsCodeLogs(
Expand Down Expand Up @@ -1133,15 +1155,18 @@ export class Commands {
if (item) {
Comment thread
EhabY marked this conversation as resolved.
return {
status: "selected",
agentName: item instanceof AgentTreeItem ? item.agent.name : undefined,
client: this.extensionClient,
workspaceId: createWorkspaceIdentifier(item.workspace),
};
}
if (this.workspace && this.remoteWorkspaceClient) {
return {
status: "selected",
agentName: this.agent?.name,
client: this.remoteWorkspaceClient,
workspaceId: createWorkspaceIdentifier(this.workspace),
remoteAuthority: vscodeProposed.env.remoteAuthority,
};
}
const pick = await this.pickWorkspace("diagnostic", {
Expand Down
11 changes: 9 additions & 2 deletions src/core/cliExec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,18 +105,25 @@ export async function netcheck(
export async function supportBundle(
env: CliEnv,
workspaceName: string,
outputPath: string,
signal?: AbortSignal,
options: {
outputPath: string;
agentName?: string;
workspaceFiles?: readonly string[];
signal?: AbortSignal;
},
): Promise<void> {
const { outputPath, agentName, workspaceFiles = [], signal } = options;
const globalFlags = getGlobalFlags(env.configs, env.auth);
const args = [
...globalFlags,
"support",
"bundle",
workspaceName,
...(agentName ? [agentName] : []),
"--output-file",
outputPath,
"--yes",
...workspaceFiles.flatMap((file) => ["--workspace-file", file]),
];
try {
await execFileAsync(env.binary, args, { signal });
Expand Down
29 changes: 27 additions & 2 deletions src/error/errorUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,37 @@ export function isAbortError(error: unknown): error is Error {
*/
export function throwIfAborted(signal: AbortSignal | undefined): void {
if (!signal?.aborted) return;
const reason: unknown = signal.reason;
throw reason instanceof Error
throw toAbortError(signal.reason);
}

function toAbortError(reason: unknown): Error {
return reason instanceof Error
? reason
: Object.assign(new Error("Aborted"), { name: "AbortError" });
}

/**
* Await `promise`, rejecting with an AbortError as soon as `signal` aborts.
* The underlying work is abandoned, not stopped; use for operations that
* cannot take a signal themselves.
*/
export async function raceWithAbort<T>(
promise: Promise<T>,
signal: AbortSignal,
): Promise<T> {
throwIfAborted(signal);
let onAbort!: () => void;
const aborted = new Promise<never>((_, reject) => {
onAbort = () => reject(toAbortError(signal.reason));
});
signal.addEventListener("abort", onAbort, { once: true });
try {
return await Promise.race([promise, aborted]);
} finally {
signal.removeEventListener("abort", onAbort);
}
}

// getErrorDetail is copied from coder/site, but changes the default return.
export const getErrorDetail = (error: unknown): string | undefined | null => {
if (isApiError(error)) {
Expand Down
3 changes: 3 additions & 0 deletions src/featureSet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export interface FeatureSet {
keyringAuth: boolean;
tokenRead: boolean;
supportBundle: boolean;
supportBundleWorkspaceFiles: boolean;
}

/**
Expand Down Expand Up @@ -49,5 +50,7 @@ export function featureSetForVersion(
tokenRead: versionAtLeast(version, "2.31.0"),
// `coder support bundle` (officially released/unhidden in 2.10.0)
supportBundle: versionAtLeast(version, "2.10.0"),
// --workspace-file flag for `coder support bundle`
supportBundleWorkspaceFiles: versionAtLeast(version, "2.36.0"),
};
}
1 change: 1 addition & 0 deletions src/remote/remote.ts
Original file line number Diff line number Diff line change
Expand Up @@ -638,6 +638,7 @@ export class Remote {
});

this.commands.workspace = workspace;
this.commands.agent = agent;
return agent;
}

Expand Down
Loading