forwarding: add built-in tunnel forwarding extension (#189874)

* forwarding: add built-in tunnel forwarding extension

- Support public/private ports, which accounts for most of the work in
  the CLI. Previously ports were only privat.
- Make the extension built-in. Ported from the remote-containers
  extension with some tweaks for privacy and durability.
- This also removes the opt-in flag, by not reimplementing it 😛

Fixes https://github.com/microsoft/vscode/issues/189677
Fixes https://github.com/microsoft/vscode/issues/189678

* fixup! comments

---------

Co-authored-by: Raymond Zhao <7199958+rzhao271@users.noreply.github.com>
This commit is contained in:
Connor Peet
2023-08-08 14:00:03 -07:00
committed by GitHub
parent 2159f75d4b
commit 71282c3d52
19 changed files with 664 additions and 35 deletions

View File

@@ -64,6 +64,7 @@ const compilations = [
'search-result/tsconfig.json',
'references-view/tsconfig.json',
'simple-browser/tsconfig.json',
'tunnel-forwarding/tsconfig.json',
'typescript-language-features/test-workspace/tsconfig.json',
'typescript-language-features/web/tsconfig.json',
'typescript-language-features/tsconfig.json',

View File

@@ -41,6 +41,7 @@ const dirs = [
'extensions/references-view',
'extensions/search-result',
'extensions/simple-browser',
'extensions/tunnel-forwarding',
'extensions/typescript-language-features',
'extensions/vscode-api-tests',
'extensions/vscode-colorize-tests',

View File

@@ -35,7 +35,7 @@ use crate::{
code_server::CodeServerArgs,
create_service_manager,
dev_tunnels::{self, DevTunnels},
forwarding, legal,
local_forwarding, legal,
paths::get_all_servers,
protocol, serve_stream,
shutdown_signal::ShutdownRequest,
@@ -444,7 +444,7 @@ pub async fn forward(
match acquire_singleton(&ctx.paths.forwarding_lockfile()).await {
Ok(SingletonConnection::Client(stream)) => {
debug!(ctx.log, "starting as client to singleton");
let r = forwarding::client(forwarding::SingletonClientArgs {
let r = local_forwarding::client(local_forwarding::SingletonClientArgs {
log: ctx.log.clone(),
shutdown: shutdown.clone(),
stream,
@@ -477,7 +477,7 @@ pub async fn forward(
.start_new_launcher_tunnel(None, true, &forward_args.ports)
.await?;
forwarding::server(ctx.log, tunnel, server, own_ports_rx, shutdown).await?;
local_forwarding::server(ctx.log, tunnel, server, own_ports_rx, shutdown).await?;
Ok(0)
}

View File

@@ -11,7 +11,7 @@ pub mod protocol;
pub mod shutdown_signal;
pub mod singleton_client;
pub mod singleton_server;
pub mod forwarding;
pub mod local_forwarding;
mod wsl_detect;
mod challenge;

View File

@@ -23,13 +23,15 @@ use std::time::Duration;
use tokio::sync::{mpsc, watch};
use tunnels::connections::{ForwardedPortConnection, RelayTunnelHost};
use tunnels::contracts::{
Tunnel, TunnelPort, TunnelRelayTunnelEndpoint, PORT_TOKEN, TUNNEL_PROTOCOL_AUTO,
Tunnel, TunnelAccessControl, TunnelPort, TunnelRelayTunnelEndpoint, PORT_TOKEN,
TUNNEL_ACCESS_SCOPES_CONNECT, TUNNEL_PROTOCOL_AUTO,
};
use tunnels::management::{
new_tunnel_management, HttpError, TunnelLocator, TunnelManagementClient, TunnelRequestOptions,
NO_REQUEST_OPTIONS,
};
use super::protocol::PortPrivacy;
use super::wsl_detect::is_wsl_installed;
static TUNNEL_COUNT_LIMIT_NAME: &str = "TunnelsPerUserPerLocation";
@@ -164,8 +166,12 @@ impl ActiveTunnel {
}
/// Forwards a port over TCP.
pub async fn add_port_tcp(&self, port_number: u16) -> Result<(), AnyError> {
self.manager.add_port_tcp(port_number).await?;
pub async fn add_port_tcp(
&self,
port_number: u16,
privacy: PortPrivacy,
) -> Result<(), AnyError> {
self.manager.add_port_tcp(port_number, privacy).await?;
Ok(())
}
@@ -866,13 +872,18 @@ impl ActiveTunnelManager {
/// Adds a port for TCP/IP forwarding.
#[allow(dead_code)] // todo: port forwarding
pub async fn add_port_tcp(&self, port_number: u16) -> Result<(), WrappedError> {
pub async fn add_port_tcp(
&self,
port_number: u16,
privacy: PortPrivacy,
) -> Result<(), WrappedError> {
self.relay
.lock()
.await
.add_port(&TunnelPort {
port_number,
protocol: Some(TUNNEL_PROTOCOL_AUTO.to_owned()),
access_control: Some(privacy_to_tunnel_acl(privacy)),
..Default::default()
})
.await
@@ -1081,6 +1092,26 @@ fn vec_eq_as_set(a: &[String], b: &[String]) -> bool {
true
}
fn privacy_to_tunnel_acl(privacy: PortPrivacy) -> TunnelAccessControl {
let mut acl = TunnelAccessControl { entries: vec![] };
if privacy == PortPrivacy::Public {
acl.entries
.push(tunnels::contracts::TunnelAccessControlEntry {
kind: tunnels::contracts::TunnelAccessControlEntryType::Anonymous,
provider: None,
is_inherited: false,
is_deny: false,
is_inverse: false,
organization: None,
subjects: vec![],
scopes: vec![TUNNEL_ACCESS_SCOPES_CONNECT.to_string()],
});
}
acl
}
#[cfg(test)]
mod test {
use super::*;

View File

@@ -5,6 +5,7 @@
use std::{
collections::HashMap,
ops::{Index, IndexMut},
sync::{Arc, Mutex},
};
@@ -26,11 +27,52 @@ use super::{
protocol::{
self,
forward_singleton::{PortList, SetPortsResponse},
PortPrivacy,
},
shutdown_signal::ShutdownSignal,
};
type PortMap = HashMap<u16, u32>;
#[derive(Default, Clone)]
struct PortCount {
public: u32,
private: u32,
}
impl Index<PortPrivacy> for PortCount {
type Output = u32;
fn index(&self, privacy: PortPrivacy) -> &Self::Output {
match privacy {
PortPrivacy::Public => &self.public,
PortPrivacy::Private => &self.private,
}
}
}
impl IndexMut<PortPrivacy> for PortCount {
fn index_mut(&mut self, privacy: PortPrivacy) -> &mut Self::Output {
match privacy {
PortPrivacy::Public => &mut self.public,
PortPrivacy::Private => &mut self.private,
}
}
}
impl PortCount {
fn is_empty(&self) -> bool {
self.public == 0 && self.private == 0
}
fn primary_privacy(&self) -> PortPrivacy {
if self.public > 0 {
PortPrivacy::Public
} else {
PortPrivacy::Private
}
}
}
type PortMap = HashMap<u16, PortCount>;
/// The PortForwardingHandle is given out to multiple consumers to allow
/// them to set_ports that they want to be forwarded.
@@ -56,23 +98,25 @@ impl PortForwardingSender {
self.sender.lock().unwrap().send_modify(|v| {
for p in current.iter() {
if !ports.contains(p) {
match v.get(p) {
Some(1) => {
v.remove(p);
}
Some(n) => {
v.insert(*p, n - 1);
}
None => unreachable!("removed port not in map"),
let n = v.get_mut(&p.number).expect("expected port in map");
n[p.privacy] -= 1;
if n.is_empty() {
v.remove(&p.number);
}
}
}
for p in ports.iter() {
if !current.contains(p) {
match v.get(p) {
Some(n) => v.insert(*p, n + 1),
None => v.insert(*p, 1),
match v.get_mut(&p.number) {
Some(n) => {
n[p.privacy] += 1;
}
None => {
let mut pc = PortCount::default();
pc[p.privacy] += 1;
v.insert(p.number, pc);
}
};
}
}
@@ -116,23 +160,26 @@ impl PortForwardingReceiver {
/// Applies all changes from PortForwardingHandles to the tunnel.
pub async fn apply_to(&mut self, log: log::Logger, tunnel: Arc<ActiveTunnel>) {
let mut current = vec![];
let mut current: PortMap = HashMap::new();
while self.receiver.changed().await.is_ok() {
let next = self.receiver.borrow().keys().copied().collect::<Vec<_>>();
let next = self.receiver.borrow().clone();
for p in current.iter() {
if !next.contains(p) {
match tunnel.remove_port(*p).await {
Ok(_) => info!(log, "stopped forwarding port {}", p),
Err(e) => error!(log, "failed to stop forwarding port {}: {}", p, e),
for (port, count) in current.iter() {
let privacy = count.primary_privacy();
if !matches!(next.get(port), Some(n) if n.primary_privacy() == privacy) {
match tunnel.remove_port(*port).await {
Ok(_) => info!(log, "stopped forwarding port {} at {:?}", *port, privacy),
Err(e) => error!(log, "failed to stop forwarding port {}: {}", port, e),
}
}
}
for p in next.iter() {
if !current.contains(p) {
match tunnel.add_port_tcp(*p).await {
Ok(_) => info!(log, "forwarding port {}", p),
Err(e) => error!(log, "failed to forward port {}: {}", p, e),
for (port, count) in next.iter() {
let privacy = count.primary_privacy();
if !matches!(current.get(port), Some(n) if n.primary_privacy() == privacy) {
match tunnel.add_port_tcp(*port, privacy).await {
Ok(_) => info!(log, "forwarding port {} at {:?}", port, privacy),
Err(e) => error!(log, "failed to forward port {}: {}", port, e),
}
}
}

View File

@@ -12,7 +12,7 @@ use crate::{
util::errors::{AnyError, CannotForwardControlPort, ServerHasClosed},
};
use super::dev_tunnels::ActiveTunnel;
use super::{dev_tunnels::ActiveTunnel, protocol::PortPrivacy};
pub enum PortForwardingRec {
Forward(u16, oneshot::Sender<Result<String, AnyError>>),
@@ -87,7 +87,7 @@ impl PortForwardingProcessor {
}
if !self.forwarded.contains(&port) {
tunnel.add_port_tcp(port).await?;
tunnel.add_port_tcp(port, PortPrivacy::Private).await?;
self.forwarded.insert(port);
}

View File

@@ -214,12 +214,27 @@ pub struct ChallengeVerifyParams {
pub response: String,
}
#[derive(Serialize, Deserialize, PartialEq, Eq, Copy, Clone, Debug)]
#[serde(rename_all = "lowercase")]
pub enum PortPrivacy {
Public,
Private,
}
pub mod forward_singleton {
use serde::{Deserialize, Serialize};
use super::PortPrivacy;
pub const METHOD_SET_PORTS: &str = "set_ports";
pub type PortList = Vec<u16>;
#[derive(Serialize, Deserialize, PartialEq, Eq, Clone, Debug)]
pub struct PortRec {
pub number: u16,
pub privacy: PortPrivacy,
}
pub type PortList = Vec<PortRec>;
#[derive(Serialize, Deserialize)]
pub struct SetPortsParams {

View File

@@ -0,0 +1,15 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "Extension",
"type": "extensionHost",
"request": "launch",
"args": ["--extensionDevelopmentPath=${workspaceFolder}"],
"env": { "VSCODE_FORWARDING_IS_DEV": "1" } // load the CLI from OSS
}
]
}

View File

@@ -0,0 +1,5 @@
src/**
tsconfig.json
out/**
extension.webpack.config.js
yarn.lock

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,
entry: {
extension: './src/extension.ts',
},
resolve: {
mainFields: ['module', 'main']
}
});

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 KiB

View File

@@ -0,0 +1,59 @@
{
"name": "tunnel-forwarding",
"displayName": "%displayName%",
"description": "%description%",
"version": "1.0.0",
"publisher": "vscode",
"license": "MIT",
"engines": {
"vscode": "^1.82.0"
},
"icon": "media/icon.png",
"capabilities": {
"virtualWorkspaces": false,
"untrustedWorkspaces": {
"supported": true
}
},
"enabledApiProposals": [
"resolvers",
"tunnelFactory"
],
"activationEvents": [
"onStartupFinished"
],
"contributes": {
"commands": [
{
"category": "%category%",
"command": "tunnel-forwarding.showLog",
"title": "%command.showLog%",
"enablement": "tunnelForwardingHasLog"
},
{
"category": "%category%",
"command": "tunnel-forwarding.restart",
"title": "%command.restart%",
"enablement": "tunnelForwardingIsRunning"
}
]
},
"main": "./out/extension",
"scripts": {
"compile": "gulp compile-extension:tunnel-forwarding",
"watch": "gulp watch-extension:tunnel-forwarding"
},
"devDependencies": {
"@types/node": "18.x"
},
"prettier": {
"printWidth": 100,
"trailingComma": "all",
"singleQuote": true,
"arrowParens": "avoid"
},
"repository": {
"type": "git",
"url": "https://github.com/microsoft/vscode.git"
}
}

View File

@@ -0,0 +1,7 @@
{
"displayName": "Local Tunnel Port Forwarding",
"description": "Allows forwarding local ports to be accessible over the internet.",
"category": "Port Forwarding",
"command.showLog": "Show Log",
"command.restart": "Restart Forwarding System"
}

View File

@@ -0,0 +1,62 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
export type ValueCallback<T = unknown> = (value: T | Promise<T>) => void;
const enum DeferredOutcome {
Resolved,
Rejected
}
/**
* Copied from src\vs\base\common\async.ts
*/
export class DeferredPromise<T> {
private completeCallback!: ValueCallback<T>;
private errorCallback!: (err: unknown) => void;
private outcome?: { outcome: DeferredOutcome.Rejected; value: any } | { outcome: DeferredOutcome.Resolved; value: T };
public get isRejected() {
return this.outcome?.outcome === DeferredOutcome.Rejected;
}
public get isResolved() {
return this.outcome?.outcome === DeferredOutcome.Resolved;
}
public get isSettled() {
return !!this.outcome;
}
public get value() {
return this.outcome?.outcome === DeferredOutcome.Resolved ? this.outcome?.value : undefined;
}
public readonly p: Promise<T>;
constructor() {
this.p = new Promise<T>((c, e) => {
this.completeCallback = c;
this.errorCallback = e;
});
}
public complete(value: T) {
return new Promise<void>(resolve => {
this.completeCallback(value);
this.outcome = { outcome: DeferredOutcome.Resolved, value };
resolve();
});
}
public error(err: unknown) {
return new Promise<void>(resolve => {
this.errorCallback(err);
this.outcome = { outcome: DeferredOutcome.Rejected, value: err };
resolve();
});
}
}

View File

@@ -0,0 +1,293 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { ChildProcessWithoutNullStreams, spawn } from 'child_process';
import * as path from 'path';
import * as vscode from 'vscode';
import { DeferredPromise } from './deferredPromise';
import { splitNewLines } from './split';
export const enum TunnelPrivacyId {
Private = 'private',
Public = 'public',
}
/**
* Timeout after the last port forwarding is disposed before we'll tear down
* the CLI. This is primarily used since privacy changes to port will appear
* as a dispose+re-create call, and we don't want to have to restart the CLI.
*/
const CLEANUP_TIMEOUT = 10_000;
const cliPath = process.env.VSCODE_FORWARDING_IS_DEV
? path.join(__dirname, '../../../cli/target/debug/code')
: path.join(
vscode.env.appRoot,
process.platform === 'win32' ? '../../bin' : 'bin',
vscode.env.appQuality === 'stable' ? 'code-tunnel' : 'code-tunnel-insiders',
) + (process.platform === 'win32' ? '.exe' : '');
class Tunnel implements vscode.Tunnel {
private readonly disposeEmitter = new vscode.EventEmitter<void>();
public readonly onDidDispose = this.disposeEmitter.event;
public localAddress!: string;
constructor(
public readonly remoteAddress: { port: number; host: string },
public readonly privacy: TunnelPrivacyId,
) { }
public setPortFormat(formatString: string) {
this.localAddress = formatString.replace('{port}', String(this.remoteAddress.port));
}
dispose() {
this.disposeEmitter.fire();
}
}
const enum State {
Starting,
Active,
Inactive,
Error,
}
type StateT =
| { state: State.Inactive }
| { state: State.Starting; process: ChildProcessWithoutNullStreams; cleanupTimeout?: NodeJS.Timeout }
| { state: State.Active; portFormat: string; process: ChildProcessWithoutNullStreams; cleanupTimeout?: NodeJS.Timeout }
| { state: State.Error; error: string };
export async function activate(context: vscode.ExtensionContext) {
if (vscode.env.remoteAuthority) {
return; // forwarding is local-only at the moment
}
const logger = new Logger(vscode.l10n.t('Port Forwarding'));
const provider = new TunnelProvider(logger);
context.subscriptions.push(
vscode.commands.registerCommand('tunnel-forwarding.showLog', () => logger.show()),
vscode.commands.registerCommand('tunnel-forwarding.restart', () => provider.restart()),
provider.onDidStateChange(s => {
vscode.commands.executeCommand('setContext', 'tunnelForwardingIsRunning', s.state !== State.Inactive);
}),
await vscode.workspace.registerTunnelProvider(
provider,
{
tunnelFeatures: {
elevation: false,
privacyOptions: [
{ themeIcon: 'globe', id: TunnelPrivacyId.Public, label: vscode.l10n.t('Public') },
{ themeIcon: 'lock', id: TunnelPrivacyId.Private, label: vscode.l10n.t('Private') },
],
},
},
),
);
}
export function deactivate() { }
class Logger {
private outputChannel?: vscode.LogOutputChannel;
constructor(private readonly label: string) { }
public show(): void {
return this.outputChannel?.show();
}
public clear() {
this.outputChannel?.clear();
}
public log(
logLevel: 'trace' | 'debug' | 'info' | 'warn' | 'error',
message: string,
...args: unknown[]
) {
if (!this.outputChannel) {
this.outputChannel = vscode.window.createOutputChannel(this.label, { log: true });
vscode.commands.executeCommand('setContext', 'tunnelForwardingHasLog', true);
}
this.outputChannel[logLevel](message, ...args);
}
}
class TunnelProvider implements vscode.TunnelProvider {
private readonly tunnels = new Set<Tunnel>();
private readonly stateChange = new vscode.EventEmitter<StateT>();
private _state: StateT = { state: State.Inactive };
private get state(): StateT {
return this._state;
}
private set state(state: StateT) {
this._state = state;
this.stateChange.fire(state);
}
public readonly onDidStateChange = this.stateChange.event;
constructor(private readonly logger: Logger) { }
/** @inheritdoc */
public async provideTunnel(tunnelOptions: vscode.TunnelOptions): Promise<vscode.Tunnel> {
const tunnel = new Tunnel(
tunnelOptions.remoteAddress,
(tunnelOptions.privacy as TunnelPrivacyId) || TunnelPrivacyId.Private,
);
this.tunnels.add(tunnel);
tunnel.onDidDispose(() => {
this.tunnels.delete(tunnel);
this.updateActivePortsIfRunning();
});
switch (this.state.state) {
case State.Error:
case State.Inactive:
await this.setupPortForwardingProcess();
// fall through since state is now starting
case State.Starting:
this.updateActivePortsIfRunning();
return new Promise<Tunnel>((resolve, reject) => {
const l = this.stateChange.event(state => {
if (state.state === State.Active) {
tunnel.setPortFormat(state.portFormat);
l.dispose();
resolve(tunnel);
} else if (state.state === State.Error) {
l.dispose();
reject(new Error(state.error));
}
});
});
case State.Active:
tunnel.setPortFormat(this.state.portFormat);
this.updateActivePortsIfRunning();
return tunnel;
}
}
/** Re/starts the port forwarding system. */
public async restart() {
this.killRunningProcess();
await this.setupPortForwardingProcess(); // will show progress
this.updateActivePortsIfRunning();
}
private isInStateWithProcess(process: ChildProcessWithoutNullStreams) {
return (
(this.state.state === State.Starting || this.state.state === State.Active) &&
this.state.process === process
);
}
private killRunningProcess() {
if (this.state.state === State.Starting || this.state.state === State.Active) {
this.logger.log('info', '[forwarding] no more ports, stopping forwarding CLI');
this.state.process.kill();
this.state = { state: State.Inactive };
}
}
private updateActivePortsIfRunning() {
if (this.state.state !== State.Starting && this.state.state !== State.Active) {
return;
}
const ports = [...this.tunnels].map(t => ({ number: t.remoteAddress.port, privacy: t.privacy }));
this.state.process.stdin.write(`${JSON.stringify(ports)}\n`);
if (ports.length === 0 && !this.state.cleanupTimeout) {
this.state.cleanupTimeout = setTimeout(() => this.killRunningProcess(), CLEANUP_TIMEOUT);
} else if (ports.length > 0 && this.state.cleanupTimeout) {
clearTimeout(this.state.cleanupTimeout);
this.state.cleanupTimeout = undefined;
}
}
private async setupPortForwardingProcess() {
const session = await vscode.authentication.getSession('github', ['user:email', 'read:org'], {
createIfNone: true,
});
const args = [
'--verbose',
'tunnel',
'forward-internal',
'--provider',
'github',
'--access-token',
session.accessToken,
];
this.logger.log('info', '[forwarding] starting CLI');
const process = spawn(cliPath, args, { stdio: 'pipe' });
this.state = { state: State.Starting, process };
const progressP = new DeferredPromise<void>();
vscode.window.withProgress(
{
location: vscode.ProgressLocation.Notification,
title: vscode.l10n.t({
comment: ['do not change link format [Show Log](command), only change the text "Show Log"'],
message: 'Starting port forwarding system ([Show Log]({0}))',
args: ['command:tunnel-forwarding.showLog']
}),
},
() => progressP.p,
);
let lastPortFormat: string | undefined;
process.on('exit', status => {
const msg = `[forwarding] exited with code ${status}`;
this.logger.log('info', msg);
progressP.complete(); // make sure to clear progress on unexpected exit
if (this.isInStateWithProcess(process)) {
this.state = { state: State.Error, error: msg };
}
});
process.on('error', err => {
this.logger.log('error', `[forwarding] ${err}`);
progressP.complete(); // make sure to clear progress on unexpected exit
if (this.isInStateWithProcess(process)) {
this.state = { state: State.Error, error: String(err) };
}
});
process.stdout
.pipe(splitNewLines())
.on('data', line => this.logger.log('info', `[forwarding] ${line}`))
.resume();
process.stderr
.pipe(splitNewLines())
.on('data', line => {
try {
const l: { port_format: string } = JSON.parse(line);
if (l.port_format && l.port_format !== lastPortFormat) {
this.state = { state: State.Active, portFormat: l.port_format, process };
progressP.complete();
}
} catch (e) {
this.logger.log('error', `[forwarding] ${line}`);
}
})
.resume();
await new Promise((resolve, reject) => {
process.on('spawn', resolve);
process.on('error', reject);
});
}
}

View File

@@ -0,0 +1,49 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Transform } from 'stream';
export const splitNewLines = () => new StreamSplitter('\n'.charCodeAt(0));
/**
* Copied and simplified from src\vs\base\node\nodeStreams.ts
*/
export class StreamSplitter extends Transform {
private buffer: Buffer | undefined;
constructor(private readonly splitter: number) {
super();
}
override _transform(chunk: Buffer, _encoding: string, callback: (error?: Error | null, data?: any) => void): void {
if (!this.buffer) {
this.buffer = chunk;
} else {
this.buffer = Buffer.concat([this.buffer, chunk]);
}
let offset = 0;
while (offset < this.buffer.length) {
const index = this.buffer.indexOf(this.splitter, offset);
if (index === -1) {
break;
}
this.push(this.buffer.subarray(offset, index + 1));
offset = index + 1;
}
this.buffer = offset === this.buffer.length ? undefined : this.buffer.subarray(offset);
callback();
}
override _flush(callback: (error?: Error | null, data?: any) => void): void {
if (this.buffer) {
this.push(this.buffer);
}
callback();
}
}

View File

@@ -0,0 +1,16 @@
{
"extends": "../tsconfig.base.json",
"compilerOptions": {
"outDir": "./out",
"downlevelIteration": true,
"types": [
"node"
]
},
"include": [
"src/**/*",
"../../src/vscode-dts/vscode.d.ts",
"../../src/vscode-dts/vscode.proposed.resolvers.d.ts",
"../../src/vscode-dts/vscode.proposed.tunnelFactory.d.ts"
]
}

View File

@@ -0,0 +1,8 @@
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1
"@types/node@18.x":
version "18.15.13"
resolved "https://registry.yarnpkg.com/@types/node/-/node-18.15.13.tgz#f64277c341150c979e42b00e4ac289290c9df469"
integrity sha512-N+0kuo9KgrUQ1Sn/ifDXsvg0TTleP7rIy4zOBGECxAljqvqfqpTfzx0Q1NUedOixRMBfe2Whhb056a42cWs26Q==