Skip to content

Commit 243dedc

Browse files
Add external API part 1
1 parent 7b9e21a commit 243dedc

File tree

9 files changed

+288
-34
lines changed

9 files changed

+288
-34
lines changed

.vscode/launch.json

Lines changed: 8 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -9,44 +9,30 @@
99
"args": [ "--extensionDevelopmentPath=${workspaceRoot}" ],
1010
"stopOnEntry": false,
1111
"sourceMaps": true,
12-
"outFiles": ["${workspaceRoot}/out/src/**/*.js"],
12+
"outFiles": ["${workspaceFolder}/out/src/**/*.js"],
1313
"preLaunchTask": "BuildAll"
1414
},
1515
{
1616
"name": "Launch Extension (Build client only)",
1717
"type": "extensionHost",
1818
"request": "launch",
1919
"runtimeExecutable": "${execPath}",
20-
"args": [ "--extensionDevelopmentPath=${workspaceRoot}" ],
20+
"args": [ "--extensionDevelopmentPath=${workspaceFolder}" ],
2121
"stopOnEntry": false,
2222
"sourceMaps": true,
23-
"outFiles": ["${workspaceRoot}/out/src/**/*.js"],
23+
"outFiles": ["${workspaceFolder}/out/src/**/*.js"],
2424
"preLaunchTask": "Build"
2525
},
2626
{
2727
"name": "Launch Extension Tests",
2828
"type": "extensionHost",
2929
"request": "launch",
3030
"runtimeExecutable": "${execPath}",
31-
"args": ["--extensionDevelopmentPath=${workspaceRoot}", "--extensionTestsPath=${workspaceRoot}/out/test" ],
32-
"stopOnEntry": false,
33-
"sourceMaps": true,
34-
"outFiles": ["${workspaceRoot}/out/test/**/*.js"],
35-
"preLaunchTask": "Build",
36-
"skipFiles": [
37-
"${workspaceFolder}/node_modules/**/*",
38-
"${workspaceFolder}/lib/**/*",
39-
"/private/var/folders/**/*",
40-
"<node_internals>/**/*"
41-
]
42-
},
43-
{
44-
"name": "Attach",
45-
"type": "node",
46-
"request": "attach",
47-
"address": "localhost",
48-
"port": 5858,
49-
"sourceMaps": false
31+
"args": [
32+
"--extensionDevelopmentPath=${workspaceFolder}",
33+
"--extensionTestsPath=${workspaceFolder}/out/test/testRunner.js" ],
34+
"outFiles": ["${workspaceFolder}/out/**/*.js"],
35+
"preLaunchTask": "Build"
5036
}
5137
]
5238
}

package-lock.json

Lines changed: 11 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,11 +40,15 @@
4040
"onCommand:PowerShell.RestartSession",
4141
"onCommand:PowerShell.EnableISEMode",
4242
"onCommand:PowerShell.DisableISEMode",
43+
"onCommand:PowerShell.RegisterExternalExtension",
44+
"onCommand:PowerShell.UnregisterExternalExtension",
45+
"onCommand:PowerShell.GetPowerShellVersionDetails",
4346
"onView:PowerShellCommands"
4447
],
4548
"dependencies": {
4649
"node-fetch": "^2.6.0",
4750
"semver": "^7.3.2",
51+
"uuid": "^8.2.0",
4852
"vscode-extension-telemetry": "~0.1.6",
4953
"vscode-languageclient": "~6.1.3"
5054
},
@@ -57,6 +61,7 @@
5761
"@types/rewire": "~2.5.28",
5862
"@types/semver": "~7.2.0",
5963
"@types/sinon": "~9.0.4",
64+
"@types/uuid": "^8.0.0",
6065
"@types/vscode": "1.43.0",
6166
"mocha": "~5.2.0",
6267
"mocha-junit-reporter": "~2.0.0",
@@ -292,6 +297,21 @@
292297
"light": "resources/light/MovePanelBottom.svg",
293298
"dark": "resources/dark/MovePanelBottom.svg"
294299
}
300+
},
301+
{
302+
"command": "PowerShell.RegisterExternalExtension",
303+
"title": "Register an external extension",
304+
"category": "PowerShell"
305+
},
306+
{
307+
"command": "PowerShell.UnregisterExternalExtension",
308+
"title": "Unregister an external extension",
309+
"category": "PowerShell"
310+
},
311+
{
312+
"command": "PowerShell.GetPowerShellVersionDetails",
313+
"title": "Get details about the PowerShell version that the PowerShell extension is using",
314+
"category": "PowerShell"
295315
}
296316
],
297317
"menus": {

src/features/ExternalApi.ts

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
/*---------------------------------------------------------
2+
* Copyright (C) Microsoft Corporation. All rights reserved.
3+
*--------------------------------------------------------*/
4+
import * as vscode from "vscode";
5+
import { v4 as uuidv4 } from 'uuid';
6+
import { LanguageClient } from "vscode-languageclient";
7+
import { IFeature } from "../feature";
8+
import { Logger } from "../logging";
9+
import { SessionManager } from "../session";
10+
11+
interface IExternalExtension {
12+
readonly id: string;
13+
readonly apiVersion: string;
14+
}
15+
16+
export interface IExternalPowerShellDetails {
17+
exePath: string;
18+
version: string;
19+
displayName: string;
20+
architecture: string;
21+
}
22+
23+
export class ExternalApiFeature implements IFeature {
24+
private commands: vscode.Disposable[];
25+
private languageClient: LanguageClient;
26+
private static readonly registeredExternalExtension: Map<string, IExternalExtension> = new Map<string, IExternalExtension>();
27+
28+
constructor(private sessionManager: SessionManager, private log: Logger) {
29+
this.commands = [
30+
/*
31+
DESCRIPTION:
32+
Registers your extension to allow usage of the external API. The returns
33+
a session UUID that will need to be passed in to subsequent API calls.
34+
35+
USAGE:
36+
vscode.commands.executeCommand(
37+
"PowerShell.RegisterExternalExtension",
38+
"ms-vscode.PesterTestExplorer" // the name of the extension using us
39+
"v1"); // API Version.
40+
41+
RETURNS:
42+
string session uuid
43+
*/
44+
vscode.commands.registerCommand("PowerShell.RegisterExternalExtension", (id: string, apiVersion: string = 'v1'): string => {
45+
log.writeDiagnostic(`Registering extension '${id}' for use with API version '${apiVersion}'.`);
46+
47+
for (const [_, externalExtension] of ExternalApiFeature.registeredExternalExtension) {
48+
if (externalExtension.id === id) {
49+
const message = `The extension '${id}' is already registered.`;
50+
log.writeWarning(message);
51+
throw new Error(message);
52+
}
53+
}
54+
55+
if (!vscode.extensions.all.some(ext => ext.id === id)) {
56+
throw new Error(`No extension installed with id '${id}'. You must use a valid extension id.`);
57+
}
58+
59+
// If we're in development mode, we allow these to be used for testing purposes.
60+
if (!sessionManager.InDevelopmentMode && (id === "ms-vscode.PowerShell" || id === "ms-vscode.PowerShell-Preview")) {
61+
throw new Error("You can't use the PowerShell extension's id in this registration.");
62+
}
63+
64+
const uuid = uuidv4();
65+
ExternalApiFeature.registeredExternalExtension.set(uuid, {
66+
id,
67+
apiVersion
68+
});
69+
return uuid;
70+
}),
71+
72+
/*
73+
DESCRIPTION:
74+
Unregisters a session that an extension has. This returns
75+
true if it succeeds or throws if it fails.
76+
77+
USAGE:
78+
vscode.commands.executeCommand(
79+
"PowerShell.UnregisterExternalExtension",
80+
"uuid"); // the uuid from above for tracking purposes
81+
82+
RETURNS:
83+
true if it worked, otherwise throws an error.
84+
*/
85+
vscode.commands.registerCommand("PowerShell.UnregisterExternalExtension", (uuid: string): boolean => {
86+
log.writeDiagnostic(`Unregistering extension with session UUID: ${uuid}`);
87+
if (!uuid && !ExternalApiFeature.registeredExternalExtension.delete(uuid)) {
88+
throw new Error(`No extension registered with session UUID: ${uuid}`);
89+
}
90+
return true;
91+
}),
92+
93+
/*
94+
DESCRIPTION:
95+
This will fetch the version details of the PowerShell used to start
96+
PowerShell Editor Services in the PowerShell extension.
97+
98+
USAGE:
99+
vscode.commands.executeCommand(
100+
"PowerShell.GetPowerShellVersionDetails",
101+
"uuid"); // the uuid from above for tracking purposes
102+
103+
RETURNS:
104+
An IPowerShellVersionDetails which consists of:
105+
{
106+
version: string;
107+
displayVersion: string;
108+
edition: string;
109+
architecture: string;
110+
}
111+
*/
112+
vscode.commands.registerCommand("PowerShell.GetPowerShellVersionDetails", async (uuid: string): Promise<IExternalPowerShellDetails> => {
113+
if (!uuid && !ExternalApiFeature.registeredExternalExtension.has(uuid)) {
114+
throw new Error(
115+
"UUID provided was invalid, make sure you execute the 'PowerShell.GetPowerShellVersionDetails' command and pass in the UUID that it returns to subsequent command executions.");
116+
}
117+
118+
// TODO: When we have more than one API version, make sure to include a check here.
119+
const extension = ExternalApiFeature.registeredExternalExtension.get(uuid);
120+
log.writeDiagnostic(`Extension '${extension.id}' used command 'PowerShell.GetPowerShellVersionDetails'.`);
121+
122+
await sessionManager.waitUntilStarted();
123+
const versionDetails = sessionManager.getPowerShellVersionDetails();
124+
125+
return {
126+
exePath: sessionManager.PowerShellExeDetails.exePath,
127+
version: versionDetails.version,
128+
displayName: sessionManager.PowerShellExeDetails.displayName, // comes from the Session Menu
129+
architecture: versionDetails.architecture
130+
};
131+
}),
132+
]
133+
}
134+
135+
public dispose() {
136+
for (const command of this.commands) {
137+
command.dispose();
138+
}
139+
}
140+
141+
public setLanguageClient(languageclient: LanguageClient) {
142+
this.languageClient = languageclient;
143+
}
144+
}

src/main.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,10 @@ import { CodeActionsFeature } from "./features/CodeActions";
1313
import { ConsoleFeature } from "./features/Console";
1414
import { CustomViewsFeature } from "./features/CustomViews";
1515
import { DebugSessionFeature } from "./features/DebugSession";
16-
import { PickPSHostProcessFeature } from "./features/DebugSession";
17-
import { PickRunspaceFeature } from "./features/DebugSession";
18-
import { SpecifyScriptArgsFeature } from "./features/DebugSession";
1916
import { ExamplesFeature } from "./features/Examples";
2017
import { ExpandAliasFeature } from "./features/ExpandAlias";
2118
import { ExtensionCommandsFeature } from "./features/ExtensionCommands";
19+
import { ExternalApiFeature } from "./features/ExternalApi";
2220
import { FindModuleFeature } from "./features/FindModule";
2321
import { GenerateBugReportFeature } from "./features/GenerateBugReport";
2422
import { GetCommandsFeature } from "./features/GetCommands";
@@ -27,14 +25,15 @@ import { ISECompatibilityFeature } from "./features/ISECompatibility";
2725
import { NewFileOrProjectFeature } from "./features/NewFileOrProject";
2826
import { OpenInISEFeature } from "./features/OpenInISE";
2927
import { PesterTestsFeature } from "./features/PesterTests";
28+
import { PickPSHostProcessFeature, PickRunspaceFeature } from "./features/DebugSession";
3029
import { RemoteFilesFeature } from "./features/RemoteFiles";
3130
import { RunCodeFeature } from "./features/RunCode";
3231
import { ShowHelpFeature } from "./features/ShowHelp";
32+
import { SpecifyScriptArgsFeature } from "./features/DebugSession";
3333
import { Logger, LogLevel } from "./logging";
3434
import { SessionManager } from "./session";
3535
import Settings = require("./settings");
3636
import { PowerShellLanguageId } from "./utils";
37-
import utils = require("./utils");
3837

3938
// The most reliable way to get the name and version of the current extension.
4039
// tslint:disable-next-line: no-var-requires
@@ -157,6 +156,7 @@ export function activate(context: vscode.ExtensionContext): void {
157156
new HelpCompletionFeature(logger),
158157
new CustomViewsFeature(),
159158
new PickRunspaceFeature(),
159+
new ExternalApiFeature(sessionManager, logger)
160160
];
161161

162162
sessionManager.setExtensionFeatures(extensionFeatures);

src/process.ts

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -179,10 +179,6 @@ export class PowerShellProcess {
179179
return true;
180180
}
181181

182-
private sleep(ms: number) {
183-
return new Promise(resolve => setTimeout(resolve, ms));
184-
}
185-
186182
private async waitForSessionFile(): Promise<utils.IEditorServicesSessionDetails> {
187183
// Determine how many tries by dividing by 2000 thus checking every 2 seconds.
188184
const numOfTries = this.sessionSettings.developer.waitForSessionFileTimeoutSeconds / 2;
@@ -203,7 +199,7 @@ export class PowerShellProcess {
203199
}
204200

205201
// Wait a bit and try again
206-
await this.sleep(2000);
202+
await utils.sleep(2000);
207203
}
208204

209205
const err = "Timed out waiting for session file to appear.";

src/session.ts

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -54,14 +54,15 @@ export class SessionManager implements Middleware {
5454
private sessionSettings: Settings.ISettings = undefined;
5555
private sessionDetails: utils.IEditorServicesSessionDetails;
5656
private bundledModulesPath: string;
57+
private started: boolean = false;
5758

5859
// Initialized by the start() method, since this requires settings
5960
private powershellExeFinder: PowerShellExeFinder;
6061

6162
// When in development mode, VS Code's session ID is a fake
6263
// value of "someValue.machineId". Use that to detect dev
6364
// mode for now until Microsoft/vscode#10272 gets implemented.
64-
private readonly inDevelopmentMode =
65+
public readonly InDevelopmentMode =
6566
vscode.env.sessionId === "someValue.sessionId";
6667

6768
constructor(
@@ -167,7 +168,7 @@ export class SessionManager implements Middleware {
167168

168169
this.bundledModulesPath = path.resolve(__dirname, this.sessionSettings.bundledModulesPath);
169170

170-
if (this.inDevelopmentMode) {
171+
if (this.InDevelopmentMode) {
171172
const devBundledModulesPath =
172173
path.resolve(
173174
__dirname,
@@ -274,6 +275,12 @@ export class SessionManager implements Middleware {
274275
return this.debugSessionProcess;
275276
}
276277

278+
public async waitUntilStarted(): Promise<void> {
279+
while(!this.started) {
280+
await utils.sleep(300);
281+
}
282+
}
283+
277284
// ----- LanguageClient middleware methods -----
278285

279286
public resolveCodeLens(
@@ -549,8 +556,9 @@ export class SessionManager implements Middleware {
549556
.then(
550557
async (versionDetails) => {
551558
this.versionDetails = versionDetails;
559+
this.started = true;
552560

553-
if (!this.inDevelopmentMode) {
561+
if (!this.InDevelopmentMode) {
554562
this.telemetryReporter.sendTelemetryEvent("powershellVersionCheck",
555563
{ powershellVersion: versionDetails.version });
556564
}

src/utils.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,10 @@ export function getTimestampString() {
9696
return `[${time.getHours()}:${time.getMinutes()}:${time.getSeconds()}]`;
9797
}
9898

99+
export function sleep(ms: number) {
100+
return new Promise(resolve => setTimeout(resolve, ms));
101+
}
102+
99103
export const isMacOS: boolean = process.platform === "darwin";
100104
export const isWindows: boolean = process.platform === "win32";
101105
export const isLinux: boolean = !isMacOS && !isWindows;

0 commit comments

Comments
 (0)