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
1 change: 0 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
### Fixed

- Suggest "Open Documentation" when toolchain not found ([#1939](https://github.com/swiftlang/vscode-swift/pull/1939))
- Fix colorization of standard swift-testing test runs ([#1933](https://github.com/swiftlang/vscode-swift/pull/1933))
- Make sure all folder operation listeners get past folder add events ([#1945](https://github.com/swiftlang/vscode-swift/pull/1945))

## 2.14.0 - 2025-11-11
Expand Down
3 changes: 2 additions & 1 deletion src/TestExplorer/TestRunner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -952,7 +952,8 @@ export class TestRunner {
presentationOptions: { reveal: vscode.TaskRevealKind.Never },
},
this.folderContext.toolchain,
{ ...process.env, ...testBuildConfig.env }
{ ...process.env, ...testBuildConfig.env },
{ readOnlyTerminal: process.platform !== "win32" }
);

task.execution.onDidWrite(str => {
Expand Down
7 changes: 5 additions & 2 deletions src/tasks/SwiftExecution.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,12 @@
//===----------------------------------------------------------------------===//
import * as vscode from "vscode";

import { SwiftProcess, SwiftPtyProcess } from "./SwiftProcess";
import { ReadOnlySwiftProcess, SwiftProcess, SwiftPtyProcess } from "./SwiftProcess";
import { SwiftPseudoterminal } from "./SwiftPseudoterminal";

export interface SwiftExecutionOptions extends vscode.ProcessExecutionOptions {
presentation?: vscode.TaskPresentationOptions;
readOnlyTerminal?: boolean;
}

/**
Expand All @@ -41,7 +42,9 @@ export class SwiftExecution extends vscode.CustomExecution implements vscode.Dis
super(async () => {
const createSwiftProcess = () => {
if (!swiftProcess) {
this.swiftProcess = new SwiftPtyProcess(command, args, options);
this.swiftProcess = options.readOnlyTerminal
? new ReadOnlySwiftProcess(command, args, options)
: new SwiftPtyProcess(command, args, options);
this.listen(this.swiftProcess);
}
return this.swiftProcess!;
Expand Down
103 changes: 103 additions & 0 deletions src/tasks/SwiftProcess.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
// SPDX-License-Identifier: Apache-2.0
//
//===----------------------------------------------------------------------===//
import * as child_process from "child_process";
import type * as nodePty from "node-pty";
import * as vscode from "vscode";

Expand Down Expand Up @@ -201,3 +202,105 @@ export class SwiftPtyProcess implements SwiftProcess {

onDidClose: vscode.Event<number | void> = this.closeHandler.event;
}

/**
* A {@link SwiftProcess} that spawns a child process and does not bind to stdio.
*
* Use this for Swift tasks that do not need to accept input, as its lighter weight and
* less error prone than using a spawned node-pty process.
*
* Specifically node-pty on Linux suffers from a long standing issue where the last chunk
* of output before a program exits is sometimes dropped, especially if that program produces
* a lot of output immediately before exiting. See https://github.com/microsoft/node-pty/issues/72
*/
export class ReadOnlySwiftProcess implements SwiftProcess {
private readonly spawnEmitter: vscode.EventEmitter<void> = new vscode.EventEmitter<void>();
private readonly writeEmitter: vscode.EventEmitter<string> = new vscode.EventEmitter<string>();
private readonly errorEmitter: vscode.EventEmitter<Error> = new vscode.EventEmitter<Error>();
private readonly closeHandler: CloseHandler = new CloseHandler();
private disposables: vscode.Disposable[] = [];

private spawnedProcess: child_process.ChildProcessWithoutNullStreams | undefined;

constructor(
public readonly command: string,
public readonly args: string[],
private readonly options: vscode.ProcessExecutionOptions = {}
) {
this.disposables.push(
this.spawnEmitter,
this.writeEmitter,
this.errorEmitter,
this.closeHandler
);
}

spawn(): void {
try {
this.spawnedProcess = child_process.spawn(this.command, this.args, {
cwd: this.options.cwd,
env: { ...process.env, ...this.options.env },
});
this.spawnEmitter.fire();

this.spawnedProcess.stdout.on("data", data => {
this.writeEmitter.fire(data.toString());
this.closeHandler.reset();
});

this.spawnedProcess.stderr.on("data", data => {
this.writeEmitter.fire(data.toString());
this.closeHandler.reset();
});

this.spawnedProcess.on("error", error => {
this.errorEmitter.fire(new Error(`${error}`));
this.closeHandler.handle();
});

this.spawnedProcess.once("exit", code => {
this.closeHandler.handle(code ?? undefined);
});

this.disposables.push(
this.onDidClose(() => {
this.dispose();
})
);
} catch (error) {
this.errorEmitter.fire(new Error(`${error}`));
this.closeHandler.handle();
}
}

handleInput(_s: string): void {
// Do nothing
}

terminate(signal?: NodeJS.Signals): void {
if (!this.spawnedProcess) {
return;
}
this.spawnedProcess.kill(signal);
this.dispose();
}

setDimensions(_dimensions: vscode.TerminalDimensions): void {
// Do nothing
}

dispose(): void {
this.spawnedProcess?.stdout.removeAllListeners();
this.spawnedProcess?.stderr.removeAllListeners();
this.spawnedProcess?.removeAllListeners();
this.disposables.forEach(d => d.dispose());
}

onDidSpawn: vscode.Event<void> = this.spawnEmitter.event;

onDidWrite: vscode.Event<string> = this.writeEmitter.event;

onDidThrowError: vscode.Event<Error> = this.errorEmitter.event;

onDidClose: vscode.Event<number | void> = this.closeHandler.event;
}
4 changes: 3 additions & 1 deletion src/tasks/SwiftTaskProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -291,7 +291,8 @@ export function createSwiftTask(
name: string,
config: TaskConfig,
toolchain: SwiftToolchain,
cmdEnv: { [key: string]: string } = {}
cmdEnv: { [key: string]: string } = {},
options: { readOnlyTerminal: boolean } = { readOnlyTerminal: false }
): SwiftTask {
const swift = toolchain.getToolchainExecutable("swift");
args = toolchain.buildFlags.withAdditionalFlags(args);
Expand Down Expand Up @@ -339,6 +340,7 @@ export function createSwiftTask(
cwd: fullCwd,
env: env,
presentation,
readOnlyTerminal: options.readOnlyTerminal,
})
);
// This doesn't include any quotes added by VS Code.
Expand Down