Added open-remote-wsl to extensions

This commit is contained in:
Joaquin Coromina 2025-03-08 00:23:54 -06:00
parent 5eb2bcfef6
commit d0921f899c
22 changed files with 1781 additions and 0 deletions

View file

@ -0,0 +1,3 @@
# Remote - WSL Support
Inherited for Void from [Open Remote - WSL](https://github.com/jeanp413/open-remote-wsl).

View file

@ -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'
}
});

View 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',
}
});

View 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"
}
}
}
}

View 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"
}
}

View 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();
}
}

View 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;
}

View 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;
}

View 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;
}
}

View 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));
}
}
}

View 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, '/');
}

View 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;
}

View 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';

View 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
}
}

View 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();
}
}

View 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() {
}

View 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;
}

View 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
};
}

View 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
`;
}

View 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
};
}
}

View 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();

View 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",
]
}