Skip to content

Commit f8be27b

Browse files
authored
Add support for cancelling Swiftly toolchain install (#1859)
1 parent e412f6a commit f8be27b

File tree

3 files changed

+435
-68
lines changed

3 files changed

+435
-68
lines changed

src/commands/installSwiftlyToolchain.ts

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -43,16 +43,18 @@ async function downloadAndInstallToolchain(selected: SwiftlyToolchainItem, ctx:
4343
export async function installSwiftlyToolchainVersion(
4444
version: string,
4545
logger?: SwiftLogger,
46-
showReloadNotification: boolean = true
46+
showReloadNotification: boolean = true,
47+
token?: vscode.CancellationToken
4748
): Promise<boolean> {
4849
try {
4950
await vscode.window.withProgress(
5051
{
5152
location: vscode.ProgressLocation.Notification,
5253
title: `Installing Swift ${version}`,
53-
cancellable: false,
54+
cancellable: true,
5455
},
55-
async progress => {
56+
async (progress, progressToken) => {
57+
const effectiveToken = token || progressToken;
5658
progress.report({ message: "Starting installation..." });
5759

5860
let lastProgress = 0;
@@ -74,7 +76,8 @@ export async function installSwiftlyToolchainVersion(
7476
lastProgress = progressData.step.percent;
7577
}
7678
},
77-
logger
79+
logger,
80+
effectiveToken
7881
);
7982

8083
progress.report({
@@ -91,6 +94,13 @@ export async function installSwiftlyToolchainVersion(
9194
}
9295
return true;
9396
} catch (error) {
97+
const errorMessage = (error as Error).message;
98+
if (errorMessage.includes(Swiftly.cancellationMessage)) {
99+
logger?.info(`Installation of Swift ${version} was cancelled by user`);
100+
// Don't show error message for user-initiated cancellation
101+
return false;
102+
}
103+
94104
logger?.error(`Failed to install Swift ${version}: ${error}`);
95105
void vscode.window.showErrorMessage(`Failed to install Swift ${version}: ${error}`);
96106
return false;

src/toolchain/swiftly.ts

Lines changed: 67 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -160,12 +160,14 @@ export function parseSwiftlyMissingToolchainError(
160160
* @param version The toolchain version to install
161161
* @param logger Optional logger for error reporting
162162
* @param folder Optional folder context
163+
* @param token Optional cancellation token to abort the installation
163164
* @returns Promise<boolean> true if toolchain was successfully installed, false otherwise
164165
*/
165166
export async function handleMissingSwiftlyToolchain(
166167
version: string,
167168
logger?: SwiftLogger,
168-
folder?: vscode.Uri
169+
folder?: vscode.Uri,
170+
token?: vscode.CancellationToken
169171
): Promise<boolean> {
170172
logger?.info(`Attempting to handle missing toolchain: ${version}`);
171173

@@ -178,10 +180,12 @@ export async function handleMissingSwiftlyToolchain(
178180

179181
// Use the existing installation function without showing reload notification
180182
// (since we want to continue the current operation)
181-
return await installSwiftlyToolchainVersion(version, logger, false);
183+
return await installSwiftlyToolchainVersion(version, logger, false, token);
182184
}
183185

184186
export class Swiftly {
187+
public static cancellationMessage = "Installation cancelled by user";
188+
185189
/**
186190
* Finds the version of Swiftly installed on the system.
187191
*
@@ -452,11 +456,13 @@ export class Swiftly {
452456
* @param version The toolchain version to install.
453457
* @param progressCallback Optional callback that receives progress data as JSON objects.
454458
* @param logger Optional logger for error reporting.
459+
* @param token Optional cancellation token to abort the installation.
455460
*/
456461
public static async installToolchain(
457462
version: string,
458463
progressCallback?: (progressData: SwiftlyProgressData) => void,
459-
logger?: SwiftLogger
464+
logger?: SwiftLogger,
465+
token?: vscode.CancellationToken
460466
): Promise<void> {
461467
if (!this.isSupported()) {
462468
throw new Error("Swiftly is not supported on this platform");
@@ -481,7 +487,18 @@ export class Swiftly {
481487
crlfDelay: Infinity,
482488
});
483489

490+
// Handle cancellation during progress tracking
491+
const cancellationHandler = token?.onCancellationRequested(() => {
492+
rl.close();
493+
reject(new Error(Swiftly.cancellationMessage));
494+
});
495+
484496
rl.on("line", (line: string) => {
497+
if (token?.isCancellationRequested) {
498+
rl.close();
499+
return;
500+
}
501+
485502
try {
486503
const progressData = JSON.parse(line.trim()) as SwiftlyProgressData;
487504
progressCallback(progressData);
@@ -491,10 +508,12 @@ export class Swiftly {
491508
});
492509

493510
rl.on("close", () => {
511+
cancellationHandler?.dispose();
494512
resolve();
495513
});
496514

497515
rl.on("error", err => {
516+
cancellationHandler?.dispose();
498517
reject(err);
499518
});
500519
});
@@ -514,29 +533,70 @@ export class Swiftly {
514533
}
515534

516535
try {
517-
const installPromise = execFile("swiftly", installArgs);
536+
// Create output streams for process output
537+
const stdoutStream = new Stream.PassThrough();
538+
const stderrStream = new Stream.PassThrough();
539+
540+
// Use execFileStreamOutput with cancellation token
541+
const installPromise = execFileStreamOutput(
542+
"swiftly",
543+
installArgs,
544+
stdoutStream,
545+
stderrStream,
546+
token || null,
547+
{}
548+
);
518549

519550
if (progressPromise) {
520-
await Promise.all([installPromise, progressPromise]);
551+
await Promise.race([
552+
Promise.all([installPromise, progressPromise]),
553+
new Promise<never>((_, reject) => {
554+
if (token) {
555+
token.onCancellationRequested(() =>
556+
reject(new Error(Swiftly.cancellationMessage))
557+
);
558+
}
559+
}),
560+
]);
521561
} else {
522562
await installPromise;
523563
}
524564

565+
// Check for cancellation before post-install
566+
if (token?.isCancellationRequested) {
567+
throw new Error(Swiftly.cancellationMessage);
568+
}
569+
525570
if (process.platform === "linux") {
526571
await this.handlePostInstallFile(postInstallFilePath, version, logger);
527572
}
573+
} catch (error) {
574+
if (
575+
token?.isCancellationRequested ||
576+
(error as Error).message.includes(Swiftly.cancellationMessage)
577+
) {
578+
logger?.info(`Installation of ${version} was cancelled by user`);
579+
throw new Error(Swiftly.cancellationMessage);
580+
}
581+
throw error;
528582
} finally {
529583
if (progressPipePath) {
530584
try {
531585
await fs.unlink(progressPipePath);
532586
} catch {
533-
// Ignore errors if the pipe file doesn't exist
587+
// Ignore errors - file may not exist
534588
}
535589
}
590+
591+
// Clean up post-install file
536592
try {
537593
await fs.unlink(postInstallFilePath);
538594
} catch {
539-
// Ignore errors if the post-install file doesn't exist
595+
// Ignore errors - file may not exist
596+
}
597+
598+
if (token?.isCancellationRequested) {
599+
logger?.info(`Cleaned up temporary files for cancelled installation of ${version}`);
540600
}
541601
}
542602
}

0 commit comments

Comments
 (0)