remote: first cut at 'inline' remote resolvers

For web, it seems the most feasible direction for resolvers as we make
existing remote extensions 'web enabled' is to allow them to run in the
extension host. However, in no case will there just be a simple
websocket we can connect to ordinarily.

This PR implements a first cut at 'inline' resolvers where messaging is
done in the extension host. I have not yet tested them on web, where I
think some more wiring is needed to mirror desktop. Also, resolution of
URLs is not in yet. I think for this we'd want to do some service-worker
-based 'loopback' approach to run requests inline in the remote
connection, similar to what I did for tunnels...

Resolvers are not yet run in a dedicated extension host, but I think
that should happen, at least on web where resolvers
will always(?) be 'inline'.

Most of the actual changes are genericizing places where we specified
the "host" and "port" previously into an enum. Additionally, instead of
having a single ISocketFactory, there's now a collection of them, which
the extension host manager registers into when a managed resolution
happens.
This commit is contained in:
Connor Peet
2023-04-18 14:51:14 -07:00
parent b242a8730c
commit f5427eed53
41 changed files with 826 additions and 272 deletions
@@ -66,6 +66,11 @@
"category": "Remote-TestResolver",
"command": "vscode-testresolver.currentWindow"
},
{
"title": "Connect to TestResolver in Current Window with Managed Connection",
"category": "Remote-TestResolver",
"command": "vscode-testresolver.currentWindowManaged"
},
{
"title": "Show TestResolver Log",
"category": "Remote-TestResolver",
@@ -27,7 +27,30 @@ export function activate(context: vscode.ExtensionContext) {
let connectionPaused = false;
const connectionPausedEvent = new vscode.EventEmitter<boolean>();
function doResolve(_authority: string, progress: vscode.Progress<{ message?: string; increment?: number }>): Promise<vscode.ResolvedAuthority> {
function getTunnelFeatures(): vscode.TunnelInformation['tunnelFeatures'] {
return {
elevation: true,
privacyOptions: vscode.workspace.getConfiguration('testresolver').get('supportPublicPorts') ? [
{
id: 'public',
label: 'Public',
themeIcon: 'eye'
},
{
id: 'other',
label: 'Other',
themeIcon: 'circuit-board'
},
{
id: 'private',
label: 'Private',
themeIcon: 'eye-closed'
}
] : []
};
}
function doResolve(authority: string, progress: vscode.Progress<{ message?: string; increment?: number }>): Promise<vscode.ResolverResult> {
if (connectionPaused) {
throw vscode.RemoteAuthorityResolverError.TemporarilyNotAvailable('Not available right now');
}
@@ -150,7 +173,35 @@ export function activate(context: vscode.ExtensionContext) {
}
});
});
return serverPromise.then(serverAddr => {
return serverPromise.then((serverAddr): Promise<vscode.ResolverResult> => {
if (authority.includes('managed')) {
console.log('Connecting via a managed authority');
return Promise.resolve(new vscode.ManagedResolvedAuthority(async () => {
const remoteSocket = net.createConnection({ port: serverAddr.port });
const dataEmitter = new vscode.EventEmitter<Uint8Array>();
const closeEmitter = new vscode.EventEmitter<Error | undefined>();
const endEmitter = new vscode.EventEmitter<void>();
await new Promise((res, rej) => {
remoteSocket.on('data', d => dataEmitter.fire(d))
.on('error', err => { rej(); closeEmitter.fire(err); })
.on('close', () => endEmitter.fire())
.on('end', () => endEmitter.fire())
.on('connect', res);
});
return {
onDidReceiveMessage: dataEmitter.event,
onDidClose: closeEmitter.event,
onDidEnd: endEmitter.event,
dataHandler: d => remoteSocket.write(d),
endHandler: () => remoteSocket.end(),
};
}, connectionToken));
}
return new Promise<vscode.ResolvedAuthority>((res, _rej) => {
const proxyServer = net.createServer(proxySocket => {
outputChannel.appendLine(`Proxy connection accepted`);
@@ -228,28 +279,7 @@ export function activate(context: vscode.ExtensionContext) {
proxyServer.listen(0, '127.0.0.1', () => {
const port = (<net.AddressInfo>proxyServer.address()).port;
outputChannel.appendLine(`Going through proxy at port ${port}`);
const r: vscode.ResolverResult = new vscode.ResolvedAuthority('127.0.0.1', port, connectionToken);
r.tunnelFeatures = {
elevation: true,
privacyOptions: vscode.workspace.getConfiguration('testresolver').get('supportPublicPorts') ? [
{
id: 'public',
label: 'Public',
themeIcon: 'eye'
},
{
id: 'other',
label: 'Other',
themeIcon: 'circuit-board'
},
{
id: 'private',
label: 'Private',
themeIcon: 'eye-closed'
}
] : []
};
res(r);
res(new vscode.ResolvedAuthority('127.0.0.1', port, connectionToken));
});
context.subscriptions.push({
dispose: () => {
@@ -264,12 +294,16 @@ export function activate(context: vscode.ExtensionContext) {
async getCanonicalURI(uri: vscode.Uri): Promise<vscode.Uri> {
return vscode.Uri.file(uri.path);
},
resolve(_authority: string): Thenable<vscode.ResolvedAuthority> {
resolve(_authority: string): Thenable<vscode.ResolverResult> {
return vscode.window.withProgress({
location: vscode.ProgressLocation.Notification,
title: 'Open TestResolver Remote ([details](command:vscode-testresolver.showLog))',
cancellable: false
}, (progress) => doResolve(_authority, progress));
}, async (progress) => {
const rr = await doResolve(_authority, progress);
rr.tunnelFeatures = getTunnelFeatures();
return rr;
});
},
tunnelFactory,
showCandidatePort
@@ -282,6 +316,9 @@ export function activate(context: vscode.ExtensionContext) {
context.subscriptions.push(vscode.commands.registerCommand('vscode-testresolver.currentWindow', () => {
return vscode.commands.executeCommand('vscode.newWindow', { remoteAuthority: 'test+test', reuseWindow: true });
}));
context.subscriptions.push(vscode.commands.registerCommand('vscode-testresolver.currentWindowManaged', () => {
return vscode.commands.executeCommand('vscode.newWindow', { remoteAuthority: 'test+managed', reuseWindow: true });
}));
context.subscriptions.push(vscode.commands.registerCommand('vscode-testresolver.newWindowWithError', () => {
return vscode.commands.executeCommand('vscode.newWindow', { remoteAuthority: 'test+error' });
}));