diff --git a/CHANGELOG.md b/CHANGELOG.md index 6d487f84c..5edf8bfcb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/src/TestExplorer/TestRunner.ts b/src/TestExplorer/TestRunner.ts index 879edbe3e..ae42eee5f 100644 --- a/src/TestExplorer/TestRunner.ts +++ b/src/TestExplorer/TestRunner.ts @@ -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 => { diff --git a/src/tasks/SwiftExecution.ts b/src/tasks/SwiftExecution.ts index a17d75f6a..78f6d6a04 100644 --- a/src/tasks/SwiftExecution.ts +++ b/src/tasks/SwiftExecution.ts @@ -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; } /** @@ -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!; diff --git a/src/tasks/SwiftProcess.ts b/src/tasks/SwiftProcess.ts index 1f50125ce..0dc9e345d 100644 --- a/src/tasks/SwiftProcess.ts +++ b/src/tasks/SwiftProcess.ts @@ -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"; @@ -201,3 +202,105 @@ export class SwiftPtyProcess implements SwiftProcess { onDidClose: vscode.Event = 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 = new vscode.EventEmitter(); + private readonly writeEmitter: vscode.EventEmitter = new vscode.EventEmitter(); + private readonly errorEmitter: vscode.EventEmitter = new vscode.EventEmitter(); + 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 = this.spawnEmitter.event; + + onDidWrite: vscode.Event = this.writeEmitter.event; + + onDidThrowError: vscode.Event = this.errorEmitter.event; + + onDidClose: vscode.Event = this.closeHandler.event; +} diff --git a/src/tasks/SwiftTaskProvider.ts b/src/tasks/SwiftTaskProvider.ts index d99e1fb2a..140692bd4 100644 --- a/src/tasks/SwiftTaskProvider.ts +++ b/src/tasks/SwiftTaskProvider.ts @@ -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); @@ -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.