Skip to content

Commit e45fd25

Browse files
Add external API part 1 (#2799)
* Add external API part 1 * use default parameters * most of robs feedback * add signature Co-authored-by: Robert Holt <[email protected]> * continue on error Co-authored-by: Robert Holt <[email protected]>
1 parent 7b9e21a commit e45fd25

File tree

10 files changed

+298
-34
lines changed

10 files changed

+298
-34
lines changed

.vscode/launch.json

+8-22
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
}

.vsts-ci/templates/ci-general.yml

+1
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ steps:
1111
1212
# Using `prependpath` to update the PATH just for this build.
1313
Write-Host "##vso[task.prependpath]$powerShellPath"
14+
continueOnError: true
1415
displayName: Install PowerShell Daily
1516
- pwsh: '$PSVersionTable'
1617
displayName: Display PowerShell version information

package-lock.json

+11
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+20
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

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

src/main.ts

+4-4
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

+1-5
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

+11-3
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
}

0 commit comments

Comments
 (0)