Skip to content

Commit 5044a3d

Browse files
authored
Prompt to restart extension on xcode-select (#1244)
1 parent 8139b11 commit 5044a3d

File tree

3 files changed

+170
-0
lines changed

3 files changed

+170
-0
lines changed

src/extension.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ import { showReloadExtensionNotification } from "./ui/ReloadExtension";
3838
import { checkAndWarnAboutWindowsSymlinks } from "./ui/win32";
3939
import { SwiftEnvironmentVariablesManager, SwiftTerminalProfileProvider } from "./terminal";
4040
import { resolveFolderDependencies } from "./commands/dependencies/resolve";
41+
import { SelectedXcodeWatcher } from "./toolchain/SelectedXcodeWatcher";
4142

4243
/**
4344
* External API as exposed by the extension. Can be queried by other extensions
@@ -125,6 +126,7 @@ export async function activate(context: vscode.ExtensionContext): Promise<Api> {
125126
context.subscriptions.push(...commands.register(workspaceContext));
126127
context.subscriptions.push(workspaceContext);
127128
context.subscriptions.push(registerDebugger(workspaceContext));
129+
context.subscriptions.push(new SelectedXcodeWatcher(outputChannel));
128130

129131
// listen for workspace folder changes and active text editor changes
130132
workspaceContext.setupEventListeners();
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the VS Code Swift open source project
4+
//
5+
// Copyright (c) 2021-2024 the VS Code Swift project authors
6+
// Licensed under Apache License v2.0
7+
//
8+
// See LICENSE.txt for license information
9+
// See CONTRIBUTORS.txt for the list of VS Code Swift project authors
10+
//
11+
// SPDX-License-Identifier: Apache-2.0
12+
//
13+
//===----------------------------------------------------------------------===//
14+
15+
import * as fs from "fs/promises";
16+
import * as vscode from "vscode";
17+
import { SwiftOutputChannel } from "../ui/SwiftOutputChannel";
18+
import { showReloadExtensionNotification } from "../ui/ReloadExtension";
19+
import configuration from "../configuration";
20+
21+
export class SelectedXcodeWatcher implements vscode.Disposable {
22+
private xcodePath: string | undefined;
23+
private disposed: boolean = false;
24+
private interval: NodeJS.Timeout | undefined;
25+
private checkIntervalMs: number;
26+
private xcodeSymlink: () => Promise<string | undefined>;
27+
28+
private static DEFAULT_CHECK_INTERVAL_MS = 2000;
29+
private static XCODE_SYMLINK_LOCATION = "/var/select/developer_dir";
30+
31+
constructor(
32+
private outputChannel: SwiftOutputChannel,
33+
testDependencies?: {
34+
checkIntervalMs?: number;
35+
xcodeSymlink?: () => Promise<string | undefined>;
36+
}
37+
) {
38+
this.checkIntervalMs =
39+
testDependencies?.checkIntervalMs || SelectedXcodeWatcher.DEFAULT_CHECK_INTERVAL_MS;
40+
this.xcodeSymlink =
41+
testDependencies?.xcodeSymlink ||
42+
(async () => {
43+
try {
44+
return await fs.readlink(SelectedXcodeWatcher.XCODE_SYMLINK_LOCATION);
45+
} catch (e) {
46+
return undefined;
47+
}
48+
});
49+
50+
if (!this.isValidXcodePlatform()) {
51+
return;
52+
}
53+
54+
// Deliberately not awaiting this, as we don't want to block the extension activation.
55+
this.setup();
56+
}
57+
58+
dispose() {
59+
this.disposed = true;
60+
clearInterval(this.interval);
61+
}
62+
63+
/**
64+
* Polls the Xcode symlink location checking if it has changed.
65+
* If the user has `swift.path` set in their settings this check is skipped.
66+
*/
67+
private async setup() {
68+
this.xcodePath = await this.xcodeSymlink();
69+
this.interval = setInterval(async () => {
70+
if (this.disposed) {
71+
return clearInterval(this.interval);
72+
}
73+
74+
const newXcodePath = await this.xcodeSymlink();
75+
if (!configuration.path && newXcodePath && this.xcodePath !== newXcodePath) {
76+
this.outputChannel.appendLine(
77+
`Selected Xcode changed from ${this.xcodePath} to ${newXcodePath}`
78+
);
79+
showReloadExtensionNotification(
80+
"The Swift Extension has detected a change in the selected Xcode. Please reload the extension to apply the changes."
81+
);
82+
this.xcodePath = newXcodePath;
83+
}
84+
}, this.checkIntervalMs);
85+
}
86+
87+
/**
88+
* Xcode selection is a macOS only concept.
89+
*/
90+
private isValidXcodePlatform() {
91+
return process.platform === "darwin";
92+
}
93+
}
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the VS Code Swift open source project
4+
//
5+
// Copyright (c) 2024 the VS Code Swift project authors
6+
// Licensed under Apache License v2.0
7+
//
8+
// See LICENSE.txt for license information
9+
// See CONTRIBUTORS.txt for the list of VS Code Swift project authors
10+
//
11+
// SPDX-License-Identifier: Apache-2.0
12+
//
13+
//===----------------------------------------------------------------------===//
14+
15+
import * as vscode from "vscode";
16+
import { expect } from "chai";
17+
import { SelectedXcodeWatcher } from "../../../src/toolchain/SelectedXcodeWatcher";
18+
import { SwiftOutputChannel } from "../../../src/ui/SwiftOutputChannel";
19+
import { instance, MockedObject, mockFn, mockGlobalObject, mockObject } from "../../MockUtils";
20+
21+
suite("Selected Xcode Watcher", () => {
22+
const mockedVSCodeWindow = mockGlobalObject(vscode, "window");
23+
let mockOutputChannel: MockedObject<SwiftOutputChannel>;
24+
25+
setup(function () {
26+
// Xcode only exists on macOS, so the SelectedXcodeWatcher is macOS-only.
27+
if (process.platform !== "darwin") {
28+
this.skip();
29+
}
30+
31+
mockOutputChannel = mockObject<SwiftOutputChannel>({
32+
appendLine: mockFn(),
33+
});
34+
});
35+
36+
async function run(symLinksOnCallback: (string | undefined)[]) {
37+
return new Promise<void>(resolve => {
38+
let ctr = 0;
39+
const watcher = new SelectedXcodeWatcher(instance(mockOutputChannel), {
40+
checkIntervalMs: 1,
41+
xcodeSymlink: async () => {
42+
if (ctr >= symLinksOnCallback.length) {
43+
watcher.dispose();
44+
resolve();
45+
return;
46+
}
47+
const response = symLinksOnCallback[ctr];
48+
ctr += 1;
49+
return response;
50+
},
51+
});
52+
});
53+
}
54+
55+
test("Does nothing when the symlink is undefined", async () => {
56+
await run([undefined, undefined]);
57+
58+
expect(mockedVSCodeWindow.showWarningMessage).to.have.not.been.called;
59+
});
60+
61+
test("Does nothing when the symlink is identical", async () => {
62+
await run(["/foo", "/foo"]);
63+
64+
expect(mockedVSCodeWindow.showWarningMessage).to.have.not.been.called;
65+
});
66+
67+
test("Prompts to restart when the symlink changes", async () => {
68+
await run(["/foo", "/bar"]);
69+
70+
expect(mockedVSCodeWindow.showWarningMessage).to.have.been.calledOnceWithExactly(
71+
"The Swift Extension has detected a change in the selected Xcode. Please reload the extension to apply the changes.",
72+
"Reload Extensions"
73+
);
74+
});
75+
});

0 commit comments

Comments
 (0)