mirror of
https://github.com/voideditor/void
synced 2026-05-22 17:08:25 +00:00
Added open-remote-wsl to extensions
This commit is contained in:
parent
5eb2bcfef6
commit
d0921f899c
22 changed files with 1781 additions and 0 deletions
3
extensions/open-remote-wsl/README.md
Normal file
3
extensions/open-remote-wsl/README.md
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
# Remote - WSL Support
|
||||
|
||||
Inherited for Void from [Open Remote - WSL](https://github.com/jeanp413/open-remote-wsl).
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
//@ts-check
|
||||
|
||||
'use strict';
|
||||
|
||||
const withBrowserDefaults = require('../shared.webpack.config').browser;
|
||||
|
||||
module.exports = withBrowserDefaults({
|
||||
context: __dirname,
|
||||
entry: {
|
||||
extension: './src/extension.ts'
|
||||
}
|
||||
});
|
||||
20
extensions/open-remote-wsl/extension.webpack.config.js
Normal file
20
extensions/open-remote-wsl/extension.webpack.config.js
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
//@ts-check
|
||||
|
||||
'use strict';
|
||||
|
||||
const withDefaults = require('../shared.webpack.config');
|
||||
|
||||
module.exports = withDefaults({
|
||||
context: __dirname,
|
||||
resolve: {
|
||||
mainFields: ['module', 'main']
|
||||
},
|
||||
entry: {
|
||||
extension: './src/extension.ts',
|
||||
}
|
||||
});
|
||||
15
extensions/open-remote-wsl/package-lock.json
generated
Normal file
15
extensions/open-remote-wsl/package-lock.json
generated
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
{
|
||||
"name": "open-remote-wsl",
|
||||
"version": "0.0.4",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "open-remote-wsl",
|
||||
"version": "0.0.4",
|
||||
"engines": {
|
||||
"vscode": "^1.70.2"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
281
extensions/open-remote-wsl/package.json
Normal file
281
extensions/open-remote-wsl/package.json
Normal file
|
|
@ -0,0 +1,281 @@
|
|||
{
|
||||
"name": "open-remote-wsl",
|
||||
"displayName": "Remote - WSL",
|
||||
"description": "Open any folder in the Windows Subsystem for Linux (WSL).",
|
||||
"version": "0.0.4",
|
||||
"icon": "resources/icon.png",
|
||||
"engines": {
|
||||
"vscode": "^1.70.2"
|
||||
},
|
||||
"extensionKind": [
|
||||
"ui"
|
||||
],
|
||||
"enabledApiProposals": [
|
||||
"resolvers",
|
||||
"contribViewsRemote"
|
||||
],
|
||||
"keywords": [
|
||||
"remote development",
|
||||
"remote",
|
||||
"wsl"
|
||||
],
|
||||
"api": "none",
|
||||
"activationEvents": [
|
||||
"onCommand:openremotewsl.connect",
|
||||
"onCommand:openremotewsl.connectInNewWindow",
|
||||
"onCommand:openremotewsl.connectUsingDistro",
|
||||
"onCommand:openremotewsl.connectUsingDistroInNewWindow",
|
||||
"onCommand:openremotewsl.showLog",
|
||||
"onResolveRemoteAuthority:wsl",
|
||||
"onView:wslTargets"
|
||||
],
|
||||
"main": "./out/extension.js",
|
||||
"contributes": {
|
||||
"configuration": {
|
||||
"title": "WSL",
|
||||
"properties": {
|
||||
"remote.WSL.serverDownloadUrlTemplate": {
|
||||
"type": "string",
|
||||
"description": "The URL from where the vscode server will be downloaded. You can use the following variables and they will be replaced dynamically:\n- ${quality}: vscode server quality, e.g. stable or insiders\n- ${version}: vscode server version, e.g. 1.69.0\n- ${commit}: vscode server release commit\n- ${arch}: vscode server arch, e.g. x64, armhf, arm64\n- ${release}: release number",
|
||||
"scope": "application",
|
||||
"default": "https://github.com/codestoryai/binaries/releases/download/${version}.${release}/void-reh-${os}-${arch}-${version}.${release}.tar.gz"
|
||||
}
|
||||
}
|
||||
},
|
||||
"views": {
|
||||
"remote": [
|
||||
{
|
||||
"id": "wslTargets",
|
||||
"name": "WSL Targets",
|
||||
"group": "targets@1",
|
||||
"when": "(isWindows && !isWeb)",
|
||||
"remoteName": "wsl"
|
||||
}
|
||||
]
|
||||
},
|
||||
"commands": [
|
||||
{
|
||||
"command": "openremotewsl.connect",
|
||||
"title": "Connect to WSL",
|
||||
"category": "Remote-WSL"
|
||||
},
|
||||
{
|
||||
"command": "openremotewsl.connectInNewWindow",
|
||||
"title": "Connect to WSL in New Window",
|
||||
"category": "Remote-WSL"
|
||||
},
|
||||
{
|
||||
"command": "openremotewsl.connectUsingDistro",
|
||||
"title": "Connect to WSL using Distro...",
|
||||
"category": "Remote-WSL"
|
||||
},
|
||||
{
|
||||
"command": "openremotewsl.connectUsingDistroInNewWindow",
|
||||
"title": "Connect to WSL using Distro in New Window...",
|
||||
"category": "Remote-WSL"
|
||||
},
|
||||
{
|
||||
"command": "openremotewsl.showLog",
|
||||
"title": "Show Log",
|
||||
"category": "Remote-WSL"
|
||||
},
|
||||
{
|
||||
"command": "openremotewsl.explorer.emptyWindowInNewWindow",
|
||||
"title": "Connect in New Window",
|
||||
"icon": "$(empty-window)"
|
||||
},
|
||||
{
|
||||
"command": "openremotewsl.explorer.emptyWindowInCurrentWindow",
|
||||
"title": "Connect in Current Window"
|
||||
},
|
||||
{
|
||||
"command": "openremotewsl.explorer.reopenFolderInCurrentWindow",
|
||||
"title": "Open in Current Window"
|
||||
},
|
||||
{
|
||||
"command": "openremotewsl.explorer.reopenFolderInNewWindow",
|
||||
"title": "Open in New Window",
|
||||
"icon": "$(folder-opened)"
|
||||
},
|
||||
{
|
||||
"command": "openremotewsl.explorer.deleteFolderHistoryItem",
|
||||
"title": "Remove From Recent List",
|
||||
"icon": "$(x)"
|
||||
},
|
||||
{
|
||||
"command": "openremotewsl.explorer.refresh",
|
||||
"title": "Refresh",
|
||||
"icon": "$(refresh)"
|
||||
},
|
||||
{
|
||||
"command": "openremotewsl.explorer.addDistro",
|
||||
"title": "Add a Distro",
|
||||
"icon": "$(plus)"
|
||||
},
|
||||
{
|
||||
"command": "openremotewsl.explorer.setDefaultDistro",
|
||||
"title": "Set as Default Distro"
|
||||
},
|
||||
{
|
||||
"command": "openremotewsl.explorer.deleteDistro",
|
||||
"title": "Delete Distro"
|
||||
}
|
||||
],
|
||||
"resourceLabelFormatters": [
|
||||
{
|
||||
"scheme": "vscode-remote",
|
||||
"authority": "wsl+*",
|
||||
"formatting": {
|
||||
"label": "${path}",
|
||||
"separator": "/",
|
||||
"tildify": true,
|
||||
"workspaceSuffix": "WSL"
|
||||
}
|
||||
}
|
||||
],
|
||||
"menus": {
|
||||
"statusBar/remoteIndicator": [
|
||||
{
|
||||
"command": "openremotewsl.connect",
|
||||
"when": "(isWindows && !isWeb)",
|
||||
"group": "remote_20_wsl_1general@1"
|
||||
},
|
||||
{
|
||||
"command": "openremotewsl.connectUsingDistro",
|
||||
"when": "(isWindows && !isWeb)",
|
||||
"group": "remote_20_wsl_1general@2"
|
||||
},
|
||||
{
|
||||
"command": "openremotewsl.showLog",
|
||||
"when": "remoteName =~ /^wsl$/",
|
||||
"group": "remote_20_wsl_1general@4"
|
||||
}
|
||||
],
|
||||
"commandPalette": [
|
||||
{
|
||||
"command": "openremotewsl.connect",
|
||||
"when": "(isWindows && !isWeb)"
|
||||
},
|
||||
{
|
||||
"command": "openremotewsl.connectInNewWindow",
|
||||
"when": "(isWindows && !isWeb)"
|
||||
},
|
||||
{
|
||||
"command": "openremotewsl.connectUsingDistro",
|
||||
"when": "(isWindows && !isWeb)"
|
||||
},
|
||||
{
|
||||
"command": "openremotewsl.connectUsingDistroInNewWindow",
|
||||
"when": "(isWindows && !isWeb)"
|
||||
},
|
||||
{
|
||||
"command": "openremotewsl.explorer.refresh",
|
||||
"when": "false"
|
||||
},
|
||||
{
|
||||
"command": "openremotewsl.explorer.addDistro",
|
||||
"when": "false"
|
||||
},
|
||||
{
|
||||
"command": "openremotewsl.explorer.emptyWindowInNewWindow",
|
||||
"when": "false"
|
||||
},
|
||||
{
|
||||
"command": "openremotewsl.explorer.emptyWindowInCurrentWindow",
|
||||
"when": "false"
|
||||
},
|
||||
{
|
||||
"command": "openremotewsl.explorer.reopenFolderInCurrentWindow",
|
||||
"when": "false"
|
||||
},
|
||||
{
|
||||
"command": "openremotewsl.explorer.reopenFolderInNewWindow",
|
||||
"when": "false"
|
||||
},
|
||||
{
|
||||
"command": "openremotewsl.explorer.deleteFolderHistoryItem",
|
||||
"when": "false"
|
||||
},
|
||||
{
|
||||
"command": "openremotewsl.explorer.setDefaultDistro",
|
||||
"when": "false"
|
||||
},
|
||||
{
|
||||
"command": "openremotewsl.explorer.deleteDistro",
|
||||
"when": "false"
|
||||
}
|
||||
],
|
||||
"view/title": [
|
||||
{
|
||||
"command": "openremotewsl.explorer.addDistro",
|
||||
"when": "view == wslTargets",
|
||||
"group": "navigation"
|
||||
},
|
||||
{
|
||||
"command": "openremotewsl.explorer.refresh",
|
||||
"when": "view == wslTargets",
|
||||
"group": "navigation"
|
||||
}
|
||||
],
|
||||
"view/item/context": [
|
||||
{
|
||||
"command": "openremotewsl.explorer.emptyWindowInNewWindow",
|
||||
"when": "viewItem =~ /^openremotewsl.explorer.distro$/",
|
||||
"group": "inline@1"
|
||||
},
|
||||
{
|
||||
"command": "openremotewsl.explorer.emptyWindowInNewWindow",
|
||||
"when": "viewItem =~ /^openremotewsl.explorer.distro$/",
|
||||
"group": "navigation@2"
|
||||
},
|
||||
{
|
||||
"command": "openremotewsl.explorer.emptyWindowInCurrentWindow",
|
||||
"when": "viewItem =~ /^openremotewsl.explorer.distro$/",
|
||||
"group": "navigation@1"
|
||||
},
|
||||
{
|
||||
"command": "openremotewsl.explorer.setDefaultDistro",
|
||||
"when": "viewItem =~ /^openremotewsl.explorer.distro$/",
|
||||
"group": "management@1"
|
||||
},
|
||||
{
|
||||
"command": "openremotewsl.explorer.deleteDistro",
|
||||
"when": "viewItem =~ /^openremotewsl.explorer.distro$/",
|
||||
"group": "management@2"
|
||||
},
|
||||
{
|
||||
"command": "openremotewsl.explorer.reopenFolderInNewWindow",
|
||||
"when": "viewItem == openremotewsl.explorer.folder",
|
||||
"group": "inline@1"
|
||||
},
|
||||
{
|
||||
"command": "openremotewsl.explorer.reopenFolderInNewWindow",
|
||||
"when": "viewItem == openremotewsl.explorer.folder",
|
||||
"group": "navigation@2"
|
||||
},
|
||||
{
|
||||
"command": "openremotewsl.explorer.reopenFolderInCurrentWindow",
|
||||
"when": "viewItem == openremotewsl.explorer.folder",
|
||||
"group": "navigation@1"
|
||||
},
|
||||
{
|
||||
"command": "openremotewsl.explorer.deleteFolderHistoryItem",
|
||||
"when": "viewItem =~ /^openremotewsl.explorer.folder/",
|
||||
"group": "navigation@3"
|
||||
},
|
||||
{
|
||||
"command": "openremotewsl.explorer.deleteFolderHistoryItem",
|
||||
"when": "viewItem =~ /^openremotewsl.explorer.folder/",
|
||||
"group": "inline@2"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"vscode:prepublish": "npm run compile",
|
||||
"compile": "gulp compile-extension:open-remote-wsl",
|
||||
"compile-web": "npx webpack-cli --config extension-browser.webpack.config --mode none",
|
||||
"watch": "gulp watch-extension:open-remote-wsl",
|
||||
"watch-web": "npx webpack-cli --config extension-browser.webpack.config --mode none --watch --info-verbosity verbose"
|
||||
}
|
||||
}
|
||||
121
extensions/open-remote-wsl/src/authResolver.ts
Normal file
121
extensions/open-remote-wsl/src/authResolver.ts
Normal file
|
|
@ -0,0 +1,121 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as vscode from 'vscode';
|
||||
import Log from './common/logger';
|
||||
import { installCodeServer, ServerInstallError } from './serverSetup';
|
||||
import { WSLManager } from './wsl/wslManager';
|
||||
|
||||
export const REMOTE_WSL_AUTHORITY = 'wsl';
|
||||
|
||||
export function getRemoteAuthority(distro: string) {
|
||||
return `${REMOTE_WSL_AUTHORITY}+${distro}`;
|
||||
}
|
||||
|
||||
class Tunnel implements vscode.Tunnel {
|
||||
private _onDidDisposeEmitter = new vscode.EventEmitter<void>();
|
||||
|
||||
readonly onDidDispose = this._onDidDisposeEmitter.event;
|
||||
|
||||
constructor(
|
||||
readonly remoteAddress: { port: number; host: string },
|
||||
readonly localAddress: { port: number; host: string }
|
||||
) {
|
||||
// If ipv6 localhost 0:0:0:0:0:0:0:1 or [::1] replace with localhost
|
||||
if (localAddress.host !== 'localhost' && localAddress.host !== '127.0.0.1') {
|
||||
localAddress.host = 'localhost';
|
||||
}
|
||||
}
|
||||
|
||||
dispose() {
|
||||
this._onDidDisposeEmitter.fire();
|
||||
}
|
||||
}
|
||||
|
||||
export class RemoteWSLResolver implements vscode.RemoteAuthorityResolver, vscode.Disposable {
|
||||
|
||||
private labelFormatterDisposable: vscode.Disposable | undefined;
|
||||
|
||||
constructor(
|
||||
private readonly wslManager: WSLManager,
|
||||
private readonly logger: Log
|
||||
) {
|
||||
}
|
||||
|
||||
resolve(authority: string, context: vscode.RemoteAuthorityResolverContext): Thenable<vscode.ResolverResult> {
|
||||
const [type, distroName] = authority.split('+');
|
||||
if (type !== REMOTE_WSL_AUTHORITY) {
|
||||
throw new Error(`Invalid authority type for WSL resolver: ${type}`);
|
||||
}
|
||||
|
||||
this.logger.info(`Resolving wsl remote authority '${authority}' (attemp #${context.resolveAttempt})`);
|
||||
|
||||
// It looks like default values are not loaded yet when resolving a remote,
|
||||
// so let's hardcode the default values here
|
||||
const remoteSSHconfig = vscode.workspace.getConfiguration('remote.WSL');
|
||||
const serverDownloadUrlTemplate = remoteSSHconfig.get<string>('serverDownloadUrlTemplate');
|
||||
|
||||
return vscode.window.withProgress({
|
||||
title: `Setting up WSL Distro: ${distroName}`,
|
||||
location: vscode.ProgressLocation.Notification,
|
||||
cancellable: false
|
||||
}, async () => {
|
||||
try {
|
||||
const installResult = await installCodeServer(this.wslManager, distroName, serverDownloadUrlTemplate, [], [], this.logger);
|
||||
|
||||
this.labelFormatterDisposable?.dispose();
|
||||
this.labelFormatterDisposable = vscode.workspace.registerResourceLabelFormatter({
|
||||
scheme: 'vscode-remote',
|
||||
authority: `${REMOTE_WSL_AUTHORITY}+*`,
|
||||
formatting: {
|
||||
label: '${path}',
|
||||
separator: '/',
|
||||
tildify: true,
|
||||
workspaceSuffix: `WSL: ${distroName}`,
|
||||
workspaceTooltip: `Running in ${distroName}`
|
||||
}
|
||||
});
|
||||
|
||||
return new vscode.ResolvedAuthority('127.0.0.1', installResult.listeningOn, installResult.connectionToken);
|
||||
} catch (e: unknown) {
|
||||
this.logger.error(`Error resolving authority`, e);
|
||||
|
||||
// Initial connection
|
||||
if (context.resolveAttempt === 1) {
|
||||
this.logger.show();
|
||||
|
||||
const closeRemote = 'Close Remote';
|
||||
const retry = 'Retry';
|
||||
const result = await vscode.window.showErrorMessage(`Could not establish connection to WSL distro "${distroName}"`, { modal: true }, closeRemote, retry);
|
||||
if (result === closeRemote) {
|
||||
await vscode.commands.executeCommand('workbench.action.remote.close');
|
||||
} else if (result === retry) {
|
||||
await vscode.commands.executeCommand('workbench.action.reloadWindow');
|
||||
}
|
||||
}
|
||||
|
||||
if (e instanceof ServerInstallError || !(e instanceof Error)) {
|
||||
throw vscode.RemoteAuthorityResolverError.NotAvailable(e instanceof Error ? e.message : String(e));
|
||||
} else {
|
||||
throw vscode.RemoteAuthorityResolverError.TemporarilyNotAvailable(e.message);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async tunnelFactory(tunnelOptions: vscode.TunnelOptions) {
|
||||
return new Tunnel(
|
||||
tunnelOptions.remoteAddress,
|
||||
{
|
||||
host: tunnelOptions.remoteAddress.host,
|
||||
port: tunnelOptions.localAddressPort ?? tunnelOptions.remoteAddress.port
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
dispose() {
|
||||
this.labelFormatterDisposable?.dispose();
|
||||
}
|
||||
}
|
||||
86
extensions/open-remote-wsl/src/commands.ts
Normal file
86
extensions/open-remote-wsl/src/commands.ts
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as vscode from 'vscode';
|
||||
import { getRemoteAuthority } from './authResolver';
|
||||
import { WSLDistro, WSLManager, WSLOnlineDistro } from './wsl/wslManager';
|
||||
import wslTerminal from './wsl/wslTerminal';
|
||||
|
||||
async function showDistrosPicker(wslManager: WSLManager, placeHolder: string): Promise<WSLDistro | undefined> {
|
||||
const pickItemsPromise = wslManager.listDistros()
|
||||
.then(distros => distros.map(distroData => {
|
||||
return {
|
||||
...distroData,
|
||||
label: `${distroData.name}`,
|
||||
detail: distroData.isDefault ? 'default distro' : undefined,
|
||||
};
|
||||
}));
|
||||
|
||||
const picked = await vscode.window.showQuickPick(pickItemsPromise, { canPickMany: false, placeHolder });
|
||||
return picked;
|
||||
}
|
||||
|
||||
async function showOnlineDistrosPicker(wslManager: WSLManager, placeHolder: string): Promise<WSLOnlineDistro | undefined> {
|
||||
const pickItemsPromise = Promise.all([wslManager.listOnlineDistros(), wslManager.listDistros()])
|
||||
.then(([onlineDistros, localDistros]) => {
|
||||
const distroToInstall = onlineDistros.filter(d => !localDistros.some(l => l.name === d.name));
|
||||
return distroToInstall.map(distroData => {
|
||||
return {
|
||||
...distroData,
|
||||
label: `${distroData.friendlyName}`,
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
const picked = await vscode.window.showQuickPick(pickItemsPromise, { canPickMany: false, placeHolder });
|
||||
return picked;
|
||||
}
|
||||
|
||||
export async function promptOpenRemoteWSLWindow(wslManager: WSLManager, useDefault: boolean, reuseWindow: boolean) {
|
||||
let distroName: string | undefined;
|
||||
if (useDefault) {
|
||||
const distros = await wslManager.listDistros();
|
||||
distroName = distros.find(distro => distro.isDefault)?.name;
|
||||
} else {
|
||||
distroName = (await showDistrosPicker(wslManager, 'Select WSL distro'))?.name;
|
||||
}
|
||||
|
||||
if (!distroName) {
|
||||
return;
|
||||
}
|
||||
|
||||
openRemoteWSLWindow(distroName, reuseWindow);
|
||||
}
|
||||
|
||||
export async function promptInstallNewWSLDistro(wslManager: WSLManager) {
|
||||
const distroName = (await showOnlineDistrosPicker(wslManager, 'Select the WSL distro to install'))?.name;
|
||||
if (!distroName) {
|
||||
return;
|
||||
}
|
||||
|
||||
wslTerminal.runCommand(`wsl.exe --install -d ${distroName}`);
|
||||
}
|
||||
|
||||
export function openRemoteWSLWindow(distro: string, reuseWindow: boolean) {
|
||||
vscode.commands.executeCommand('vscode.newWindow', { remoteAuthority: getRemoteAuthority(distro), reuseWindow });
|
||||
}
|
||||
|
||||
export function openRemoteWSLLocationWindow(distro: string, path: string, reuseWindow: boolean) {
|
||||
vscode.commands.executeCommand('vscode.openFolder', vscode.Uri.from({ scheme: 'vscode-remote', authority: getRemoteAuthority(distro), path }), { forceNewWindow: !reuseWindow });
|
||||
}
|
||||
|
||||
export async function setDefaultWSLDistro(wslManager: WSLManager, distroName: string) {
|
||||
await wslManager.setDefaultDistro(distroName);
|
||||
}
|
||||
|
||||
export async function deleteWSLDistro(wslManager: WSLManager, distroName: string) {
|
||||
const deleteAction = 'Delete';
|
||||
const resp = await vscode.window.showInformationMessage(`Are you sure you want to permanently delete the distro "${distroName}" including all its data?`, { modal: true }, deleteAction);
|
||||
if (resp === deleteAction) {
|
||||
await wslManager.deleteDistro(distroName);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
28
extensions/open-remote-wsl/src/common/async.ts
Normal file
28
extensions/open-remote-wsl/src/common/async.ts
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
export function timeout(millis: number): Promise<void> {
|
||||
return new Promise((resolve) => setTimeout(resolve, millis));
|
||||
}
|
||||
|
||||
export interface ITask<T> {
|
||||
(): T;
|
||||
}
|
||||
|
||||
export async function retry<T>(task: ITask<Promise<T>>, delay: number, retries: number): Promise<T> {
|
||||
let lastError: Error | undefined;
|
||||
|
||||
for (let i = 0; i < retries; i++) {
|
||||
try {
|
||||
return await task();
|
||||
} catch (error) {
|
||||
lastError = error;
|
||||
|
||||
await timeout(delay);
|
||||
}
|
||||
}
|
||||
|
||||
throw lastError;
|
||||
}
|
||||
42
extensions/open-remote-wsl/src/common/disposable.ts
Normal file
42
extensions/open-remote-wsl/src/common/disposable.ts
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as vscode from 'vscode';
|
||||
|
||||
export function disposeAll(disposables: vscode.Disposable[]): void {
|
||||
while (disposables.length) {
|
||||
const item = disposables.pop();
|
||||
if (item) {
|
||||
item.dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export abstract class Disposable {
|
||||
private _isDisposed = false;
|
||||
|
||||
protected _disposables: vscode.Disposable[] = [];
|
||||
|
||||
public dispose(): any {
|
||||
if (this._isDisposed) {
|
||||
return;
|
||||
}
|
||||
this._isDisposed = true;
|
||||
disposeAll(this._disposables);
|
||||
}
|
||||
|
||||
protected _register<T extends vscode.Disposable>(value: T): T {
|
||||
if (this._isDisposed) {
|
||||
value.dispose();
|
||||
} else {
|
||||
this._disposables.push(value);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
protected get isDisposed(): boolean {
|
||||
return this._isDisposed;
|
||||
}
|
||||
}
|
||||
142
extensions/open-remote-wsl/src/common/event.ts
Normal file
142
extensions/open-remote-wsl/src/common/event.ts
Normal file
|
|
@ -0,0 +1,142 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
export interface IDisposable {
|
||||
dispose(): void;
|
||||
}
|
||||
|
||||
export interface Event<T> {
|
||||
(listener: (e: T) => any, thisArgs?: any, disposables?: IDisposable[]): IDisposable;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a promise that resolves when the event fires, or when cancellation
|
||||
* is requested, whichever happens first.
|
||||
*/
|
||||
export function toPromise<T>(event: Event<T>): Promise<T>;
|
||||
export function toPromise<T>(event: Event<T>, signal: AbortSignal): Promise<T | undefined>;
|
||||
export function toPromise<T>(event: Event<T>, signal?: AbortSignal): Promise<T | undefined> {
|
||||
if (!signal) {
|
||||
return new Promise<T>((resolve) => once(event, resolve));
|
||||
}
|
||||
|
||||
if (signal.aborted) {
|
||||
return Promise.resolve(undefined);
|
||||
}
|
||||
|
||||
return new Promise((resolve) => {
|
||||
const d2 = once(event, (data) => {
|
||||
(signal as any).removeEventListener('abort', d1);
|
||||
resolve(data);
|
||||
});
|
||||
|
||||
const d1 = () => {
|
||||
d2.dispose();
|
||||
(signal as any).removeEventListener('abort', d1);
|
||||
resolve(undefined);
|
||||
};
|
||||
|
||||
(signal as any).addEventListener('abort', d1);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a handler that handles one event on the emitter, then disposes itself.
|
||||
*/
|
||||
export const once = <T>(event: Event<T>, listener: (data: T) => void): IDisposable => {
|
||||
const disposable = event((value) => {
|
||||
listener(value);
|
||||
disposable.dispose();
|
||||
});
|
||||
|
||||
return disposable;
|
||||
};
|
||||
|
||||
/**
|
||||
* Base event emitter. Calls listeners when data is emitted.
|
||||
*/
|
||||
export class EventEmitter<T> {
|
||||
private listeners?: Array<(data: T) => void> | ((data: T) => void);
|
||||
|
||||
/**
|
||||
* Event<T> function.
|
||||
*/
|
||||
public readonly event: Event<T> = (listener, thisArgs, disposables) => {
|
||||
const d = this.add(thisArgs ? listener.bind(thisArgs) : listener);
|
||||
disposables?.push(d);
|
||||
return d;
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets the number of event listeners.
|
||||
*/
|
||||
public get size() {
|
||||
if (!this.listeners) {
|
||||
return 0;
|
||||
} else if (typeof this.listeners === 'function') {
|
||||
return 1;
|
||||
} else {
|
||||
return this.listeners.length;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Emits event data.
|
||||
*/
|
||||
public fire(value: T) {
|
||||
if (!this.listeners) {
|
||||
// no-op
|
||||
} else if (typeof this.listeners === 'function') {
|
||||
this.listeners(value);
|
||||
} else {
|
||||
for (const listener of this.listeners) {
|
||||
listener(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Disposes of the emitter.
|
||||
*/
|
||||
public dispose() {
|
||||
this.listeners = undefined;
|
||||
}
|
||||
|
||||
private add(listener: (data: T) => void): IDisposable {
|
||||
if (!this.listeners) {
|
||||
this.listeners = listener;
|
||||
} else if (typeof this.listeners === 'function') {
|
||||
this.listeners = [this.listeners, listener];
|
||||
} else {
|
||||
this.listeners.push(listener);
|
||||
}
|
||||
|
||||
return { dispose: () => this.rm(listener) };
|
||||
}
|
||||
|
||||
private rm(listener: (data: T) => void) {
|
||||
if (!this.listeners) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof this.listeners === 'function') {
|
||||
if (this.listeners === listener) {
|
||||
this.listeners = undefined;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const index = this.listeners.indexOf(listener);
|
||||
if (index === -1) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.listeners.length === 2) {
|
||||
this.listeners = index === 0 ? this.listeners[1] : this.listeners[0];
|
||||
} else {
|
||||
this.listeners = this.listeners.slice(0, index).concat(this.listeners.slice(index + 1));
|
||||
}
|
||||
}
|
||||
}
|
||||
26
extensions/open-remote-wsl/src/common/files.ts
Normal file
26
extensions/open-remote-wsl/src/common/files.ts
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as fs from 'fs';
|
||||
import * as os from 'os';
|
||||
|
||||
const homeDir = os.homedir();
|
||||
|
||||
export async function exists(path: string) {
|
||||
try {
|
||||
await fs.promises.access(path);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export function untildify(path: string) {
|
||||
return path.replace(/^~(?=$|\/|\\)/, homeDir);
|
||||
}
|
||||
|
||||
export function normalizeToSlash(path: string) {
|
||||
return path.replace(/\\/g, '/');
|
||||
}
|
||||
64
extensions/open-remote-wsl/src/common/logger.ts
Normal file
64
extensions/open-remote-wsl/src/common/logger.ts
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as vscode from 'vscode';
|
||||
|
||||
type LogLevel = 'Trace' | 'Info' | 'Error';
|
||||
|
||||
export default class Log {
|
||||
private output: vscode.OutputChannel;
|
||||
|
||||
constructor(name: string) {
|
||||
this.output = vscode.window.createOutputChannel(name);
|
||||
}
|
||||
|
||||
private data2String(data: any): string {
|
||||
if (data instanceof Error) {
|
||||
return data.stack || data.message;
|
||||
}
|
||||
if (data.success === false && data.message) {
|
||||
return data.message;
|
||||
}
|
||||
return data.toString();
|
||||
}
|
||||
|
||||
public trace(message: string, data?: any): void {
|
||||
this.logLevel('Trace', message, data);
|
||||
}
|
||||
|
||||
public info(message: string, data?: any): void {
|
||||
this.logLevel('Info', message, data);
|
||||
}
|
||||
|
||||
public error(message: string, data?: any): void {
|
||||
this.logLevel('Error', message, data);
|
||||
}
|
||||
|
||||
public logLevel(level: LogLevel, message: string, data?: any): void {
|
||||
this.output.appendLine(`[${level} - ${this.now()}] ${message}`);
|
||||
if (data) {
|
||||
this.output.appendLine(this.data2String(data));
|
||||
}
|
||||
}
|
||||
|
||||
private now(): string {
|
||||
const now = new Date();
|
||||
return padLeft(now.getUTCHours() + '', 2, '0')
|
||||
+ ':' + padLeft(now.getMinutes() + '', 2, '0')
|
||||
+ ':' + padLeft(now.getUTCSeconds() + '', 2, '0') + '.' + now.getMilliseconds();
|
||||
}
|
||||
|
||||
public show() {
|
||||
this.output.show();
|
||||
}
|
||||
|
||||
public dispose() {
|
||||
this.output.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
function padLeft(s: string, n: number, pad = ' ') {
|
||||
return pad.repeat(Math.max(0, n - s.length)) + s;
|
||||
}
|
||||
8
extensions/open-remote-wsl/src/common/platform.ts
Normal file
8
extensions/open-remote-wsl/src/common/platform.ts
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
export const isWindows = process.platform === 'win32';
|
||||
export const isMacintosh = process.platform === 'darwin';
|
||||
export const isLinux = process.platform === 'linux';
|
||||
134
extensions/open-remote-wsl/src/common/ports.ts
Normal file
134
extensions/open-remote-wsl/src/common/ports.ts
Normal file
|
|
@ -0,0 +1,134 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as net from 'net';
|
||||
|
||||
/**
|
||||
* Finds a random unused port assigned by the operating system. Will reject in case no free port can be found.
|
||||
*/
|
||||
export function findRandomPort(): Promise<number> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const server = net.createServer({ pauseOnConnect: true });
|
||||
server.on('error', reject);
|
||||
server.on('listening', () => {
|
||||
const port = (server.address() as net.AddressInfo).port;
|
||||
server.close(() => resolve(port));
|
||||
});
|
||||
server.listen(0, '127.0.0.1');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a start point and a max number of retries, will find a port that
|
||||
* is openable. Will return 0 in case no free port can be found.
|
||||
*/
|
||||
export function findFreePort(startPort: number, giveUpAfter: number, timeout: number, stride = 1): Promise<number> {
|
||||
let done = false;
|
||||
|
||||
return new Promise(resolve => {
|
||||
const timeoutHandle = setTimeout(() => {
|
||||
if (!done) {
|
||||
done = true;
|
||||
return resolve(0);
|
||||
}
|
||||
}, timeout);
|
||||
|
||||
doFindFreePort(startPort, giveUpAfter, stride, (port) => {
|
||||
if (!done) {
|
||||
done = true;
|
||||
clearTimeout(timeoutHandle);
|
||||
return resolve(port);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function doFindFreePort(startPort: number, giveUpAfter: number, stride: number, clb: (port: number) => void): void {
|
||||
if (giveUpAfter === 0) {
|
||||
return clb(0);
|
||||
}
|
||||
|
||||
const client = new net.Socket();
|
||||
|
||||
// If we can connect to the port it means the port is already taken so we continue searching
|
||||
client.once('connect', () => {
|
||||
dispose(client);
|
||||
|
||||
return doFindFreePort(startPort + stride, giveUpAfter - 1, stride, clb);
|
||||
});
|
||||
|
||||
client.once('data', () => {
|
||||
// this listener is required since node.js 8.x
|
||||
});
|
||||
|
||||
client.once('error', (err: Error & { code?: string }) => {
|
||||
dispose(client);
|
||||
|
||||
// If we receive any non ECONNREFUSED error, it means the port is used but we cannot connect
|
||||
if (err.code !== 'ECONNREFUSED') {
|
||||
return doFindFreePort(startPort + stride, giveUpAfter - 1, stride, clb);
|
||||
}
|
||||
|
||||
// Otherwise it means the port is free to use!
|
||||
return clb(startPort);
|
||||
});
|
||||
|
||||
client.connect(startPort, '127.0.0.1');
|
||||
}
|
||||
|
||||
/**
|
||||
* Uses listen instead of connect. Is faster, but if there is another listener on 0.0.0.0 then this will take 127.0.0.1 from that listener.
|
||||
*/
|
||||
export function findFreePortFaster(startPort: number, giveUpAfter: number, timeout: number): Promise<number> {
|
||||
let resolved = false;
|
||||
let timeoutHandle: NodeJS.Timeout | undefined = undefined;
|
||||
let countTried = 1;
|
||||
const server = net.createServer({ pauseOnConnect: true });
|
||||
function doResolve(port: number, resolve: (port: number) => void) {
|
||||
if (!resolved) {
|
||||
resolved = true;
|
||||
server.removeAllListeners();
|
||||
server.close();
|
||||
if (timeoutHandle) {
|
||||
clearTimeout(timeoutHandle);
|
||||
}
|
||||
resolve(port);
|
||||
}
|
||||
}
|
||||
return new Promise<number>(resolve => {
|
||||
timeoutHandle = setTimeout(() => {
|
||||
doResolve(0, resolve);
|
||||
}, timeout);
|
||||
|
||||
server.on('listening', () => {
|
||||
doResolve(startPort, resolve);
|
||||
});
|
||||
server.on('error', err => {
|
||||
if (err && ((<any>err).code === 'EADDRINUSE' || (<any>err).code === 'EACCES') && (countTried < giveUpAfter)) {
|
||||
startPort++;
|
||||
countTried++;
|
||||
server.listen(startPort, '127.0.0.1');
|
||||
} else {
|
||||
doResolve(0, resolve);
|
||||
}
|
||||
});
|
||||
server.on('close', () => {
|
||||
doResolve(0, resolve);
|
||||
});
|
||||
server.listen(startPort, '127.0.0.1');
|
||||
});
|
||||
}
|
||||
|
||||
function dispose(socket: net.Socket): void {
|
||||
try {
|
||||
socket.removeAllListeners('connect');
|
||||
socket.removeAllListeners('error');
|
||||
socket.end();
|
||||
socket.destroy();
|
||||
socket.unref();
|
||||
} catch (error) {
|
||||
console.error(error); // otherwise this error would get lost in the callback chain
|
||||
}
|
||||
}
|
||||
109
extensions/open-remote-wsl/src/distroTreeView.ts
Normal file
109
extensions/open-remote-wsl/src/distroTreeView.ts
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as vscode from 'vscode';
|
||||
import * as path from 'path';
|
||||
import { RemoteLocationHistory } from './remoteLocationHistory';
|
||||
import { Disposable } from './common/disposable';
|
||||
import { openRemoteWSLWindow, openRemoteWSLLocationWindow, promptInstallNewWSLDistro, deleteWSLDistro, setDefaultWSLDistro } from './commands';
|
||||
import { WSLManager } from './wsl/wslManager';
|
||||
|
||||
class DistroItem {
|
||||
constructor(
|
||||
public name: string,
|
||||
public isDefault: boolean,
|
||||
public locations: string[]
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
||||
class DistroLocationItem {
|
||||
constructor(
|
||||
public path: string,
|
||||
public name: string
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
||||
type DataTreeItem = DistroItem | DistroLocationItem;
|
||||
|
||||
export class DistroTreeDataProvider extends Disposable implements vscode.TreeDataProvider<DataTreeItem> {
|
||||
|
||||
private readonly _onDidChangeTreeData = this._register(new vscode.EventEmitter<DataTreeItem | DataTreeItem[] | void>());
|
||||
public readonly onDidChangeTreeData = this._onDidChangeTreeData.event;
|
||||
|
||||
constructor(
|
||||
private readonly locationHistory: RemoteLocationHistory,
|
||||
private readonly wslManager: WSLManager
|
||||
) {
|
||||
super();
|
||||
|
||||
this._register(vscode.commands.registerCommand('openremotewsl.explorer.addDistro', () => promptInstallNewWSLDistro(wslManager)));
|
||||
this._register(vscode.commands.registerCommand('openremotewsl.explorer.refresh', () => this.refresh()));
|
||||
this._register(vscode.commands.registerCommand('openremotewsl.explorer.emptyWindowInNewWindow', e => this.openRemoteWSLWindow(e, false)));
|
||||
this._register(vscode.commands.registerCommand('openremotewsl.explorer.emptyWindowInCurrentWindow', e => this.openRemoteWSLWindow(e, true)));
|
||||
this._register(vscode.commands.registerCommand('openremotewsl.explorer.reopenFolderInNewWindow', e => this.openRemoteWSLocationWindow(e, false)));
|
||||
this._register(vscode.commands.registerCommand('openremotewsl.explorer.reopenFolderInCurrentWindow', e => this.openRemoteWSLocationWindow(e, true)));
|
||||
this._register(vscode.commands.registerCommand('openremotewsl.explorer.deleteFolderHistoryItem', e => this.deleteDistroLocation(e)));
|
||||
this._register(vscode.commands.registerCommand('openremotewsl.explorer.setDefaultDistro', e => this.setDefaultDistro(e)));
|
||||
this._register(vscode.commands.registerCommand('openremotewsl.explorer.deleteDistro', e => this.deleteDistro(e)));
|
||||
}
|
||||
|
||||
getTreeItem(element: DataTreeItem): vscode.TreeItem {
|
||||
if (element instanceof DistroLocationItem) {
|
||||
const label = path.posix.basename(element.path).replace(/\.code-workspace$/, ' (Workspace)');
|
||||
const treeItem = new vscode.TreeItem(label);
|
||||
treeItem.description = path.posix.dirname(element.path);
|
||||
treeItem.iconPath = new vscode.ThemeIcon('folder');
|
||||
treeItem.contextValue = 'openremotewsl.explorer.folder';
|
||||
return treeItem;
|
||||
}
|
||||
|
||||
const treeItem = new vscode.TreeItem(element.name);
|
||||
treeItem.description = element.isDefault ? 'default distro' : undefined;
|
||||
treeItem.collapsibleState = element.locations.length ? vscode.TreeItemCollapsibleState.Collapsed : vscode.TreeItemCollapsibleState.None;
|
||||
treeItem.iconPath = new vscode.ThemeIcon('vm');
|
||||
treeItem.contextValue = 'openremotewsl.explorer.distro';
|
||||
return treeItem;
|
||||
}
|
||||
|
||||
async getChildren(element?: DistroItem): Promise<DataTreeItem[]> {
|
||||
if (!element) {
|
||||
const distros = await this.wslManager.listDistros();
|
||||
return distros.map(distro => new DistroItem(distro.name, distro.isDefault, this.locationHistory.getHistory(distro.name)));
|
||||
}
|
||||
if (element instanceof DistroItem) {
|
||||
return element.locations.map(location => new DistroLocationItem(location, element.name));
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
private refresh() {
|
||||
this._onDidChangeTreeData.fire();
|
||||
}
|
||||
|
||||
private async deleteDistroLocation(element: DistroLocationItem) {
|
||||
await this.locationHistory.removeLocation(element.name, element.path);
|
||||
this.refresh();
|
||||
}
|
||||
|
||||
private async openRemoteWSLWindow(element: DistroItem, reuseWindow: boolean) {
|
||||
openRemoteWSLWindow(element.name, reuseWindow);
|
||||
}
|
||||
|
||||
private async openRemoteWSLocationWindow(element: DistroLocationItem, reuseWindow: boolean) {
|
||||
openRemoteWSLLocationWindow(element.name, element.path, reuseWindow);
|
||||
}
|
||||
|
||||
private async setDefaultDistro(element: DistroItem) {
|
||||
await setDefaultWSLDistro(this.wslManager, element.name);
|
||||
this.refresh();
|
||||
}
|
||||
|
||||
private async deleteDistro(element: DistroItem) {
|
||||
await deleteWSLDistro(this.wslManager, element.name);
|
||||
this.refresh();
|
||||
}
|
||||
}
|
||||
46
extensions/open-remote-wsl/src/extension.ts
Normal file
46
extensions/open-remote-wsl/src/extension.ts
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as vscode from 'vscode';
|
||||
import Log from './common/logger';
|
||||
import { RemoteWSLResolver, REMOTE_WSL_AUTHORITY } from './authResolver';
|
||||
import { promptOpenRemoteWSLWindow } from './commands';
|
||||
import { DistroTreeDataProvider } from './distroTreeView';
|
||||
import { getRemoteWorkspaceLocationData, RemoteLocationHistory } from './remoteLocationHistory';
|
||||
import { WSLManager } from './wsl/wslManager';
|
||||
import { isWindows } from './common/platform';
|
||||
|
||||
export async function activate(context: vscode.ExtensionContext) {
|
||||
if (!isWindows) {
|
||||
return;
|
||||
}
|
||||
|
||||
const logger = new Log('Remote - WSL');
|
||||
context.subscriptions.push(logger);
|
||||
|
||||
const wslManager = new WSLManager(logger);
|
||||
const remoteWSLResolver = new RemoteWSLResolver(wslManager, logger);
|
||||
context.subscriptions.push(vscode.workspace.registerRemoteAuthorityResolver(REMOTE_WSL_AUTHORITY, remoteWSLResolver));
|
||||
context.subscriptions.push(remoteWSLResolver);
|
||||
|
||||
const locationHistory = new RemoteLocationHistory(context);
|
||||
const locationData = getRemoteWorkspaceLocationData();
|
||||
if (locationData) {
|
||||
await locationHistory.addLocation(locationData[0], locationData[1]);
|
||||
}
|
||||
|
||||
const distroTreeDataProvider = new DistroTreeDataProvider(locationHistory, wslManager);
|
||||
context.subscriptions.push(vscode.window.createTreeView('wslTargets', { treeDataProvider: distroTreeDataProvider }));
|
||||
context.subscriptions.push(distroTreeDataProvider);
|
||||
|
||||
context.subscriptions.push(vscode.commands.registerCommand('openremotewsl.connect', () => promptOpenRemoteWSLWindow(wslManager, true, true)));
|
||||
context.subscriptions.push(vscode.commands.registerCommand('openremotewsl.connectInNewWindow', () => promptOpenRemoteWSLWindow(wslManager, true, false)));
|
||||
context.subscriptions.push(vscode.commands.registerCommand('openremotewsl.connectUsingDistro', () => promptOpenRemoteWSLWindow(wslManager, false, true)));
|
||||
context.subscriptions.push(vscode.commands.registerCommand('openremotewsl.connectUsingDistroInNewWindow', () => promptOpenRemoteWSLWindow(wslManager, false, false)));
|
||||
context.subscriptions.push(vscode.commands.registerCommand('openremotewsl.showLog', () => logger.show()));
|
||||
}
|
||||
|
||||
export function deactivate() {
|
||||
}
|
||||
56
extensions/open-remote-wsl/src/remoteLocationHistory.ts
Normal file
56
extensions/open-remote-wsl/src/remoteLocationHistory.ts
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as vscode from 'vscode';
|
||||
import { REMOTE_WSL_AUTHORITY } from './authResolver';
|
||||
|
||||
export class RemoteLocationHistory {
|
||||
private static STORAGE_KEY = 'remoteLocationHistory_v0';
|
||||
|
||||
private remoteLocationHistory: Record<string, string[]> = {};
|
||||
|
||||
constructor(private context: vscode.ExtensionContext) {
|
||||
// context.globalState.update(RemoteLocationHistory.STORAGE_KEY, undefined);
|
||||
this.remoteLocationHistory = context.globalState.get(RemoteLocationHistory.STORAGE_KEY) || {};
|
||||
}
|
||||
|
||||
getHistory(host: string): string[] {
|
||||
return this.remoteLocationHistory[host] || [];
|
||||
}
|
||||
|
||||
async addLocation(host: string, path: string) {
|
||||
const hostLocations = this.remoteLocationHistory[host] || [];
|
||||
if (!hostLocations.includes(path)) {
|
||||
hostLocations.unshift(path);
|
||||
this.remoteLocationHistory[host] = hostLocations;
|
||||
|
||||
await this.context.globalState.update(RemoteLocationHistory.STORAGE_KEY, this.remoteLocationHistory);
|
||||
}
|
||||
}
|
||||
|
||||
async removeLocation(host: string, path: string) {
|
||||
let hostLocations = this.remoteLocationHistory[host] || [];
|
||||
hostLocations = hostLocations.filter(l => l !== path);
|
||||
this.remoteLocationHistory[host] = hostLocations;
|
||||
|
||||
await this.context.globalState.update(RemoteLocationHistory.STORAGE_KEY, this.remoteLocationHistory);
|
||||
}
|
||||
}
|
||||
|
||||
export function getRemoteWorkspaceLocationData(): [string, string] | undefined {
|
||||
let location = vscode.workspace.workspaceFile;
|
||||
if (location && location.scheme === 'vscode-remote' && location.authority.startsWith(REMOTE_WSL_AUTHORITY) && location.path.endsWith('.code-workspace')) {
|
||||
const [, distroName] = location.authority.split('+');
|
||||
return [distroName, location.path];
|
||||
}
|
||||
|
||||
location = vscode.workspace.workspaceFolders?.[0].uri;
|
||||
if (location && location.scheme === 'vscode-remote' && location.authority.startsWith(REMOTE_WSL_AUTHORITY)) {
|
||||
const [, distroName] = location.authority.split('+');
|
||||
return [distroName, location.path];
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
42
extensions/open-remote-wsl/src/serverConfig.ts
Normal file
42
extensions/open-remote-wsl/src/serverConfig.ts
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as vscode from 'vscode';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
let vscodeProductJson: any;
|
||||
async function getVSCodeProductJson() {
|
||||
if (!vscodeProductJson) {
|
||||
const productJsonStr = await fs.promises.readFile(path.join(vscode.env.appRoot, 'product.json'), 'utf8');
|
||||
vscodeProductJson = JSON.parse(productJsonStr);
|
||||
}
|
||||
|
||||
return vscodeProductJson;
|
||||
}
|
||||
|
||||
export interface IServerConfig {
|
||||
version: string;
|
||||
commit: string;
|
||||
quality: string;
|
||||
release?: string; // void-like specific
|
||||
serverApplicationName: string;
|
||||
serverDataFolderName: string;
|
||||
serverDownloadUrlTemplate?: string; // void-like specific
|
||||
}
|
||||
|
||||
export async function getVSCodeServerConfig(): Promise<IServerConfig> {
|
||||
const productJson = await getVSCodeProductJson();
|
||||
|
||||
return {
|
||||
version: vscode.version.replace('-insider', ''),
|
||||
commit: productJson.commit,
|
||||
quality: productJson.quality,
|
||||
release: productJson.release,
|
||||
serverApplicationName: productJson.serverApplicationName,
|
||||
serverDataFolderName: productJson.serverDataFolderName,
|
||||
serverDownloadUrlTemplate: productJson.serverDownloadUrlTemplate
|
||||
};
|
||||
}
|
||||
353
extensions/open-remote-wsl/src/serverSetup.ts
Normal file
353
extensions/open-remote-wsl/src/serverSetup.ts
Normal file
|
|
@ -0,0 +1,353 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as crypto from 'crypto';
|
||||
import Log from './common/logger';
|
||||
import { getVSCodeServerConfig } from './serverConfig';
|
||||
import { WSLManager } from './wsl/wslManager';
|
||||
|
||||
export interface ServerInstallOptions {
|
||||
id: string;
|
||||
quality: string;
|
||||
commit: string;
|
||||
version: string;
|
||||
release?: string; // void specific
|
||||
extensionIds: string[];
|
||||
envVariables: string[];
|
||||
serverApplicationName: string;
|
||||
serverDataFolderName: string;
|
||||
serverDownloadUrlTemplate: string;
|
||||
}
|
||||
|
||||
export interface ServerInstallResult {
|
||||
exitCode: number;
|
||||
listeningOn: number;
|
||||
connectionToken: string;
|
||||
logFile: string;
|
||||
osReleaseId: string;
|
||||
arch: string;
|
||||
platform: string;
|
||||
tmpDir: string;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
export class ServerInstallError extends Error {
|
||||
constructor(message: string) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
|
||||
const DEFAULT_DOWNLOAD_URL_TEMPLATE = 'https://github.com/codestoryai/binaries/releases/download/${version}.${release}/void-reh-${os}-${arch}-${version}.${release}.tar.gz';
|
||||
|
||||
export async function installCodeServer(wslManager: WSLManager, distroName: string, serverDownloadUrlTemplate: string | undefined, extensionIds: string[], envVariables: string[], logger: Log): Promise<ServerInstallResult> {
|
||||
const scriptId = crypto.randomBytes(12).toString('hex');
|
||||
|
||||
const vscodeServerConfig = await getVSCodeServerConfig();
|
||||
const installOptions: ServerInstallOptions = {
|
||||
id: scriptId,
|
||||
version: vscodeServerConfig.version,
|
||||
commit: vscodeServerConfig.commit,
|
||||
quality: vscodeServerConfig.quality,
|
||||
release: vscodeServerConfig.release,
|
||||
extensionIds,
|
||||
envVariables,
|
||||
serverApplicationName: vscodeServerConfig.serverApplicationName,
|
||||
serverDataFolderName: vscodeServerConfig.serverDataFolderName,
|
||||
serverDownloadUrlTemplate: serverDownloadUrlTemplate ?? vscodeServerConfig.serverDownloadUrlTemplate ?? DEFAULT_DOWNLOAD_URL_TEMPLATE,
|
||||
};
|
||||
|
||||
const installServerScript = generateBashInstallScript(installOptions);
|
||||
|
||||
// Fish shell does not support heredoc so let's workaround it using -c option,
|
||||
// also replace single quotes (') within the script with ('\'') as there's no quoting within single quotes, see https://unix.stackexchange.com/a/24676
|
||||
const resp = await wslManager.exec('bash', ['-c', `'${installServerScript.replace(/'/g, `'\\''`)}'`], distroName);
|
||||
|
||||
const endScriptRegex = new RegExp(`${scriptId}: Server installation script done`, 'm');
|
||||
const commandOutput = await Promise.race([
|
||||
resp.exitPromise.then(result => ({ stdout: resp.stdout, stderr: resp.stderr, exitCode: result.exitCode })),
|
||||
new Promise<{ stdout: string; stderr: string; exitCode: number }>((resolve) => {
|
||||
resp.onStdoutData(buffer => {
|
||||
if (endScriptRegex.test(buffer.toString('utf8'))) {
|
||||
resolve({ stdout: resp.stdout, stderr: resp.stderr, exitCode: 0 });
|
||||
}
|
||||
});
|
||||
})
|
||||
]);
|
||||
|
||||
if (commandOutput.exitCode) {
|
||||
logger.trace('Server install command stderr:', commandOutput.stderr);
|
||||
}
|
||||
logger.trace('Server install command stdout:', commandOutput.stdout);
|
||||
|
||||
const resultMap = parseServerInstallOutput(commandOutput.stdout, scriptId);
|
||||
if (!resultMap) {
|
||||
throw new ServerInstallError(`Failed parsing install script output`);
|
||||
}
|
||||
|
||||
const exitCode = parseInt(resultMap.exitCode, 10);
|
||||
if (exitCode !== 0) {
|
||||
throw new ServerInstallError(`Couldn't install void server on remote server, install script returned non-zero exit status`);
|
||||
}
|
||||
|
||||
const listeningOn = parseInt(resultMap.listeningOn, 10);
|
||||
|
||||
const remoteEnvVars = Object.fromEntries(Object.entries(resultMap).filter(([key,]) => envVariables.includes(key)));
|
||||
|
||||
return {
|
||||
exitCode,
|
||||
listeningOn,
|
||||
connectionToken: resultMap.connectionToken,
|
||||
logFile: resultMap.logFile,
|
||||
osReleaseId: resultMap.osReleaseId,
|
||||
arch: resultMap.arch,
|
||||
platform: resultMap.platform,
|
||||
tmpDir: resultMap.tmpDir,
|
||||
...remoteEnvVars
|
||||
};
|
||||
}
|
||||
|
||||
function parseServerInstallOutput(str: string, scriptId: string): { [k: string]: string } | undefined {
|
||||
const startResultStr = `${scriptId}: start`;
|
||||
const endResultStr = `${scriptId}: end`;
|
||||
|
||||
const startResultIdx = str.indexOf(startResultStr);
|
||||
if (startResultIdx < 0) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const endResultIdx = str.indexOf(endResultStr, startResultIdx + startResultStr.length);
|
||||
if (endResultIdx < 0) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const installResult = str.substring(startResultIdx + startResultStr.length, endResultIdx);
|
||||
|
||||
const resultMap: { [k: string]: string } = {};
|
||||
const resultArr = installResult.split(/\r?\n/);
|
||||
for (const line of resultArr) {
|
||||
const [key, value] = line.split('==');
|
||||
resultMap[key] = value;
|
||||
}
|
||||
|
||||
return resultMap;
|
||||
}
|
||||
|
||||
function generateBashInstallScript({ id, quality, version, commit, release, extensionIds, envVariables, serverApplicationName, serverDataFolderName, serverDownloadUrlTemplate }: ServerInstallOptions) {
|
||||
const extensions = extensionIds.map(id => '--install-extension ' + id).join(' ');
|
||||
return `
|
||||
# Server installation script
|
||||
|
||||
TMP_DIR="\${XDG_RUNTIME_DIR:-"/tmp"}"
|
||||
|
||||
DISTRO_VERSION="${version}"
|
||||
DISTRO_COMMIT="${commit}"
|
||||
DISTRO_QUALITY="${quality}"
|
||||
DISTRO_VSCODIUM_RELEASE="${release ?? ''}"
|
||||
|
||||
SERVER_APP_NAME="${serverApplicationName}"
|
||||
SERVER_INITIAL_EXTENSIONS="${extensions}"
|
||||
SERVER_LISTEN_FLAG="--port=0"
|
||||
SERVER_DATA_DIR="$HOME/${serverDataFolderName}"
|
||||
SERVER_DIR="$SERVER_DATA_DIR/bin/$DISTRO_COMMIT"
|
||||
SERVER_SCRIPT="$SERVER_DIR/bin/$SERVER_APP_NAME"
|
||||
SERVER_LOGFILE="$SERVER_DATA_DIR/.$DISTRO_COMMIT.log"
|
||||
SERVER_PIDFILE="$SERVER_DATA_DIR/.$DISTRO_COMMIT.pid"
|
||||
SERVER_TOKENFILE="$SERVER_DATA_DIR/.$DISTRO_COMMIT.token"
|
||||
SERVER_OS=
|
||||
SERVER_ARCH=
|
||||
SERVER_CONNECTION_TOKEN=
|
||||
SERVER_DOWNLOAD_URL=
|
||||
|
||||
LISTENING_ON=
|
||||
OS_RELEASE_ID=
|
||||
ARCH=
|
||||
PLATFORM=
|
||||
|
||||
# Mimic output from logs of remote-ssh extension
|
||||
print_install_results_and_exit() {
|
||||
echo "${id}: start"
|
||||
echo "exitCode==$1=="
|
||||
echo "listeningOn==$LISTENING_ON=="
|
||||
echo "connectionToken==$SERVER_CONNECTION_TOKEN=="
|
||||
echo "logFile==$SERVER_LOGFILE=="
|
||||
echo "osReleaseId==$OS_RELEASE_ID=="
|
||||
echo "arch==$ARCH=="
|
||||
echo "platform==$PLATFORM=="
|
||||
echo "tmpDir==$TMP_DIR=="
|
||||
${envVariables.map(envVar => `echo "${envVar}==$${envVar}=="`).join('\n')}
|
||||
echo "${id}: end"
|
||||
exit 0
|
||||
}
|
||||
|
||||
# Check if platform is supported
|
||||
PLATFORM="$(uname -s)"
|
||||
case $PLATFORM in
|
||||
Linux)
|
||||
SERVER_OS="linux"
|
||||
;;
|
||||
*)
|
||||
echo "Error platform not supported: $PLATFORM"
|
||||
print_install_results_and_exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
# Check machine architecture
|
||||
ARCH="$(uname -m)"
|
||||
case $ARCH in
|
||||
x86_64 | amd64)
|
||||
SERVER_ARCH="x64"
|
||||
;;
|
||||
armv7l | armv8l)
|
||||
SERVER_ARCH="armhf"
|
||||
;;
|
||||
arm64 | aarch64)
|
||||
SERVER_ARCH="arm64"
|
||||
;;
|
||||
*)
|
||||
echo "Error architecture not supported: $ARCH"
|
||||
print_install_results_and_exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
# https://www.freedesktop.org/software/systemd/man/os-release.html
|
||||
OS_RELEASE_ID="$(grep -i '^ID=' /etc/os-release 2>/dev/null | sed 's/^ID=//gi' | sed 's/"//g')"
|
||||
if [[ -z $OS_RELEASE_ID ]]; then
|
||||
OS_RELEASE_ID="$(grep -i '^ID=' /usr/lib/os-release 2>/dev/null | sed 's/^ID=//gi' | sed 's/"//g')"
|
||||
if [[ -z $OS_RELEASE_ID ]]; then
|
||||
OS_RELEASE_ID="unknown"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Create installation folder
|
||||
if [[ ! -d $SERVER_DIR ]]; then
|
||||
mkdir -p $SERVER_DIR
|
||||
if (( $? > 0 )); then
|
||||
echo "Error creating server install directory"
|
||||
print_install_results_and_exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
SERVER_DOWNLOAD_URL="$(echo "${serverDownloadUrlTemplate.replace(/\$\{/g, '\\${')}" | sed "s/\\\${quality}/$DISTRO_QUALITY/g" | sed "s/\\\${version}/$DISTRO_VERSION/g" | sed "s/\\\${commit}/$DISTRO_COMMIT/g" | sed "s/\\\${os}/$SERVER_OS/g" | sed "s/\\\${arch}/$SERVER_ARCH/g" | sed "s/\\\${release}/$DISTRO_VSCODIUM_RELEASE/g")"
|
||||
|
||||
# Check if server script is already installed
|
||||
if [[ ! -f $SERVER_SCRIPT ]]; then
|
||||
if [[ "$SERVER_OS" = "dragonfly" ]] || [[ "$SERVER_OS" = "freebsd" ]]; then
|
||||
echo "Error "$SERVER_OS" needs manual installation of remote extension host"
|
||||
print_install_results_and_exit 1
|
||||
fi
|
||||
|
||||
pushd $SERVER_DIR > /dev/null
|
||||
|
||||
if [[ ! -z $(which wget) ]]; then
|
||||
wget --tries=3 --timeout=10 --continue --no-verbose -O vscode-server.tar.gz $SERVER_DOWNLOAD_URL
|
||||
elif [[ ! -z $(which curl) ]]; then
|
||||
curl --retry 3 --connect-timeout 10 --location --show-error --silent --output vscode-server.tar.gz $SERVER_DOWNLOAD_URL
|
||||
else
|
||||
echo "Error no tool to download server binary"
|
||||
print_install_results_and_exit 1
|
||||
fi
|
||||
|
||||
if (( $? > 0 )); then
|
||||
echo "Error downloading server from $SERVER_DOWNLOAD_URL"
|
||||
print_install_results_and_exit 1
|
||||
fi
|
||||
|
||||
tar -xf vscode-server.tar.gz --strip-components 1
|
||||
if (( $? > 0 )); then
|
||||
echo "Error while extracting server contents"
|
||||
print_install_results_and_exit 1
|
||||
fi
|
||||
|
||||
if [[ ! -f $SERVER_SCRIPT ]]; then
|
||||
echo "Error server contents are corrupted"
|
||||
print_install_results_and_exit 1
|
||||
fi
|
||||
|
||||
rm -f vscode-server.tar.gz
|
||||
|
||||
popd > /dev/null
|
||||
else
|
||||
echo "Server script already installed in $SERVER_SCRIPT"
|
||||
fi
|
||||
|
||||
# Try to find if server is already running
|
||||
if [[ -f $SERVER_PIDFILE ]]; then
|
||||
SERVER_PID="$(cat $SERVER_PIDFILE)"
|
||||
SERVER_RUNNING_PROCESS="$(ps -o pid,args -p $SERVER_PID | grep $SERVER_SCRIPT)"
|
||||
else
|
||||
SERVER_RUNNING_PROCESS="$(ps -o pid,args -A | grep $SERVER_SCRIPT | grep -v grep)"
|
||||
fi
|
||||
|
||||
if [[ -z $SERVER_RUNNING_PROCESS ]]; then
|
||||
if [[ -f $SERVER_LOGFILE ]]; then
|
||||
rm $SERVER_LOGFILE
|
||||
fi
|
||||
if [[ -f $SERVER_TOKENFILE ]]; then
|
||||
rm $SERVER_TOKENFILE
|
||||
fi
|
||||
|
||||
touch $SERVER_TOKENFILE
|
||||
chmod 600 $SERVER_TOKENFILE
|
||||
SERVER_CONNECTION_TOKEN="${crypto.randomUUID()}"
|
||||
echo $SERVER_CONNECTION_TOKEN > $SERVER_TOKENFILE
|
||||
|
||||
$SERVER_SCRIPT --start-server --host=127.0.0.1 $SERVER_LISTEN_FLAG $SERVER_INITIAL_EXTENSIONS --connection-token-file $SERVER_TOKENFILE --telemetry-level off --use-host-proxy --disable-websocket-compression --without-browser-env-var --enable-remote-auto-shutdown --accept-server-license-terms &> $SERVER_LOGFILE &
|
||||
echo $! > $SERVER_PIDFILE
|
||||
else
|
||||
echo "Server script is already running $SERVER_SCRIPT"
|
||||
fi
|
||||
|
||||
if [[ -f $SERVER_TOKENFILE ]]; then
|
||||
SERVER_CONNECTION_TOKEN="$(cat $SERVER_TOKENFILE)"
|
||||
else
|
||||
echo "Error server token file not found $SERVER_TOKENFILE"
|
||||
print_install_results_and_exit 1
|
||||
fi
|
||||
|
||||
if [[ -f $SERVER_LOGFILE ]]; then
|
||||
for i in {1..5}; do
|
||||
LISTENING_ON="$(cat $SERVER_LOGFILE | grep -E 'Extension host agent listening on .+' | sed 's/Extension host agent listening on //')"
|
||||
if [[ -n $LISTENING_ON ]]; then
|
||||
break
|
||||
fi
|
||||
sleep 0.5
|
||||
done
|
||||
|
||||
if [[ -z $LISTENING_ON ]]; then
|
||||
echo "Error server did not start sucessfully"
|
||||
print_install_results_and_exit 1
|
||||
fi
|
||||
else
|
||||
echo "Error server log file not found $SERVER_LOGFILE"
|
||||
print_install_results_and_exit 1
|
||||
fi
|
||||
|
||||
# Finish server setup and keep script running
|
||||
if [[ -z $SERVER_RUNNING_PROCESS ]]; then
|
||||
echo "${id}: start"
|
||||
echo "exitCode==0=="
|
||||
echo "listeningOn==$LISTENING_ON=="
|
||||
echo "connectionToken==$SERVER_CONNECTION_TOKEN=="
|
||||
echo "logFile==$SERVER_LOGFILE=="
|
||||
echo "osReleaseId==$OS_RELEASE_ID=="
|
||||
echo "arch==$ARCH=="
|
||||
echo "platform==$PLATFORM=="
|
||||
echo "tmpDir==$TMP_DIR=="
|
||||
${envVariables.map(envVar => `echo "${envVar}==$${envVar}=="`).join('\n')}
|
||||
echo "${id}: end"
|
||||
|
||||
echo "${id}: Server installation script done"
|
||||
|
||||
SERVER_PID="$(cat $SERVER_PIDFILE)"
|
||||
SERVER_RUNNING_PROCESS="$(ps -o pid,args -p $SERVER_PID | grep $SERVER_SCRIPT)"
|
||||
while [[ -n $SERVER_RUNNING_PROCESS ]]; do
|
||||
sleep 300;
|
||||
SERVER_RUNNING_PROCESS="$(ps -o pid,args -p $SERVER_PID | grep $SERVER_SCRIPT)"
|
||||
done
|
||||
else
|
||||
print_install_results_and_exit 0
|
||||
fi
|
||||
`;
|
||||
}
|
||||
150
extensions/open-remote-wsl/src/wsl/wslManager.ts
Normal file
150
extensions/open-remote-wsl/src/wsl/wslManager.ts
Normal file
|
|
@ -0,0 +1,150 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as cp from 'child_process';
|
||||
import Log from '../common/logger';
|
||||
import { EventEmitter } from '../common/event';
|
||||
|
||||
const wslBinary = 'wsl.exe';
|
||||
|
||||
export interface WSLDistro {
|
||||
isDefault: boolean;
|
||||
name: string;
|
||||
state: string;
|
||||
version: string;
|
||||
}
|
||||
|
||||
export interface WSLOnlineDistro {
|
||||
name: string;
|
||||
friendlyName: string;
|
||||
}
|
||||
|
||||
export class WSLManager {
|
||||
constructor(private readonly logger: Log) {
|
||||
}
|
||||
|
||||
async listDistros() {
|
||||
const resp = this._runWSLCommand(['--list', '--verbose'], 'utf16le');
|
||||
const { exitCode } = await resp.exitPromise;
|
||||
const { stdout, stderr } = resp;
|
||||
if (exitCode) {
|
||||
this.logger.trace(`Command wsl listDistros exited with code ${exitCode}`, stdout + '\n\n' + stderr);
|
||||
throw new Error(`Command wsl listDistros exited with code ${exitCode}`);
|
||||
}
|
||||
|
||||
const regex = /(?<default>\*|\s)\s+(?<name>[\w\.-]+)\s+(?<state>[\w]+)\s+(?<version>\d)/;
|
||||
const distros: WSLDistro[] = [];
|
||||
for (const line of stdout.split(/\r?\n/)) {
|
||||
const matches = line.match(regex);
|
||||
if (matches && matches.groups) {
|
||||
distros.push({
|
||||
isDefault: matches.groups.default === '*',
|
||||
name: matches.groups.name,
|
||||
state: matches.groups.state,
|
||||
version: matches.groups.version,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return distros;
|
||||
}
|
||||
|
||||
async listOnlineDistros() {
|
||||
const resp = this._runWSLCommand(['--list', '--online'], 'utf16le');
|
||||
const { exitCode } = await resp.exitPromise;
|
||||
const { stdout, stderr } = resp;
|
||||
if (exitCode) {
|
||||
this.logger.trace(`Command wsl listOnlineDistros exited with code ${exitCode}`, stdout + '\n\n' + stderr);
|
||||
throw new Error(`Command wsl listOnlineDistros exited with code ${exitCode}`);
|
||||
}
|
||||
|
||||
let lines = stdout.split(/\r?\n/);
|
||||
const idx = lines.findIndex(l => /\s*NAME\s+FRIENDLY NAME\s*/.test(l));
|
||||
lines = lines.slice(idx + 1);
|
||||
|
||||
const regex = /(?<name>[\w\.-]+)\s+(?<friendlyName>\w.+\w)/;
|
||||
const distros: WSLOnlineDistro[] = [];
|
||||
for (const line of lines) {
|
||||
const matches = line.match(regex);
|
||||
if (matches && matches.groups) {
|
||||
distros.push({
|
||||
name: matches.groups.name,
|
||||
friendlyName: matches.groups.friendlyName,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return distros;
|
||||
}
|
||||
|
||||
async setDefaultDistro(distroName: string) {
|
||||
const resp = this._runWSLCommand(['--set-default', distroName], 'utf16le');
|
||||
const { exitCode } = await resp.exitPromise;
|
||||
const { stdout, stderr } = resp;
|
||||
if (exitCode) {
|
||||
this.logger.trace(`Command wsl setDefaultDistro exited with code ${exitCode}`, stdout + '\n\n' + stderr);
|
||||
throw new Error(`Command wsl setDefaultDistro exited with code ${exitCode}`);
|
||||
}
|
||||
}
|
||||
|
||||
async deleteDistro(distroName: string) {
|
||||
const resp = this._runWSLCommand(['--unregister', distroName], 'utf16le');
|
||||
const { exitCode } = await resp.exitPromise;
|
||||
const { stdout, stderr } = resp;
|
||||
if (exitCode) {
|
||||
this.logger.trace(`Command wsl deleteDistro exited with code ${exitCode}`, stdout + '\n\n' + stderr);
|
||||
throw new Error(`Command wsl deleteDistro exited with code ${exitCode}`);
|
||||
}
|
||||
}
|
||||
|
||||
async exec(cmd: string, args: string[], distro: string) {
|
||||
return this._runWSLCommand(['--distribution', distro, '--', cmd, ...args], 'utf8');
|
||||
}
|
||||
|
||||
private _runWSLCommand(args: string[], encoding: 'utf8' | 'utf16le') {
|
||||
this.logger.trace(`Running WSL command: ${wslBinary} ${args.join(' ')}`);
|
||||
|
||||
const cmd = cp.spawn(wslBinary, args, { windowsHide: true, windowsVerbatimArguments: true });
|
||||
|
||||
const stdoutDataEmitter = new EventEmitter<Buffer>();
|
||||
const stdoutData: Buffer[] = [];
|
||||
const stderrDataEmitter = new EventEmitter<Buffer>();
|
||||
const stderrData: Buffer[] = [];
|
||||
cmd.stdout.on('data', (data: Buffer) => {
|
||||
stdoutData.push(data);
|
||||
stdoutDataEmitter.fire(data);
|
||||
});
|
||||
cmd.stderr.on('data', (data: Buffer) => {
|
||||
stderrData.push(data);
|
||||
stderrDataEmitter.fire(data);
|
||||
});
|
||||
|
||||
const exitPromise = new Promise<{ exitCode: number }>((resolve, reject) => {
|
||||
cmd.on('error', (err) => {
|
||||
this.logger.error(`Error running WSL command: ${wslBinary} ${args.join(' ')}`, err);
|
||||
reject(err);
|
||||
});
|
||||
cmd.on('exit', (code, _signal) => {
|
||||
resolve({ exitCode: code ?? 0 });
|
||||
});
|
||||
});
|
||||
|
||||
return {
|
||||
get stdout() {
|
||||
return Buffer.concat(stdoutData).toString(encoding);
|
||||
},
|
||||
get stderr() {
|
||||
return Buffer.concat(stderrData).toString(encoding);
|
||||
},
|
||||
get onStdoutData() {
|
||||
return stdoutDataEmitter.event;
|
||||
},
|
||||
get onStderrData() {
|
||||
return stderrDataEmitter.event;
|
||||
},
|
||||
exitPromise
|
||||
};
|
||||
}
|
||||
}
|
||||
26
extensions/open-remote-wsl/src/wsl/wslTerminal.ts
Normal file
26
extensions/open-remote-wsl/src/wsl/wslTerminal.ts
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as vscode from 'vscode';
|
||||
|
||||
class WSLTerminal {
|
||||
static NAME = 'WSL';
|
||||
|
||||
private getTerminal() {
|
||||
const wslTerminal = vscode.window.terminals.find(t => t.name === WSLTerminal.NAME);
|
||||
if (wslTerminal) {
|
||||
return wslTerminal;
|
||||
}
|
||||
return vscode.window.createTerminal(WSLTerminal.NAME);
|
||||
}
|
||||
|
||||
runCommand(command: string) {
|
||||
const wslTerminal = this.getTerminal();
|
||||
wslTerminal.show(false);
|
||||
wslTerminal.sendText(command, true);
|
||||
}
|
||||
}
|
||||
|
||||
export default new WSLTerminal();
|
||||
12
extensions/open-remote-wsl/tsconfig.json
Normal file
12
extensions/open-remote-wsl/tsconfig.json
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"extends": "../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./out",
|
||||
},
|
||||
"include": [
|
||||
"src/**/*",
|
||||
"../../src/vscode-dts/vscode.d.ts",
|
||||
"../../src/vscode-dts/vscode.proposed.resolvers.d.ts",
|
||||
"../../src/vscode-dts/vscode.proposed.contribViewsRemote.d.ts",
|
||||
]
|
||||
}
|
||||
Loading…
Reference in a new issue