mirror of
https://github.com/microsoft/vscode.git
synced 2025-12-19 17:58:39 +00:00
cli: allow installation as a service from the UI (#187869)
- When turning on remote tunnel access, a quickpick is now shown asking users whether it should be installed as a service or just run in the session. - Picking the service install will install the tunnel as a service on the machine, and start it. - Turning off remote tunnel access will uninstall the service only if we were the ones to install it. - This involved some refactoring to add extra state to the RemoteTunnelService. There's now a "mode" that includes the previous "session" and reflects the desired end state. - I also did a cleanup with a `StreamSplitter` to ensure output of the CLI gets read line-by-line. This was depended upon by the remote tunnel service code, but it's not actually guaranteed. - Changes in the CLI: allow setting the tunnel name while installing the service, and make both service un/installation and renames idempotent. Closes https://github.com/microsoft/vscode/issues/184663
This commit is contained in:
@@ -649,6 +649,10 @@ pub struct TunnelServiceInstallArgs {
|
||||
/// If set, the user accepts the server license terms and the server will be started without a user prompt.
|
||||
#[clap(long)]
|
||||
pub accept_server_license_terms: bool,
|
||||
|
||||
/// Sets the machine name for port forwarding service
|
||||
#[clap(long)]
|
||||
pub name: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Args, Debug, Clone)]
|
||||
|
||||
@@ -135,10 +135,17 @@ pub async fn service(
|
||||
let manager = create_service_manager(ctx.log.clone(), &ctx.paths);
|
||||
match service_args {
|
||||
TunnelServiceSubCommands::Install(args) => {
|
||||
// ensure logged in, otherwise subsequent serving will fail
|
||||
Auth::new(&ctx.paths, ctx.log.clone())
|
||||
.get_credential()
|
||||
.await?;
|
||||
let auth = Auth::new(&ctx.paths, ctx.log.clone());
|
||||
|
||||
if let Some(name) = &args.name {
|
||||
// ensure the name matches, and tunnel exists
|
||||
dev_tunnels::DevTunnels::new(&ctx.log, auth, &ctx.paths)
|
||||
.rename_tunnel(name)
|
||||
.await?;
|
||||
} else {
|
||||
// still ensure they're logged in, otherwise subsequent serving will fail
|
||||
auth.get_credential().await?;
|
||||
}
|
||||
|
||||
// likewise for license consent
|
||||
legal::require_consent(&ctx.paths, args.accept_server_license_terms)?;
|
||||
@@ -203,20 +210,20 @@ pub async fn user(ctx: CommandContext, user_args: TunnelUserSubCommands) -> Resu
|
||||
Ok(0)
|
||||
}
|
||||
|
||||
/// Remove the tunnel used by this gateway, if any.
|
||||
/// Remove the tunnel used by this tunnel, if any.
|
||||
pub async fn rename(ctx: CommandContext, rename_args: TunnelRenameArgs) -> Result<i32, AnyError> {
|
||||
let auth = Auth::new(&ctx.paths, ctx.log.clone());
|
||||
let mut dt = dev_tunnels::DevTunnels::new(&ctx.log, auth, &ctx.paths);
|
||||
dt.rename_tunnel(&rename_args.name).await?;
|
||||
ctx.log.result(format!(
|
||||
"Successfully renamed this gateway to {}",
|
||||
"Successfully renamed this tunnel to {}",
|
||||
&rename_args.name
|
||||
));
|
||||
|
||||
Ok(0)
|
||||
}
|
||||
|
||||
/// Remove the tunnel used by this gateway, if any.
|
||||
/// Remove the tunnel used by this tunnel, if any.
|
||||
pub async fn unregister(ctx: CommandContext) -> Result<i32, AnyError> {
|
||||
let auth = Auth::new(&ctx.paths, ctx.log.clone());
|
||||
let mut dt = dev_tunnels::DevTunnels::new(&ctx.log, auth, &ctx.paths);
|
||||
|
||||
@@ -117,7 +117,7 @@ impl<S: Serialization, C: Send + Sync + 'static> RpcMethodBuilder<S, C> {
|
||||
Ok(p) => p,
|
||||
Err(err) => {
|
||||
return id.map(|id| {
|
||||
serial.serialize(&ErrorResponse {
|
||||
serial.serialize(ErrorResponse {
|
||||
id,
|
||||
error: ResponseError {
|
||||
code: 0,
|
||||
@@ -131,7 +131,7 @@ impl<S: Serialization, C: Send + Sync + 'static> RpcMethodBuilder<S, C> {
|
||||
match callback(param.params, &context) {
|
||||
Ok(result) => id.map(|id| serial.serialize(&SuccessResponse { id, result })),
|
||||
Err(err) => id.map(|id| {
|
||||
serial.serialize(&ErrorResponse {
|
||||
serial.serialize(ErrorResponse {
|
||||
id,
|
||||
error: ResponseError {
|
||||
code: -1,
|
||||
@@ -161,7 +161,7 @@ impl<S: Serialization, C: Send + Sync + 'static> RpcMethodBuilder<S, C> {
|
||||
Ok(p) => p,
|
||||
Err(err) => {
|
||||
return future::ready(id.map(|id| {
|
||||
serial.serialize(&ErrorResponse {
|
||||
serial.serialize(ErrorResponse {
|
||||
id,
|
||||
error: ResponseError {
|
||||
code: 0,
|
||||
@@ -182,7 +182,7 @@ impl<S: Serialization, C: Send + Sync + 'static> RpcMethodBuilder<S, C> {
|
||||
id.map(|id| serial.serialize(&SuccessResponse { id, result }))
|
||||
}
|
||||
Err(err) => id.map(|id| {
|
||||
serial.serialize(&ErrorResponse {
|
||||
serial.serialize(ErrorResponse {
|
||||
id,
|
||||
error: ResponseError {
|
||||
code: -1,
|
||||
@@ -222,7 +222,7 @@ impl<S: Serialization, C: Send + Sync + 'static> RpcMethodBuilder<S, C> {
|
||||
return (
|
||||
None,
|
||||
future::ready(id.map(|id| {
|
||||
serial.serialize(&ErrorResponse {
|
||||
serial.serialize(ErrorResponse {
|
||||
id,
|
||||
error: ResponseError {
|
||||
code: 0,
|
||||
@@ -255,7 +255,7 @@ impl<S: Serialization, C: Send + Sync + 'static> RpcMethodBuilder<S, C> {
|
||||
match callback(servers, param.params, context).await {
|
||||
Ok(r) => id.map(|id| serial.serialize(&SuccessResponse { id, result: r })),
|
||||
Err(err) => id.map(|id| {
|
||||
serial.serialize(&ErrorResponse {
|
||||
serial.serialize(ErrorResponse {
|
||||
id,
|
||||
error: ResponseError {
|
||||
code: -1,
|
||||
@@ -427,7 +427,7 @@ impl<S: Serialization, C: Send + Sync> RpcDispatcher<S, C> {
|
||||
Some(Method::Async(callback)) => MaybeSync::Future(callback(id, body)),
|
||||
Some(Method::Duplex(callback)) => MaybeSync::Stream(callback(id, body)),
|
||||
None => MaybeSync::Sync(id.map(|id| {
|
||||
self.serializer.serialize(&ErrorResponse {
|
||||
self.serializer.serialize(ErrorResponse {
|
||||
id,
|
||||
error: ResponseError {
|
||||
code: -1,
|
||||
|
||||
@@ -275,7 +275,9 @@ impl DevTunnels {
|
||||
|
||||
/// Renames the current tunnel to the new name.
|
||||
pub async fn rename_tunnel(&mut self, name: &str) -> Result<(), AnyError> {
|
||||
self.update_tunnel_name(None, name).await.map(|_| ())
|
||||
self.update_tunnel_name(self.launcher_tunnel.load(), name)
|
||||
.await
|
||||
.map(|_| ())
|
||||
}
|
||||
|
||||
/// Updates the name of the existing persisted tunnel to the new name.
|
||||
@@ -286,28 +288,34 @@ impl DevTunnels {
|
||||
name: &str,
|
||||
) -> Result<(Tunnel, PersistedTunnel), AnyError> {
|
||||
let name = name.to_ascii_lowercase();
|
||||
self.check_is_name_free(&name).await?;
|
||||
|
||||
debug!(self.log, "Tunnel name changed, applying updates...");
|
||||
|
||||
let (mut full_tunnel, mut persisted, is_new) = match persisted {
|
||||
Some(persisted) => {
|
||||
debug!(
|
||||
self.log,
|
||||
"Found a persisted tunnel, seeing if the name matches..."
|
||||
);
|
||||
self.get_or_create_tunnel(persisted, Some(&name), NO_REQUEST_OPTIONS)
|
||||
.await
|
||||
}
|
||||
None => self
|
||||
.create_tunnel(&name, NO_REQUEST_OPTIONS)
|
||||
.await
|
||||
.map(|(pt, t)| (t, pt, true)),
|
||||
None => {
|
||||
debug!(self.log, "Creating a new tunnel with the requested name");
|
||||
self.create_tunnel(&name, NO_REQUEST_OPTIONS)
|
||||
.await
|
||||
.map(|(pt, t)| (t, pt, true))
|
||||
}
|
||||
}?;
|
||||
|
||||
if is_new {
|
||||
let desired_tags = self.get_tags(&name);
|
||||
if is_new || vec_eq_as_set(&full_tunnel.tags, &desired_tags) {
|
||||
return Ok((full_tunnel, persisted));
|
||||
}
|
||||
|
||||
full_tunnel.tags = self.get_tags(&name);
|
||||
debug!(self.log, "Tunnel name changed, applying updates...");
|
||||
|
||||
let new_tunnel = spanf!(
|
||||
full_tunnel.tags = desired_tags;
|
||||
|
||||
let updated_tunnel = spanf!(
|
||||
self.log,
|
||||
self.log.span("dev-tunnel.tag.update"),
|
||||
self.client.update_tunnel(&full_tunnel, NO_REQUEST_OPTIONS)
|
||||
@@ -317,7 +325,7 @@ impl DevTunnels {
|
||||
persisted.name = name;
|
||||
self.launcher_tunnel.save(Some(persisted.clone()))?;
|
||||
|
||||
Ok((new_tunnel, persisted))
|
||||
Ok((updated_tunnel, persisted))
|
||||
}
|
||||
|
||||
/// Gets the persisted tunnel from the service, or creates a new one.
|
||||
@@ -443,6 +451,8 @@ impl DevTunnels {
|
||||
) -> Result<(PersistedTunnel, Tunnel), AnyError> {
|
||||
info!(self.log, "Creating tunnel with the name: {}", name);
|
||||
|
||||
self.check_is_name_free(name).await?;
|
||||
|
||||
let mut tried_recycle = false;
|
||||
|
||||
let new_tunnel = Tunnel {
|
||||
@@ -527,7 +537,7 @@ impl DevTunnels {
|
||||
options: &TunnelRequestOptions,
|
||||
) -> Result<Tunnel, AnyError> {
|
||||
let new_tags = self.get_tags(name);
|
||||
if vec_eq_unsorted(&tunnel.tags, &new_tags) {
|
||||
if vec_eq_as_set(&tunnel.tags, &new_tags) {
|
||||
return Ok(tunnel);
|
||||
}
|
||||
|
||||
@@ -610,7 +620,7 @@ impl DevTunnels {
|
||||
}
|
||||
|
||||
async fn check_is_name_free(&mut self, name: &str) -> Result<(), AnyError> {
|
||||
let existing = spanf!(
|
||||
let existing: Vec<Tunnel> = spanf!(
|
||||
self.log,
|
||||
self.log.span("dev-tunnel.rename.search"),
|
||||
self.client.list_all_tunnels(&TunnelRequestOptions {
|
||||
@@ -998,7 +1008,7 @@ fn clean_hostname_for_tunnel(hostname: &str) -> String {
|
||||
}
|
||||
}
|
||||
|
||||
fn vec_eq_unsorted(a: &[String], b: &[String]) -> bool {
|
||||
fn vec_eq_as_set(a: &[String], b: &[String]) -> bool {
|
||||
if a.len() != b.len() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -78,6 +78,7 @@ impl CliServiceManager for WindowsService {
|
||||
cmd.stderr(Stdio::null());
|
||||
cmd.stdout(Stdio::null());
|
||||
cmd.stdin(Stdio::null());
|
||||
cmd.creation_flags(CREATE_NEW_PROCESS_GROUP | DETACHED_PROCESS);
|
||||
cmd.spawn()
|
||||
.map_err(|e| wrapdbg(e, "error starting service"))?;
|
||||
|
||||
@@ -121,8 +122,12 @@ impl CliServiceManager for WindowsService {
|
||||
|
||||
async fn unregister(&self) -> Result<(), AnyError> {
|
||||
let key = WindowsService::open_key()?;
|
||||
key.delete_value(TUNNEL_ACTIVITY_NAME)
|
||||
.map_err(|e| AnyError::from(wrap(e, "error deleting registry key")))?;
|
||||
match key.delete_value(TUNNEL_ACTIVITY_NAME) {
|
||||
Ok(_) => {}
|
||||
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {}
|
||||
Err(e) => return Err(wrap(e, "error deleting registry key").into()),
|
||||
}
|
||||
|
||||
info!(self.log, "Tunnel service uninstalled");
|
||||
|
||||
let r = do_single_rpc_call::<_, ()>(
|
||||
|
||||
@@ -172,53 +172,59 @@ export class VSBuffer {
|
||||
writeUInt8(this.buffer, value, offset);
|
||||
}
|
||||
|
||||
indexOf(subarray: VSBuffer | Uint8Array) {
|
||||
const needle = subarray instanceof VSBuffer ? subarray.buffer : subarray;
|
||||
const needleLen = needle.byteLength;
|
||||
const haystack = this.buffer;
|
||||
const haystackLen = haystack.byteLength;
|
||||
|
||||
if (needleLen === 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (needleLen === 1) {
|
||||
return haystack.indexOf(needle[0]);
|
||||
}
|
||||
|
||||
if (needleLen > haystackLen) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
// find index of the subarray using boyer-moore-horspool algorithm
|
||||
const table = indexOfTable.value;
|
||||
table.fill(needle.length);
|
||||
for (let i = 0; i < needle.length; i++) {
|
||||
table[needle[i]] = needle.length - i - 1;
|
||||
}
|
||||
|
||||
let i = needle.length - 1;
|
||||
let j = i;
|
||||
let result = -1;
|
||||
while (i < haystackLen) {
|
||||
if (haystack[i] === needle[j]) {
|
||||
if (j === 0) {
|
||||
result = i;
|
||||
break;
|
||||
}
|
||||
|
||||
i--;
|
||||
j--;
|
||||
} else {
|
||||
i += Math.max(needle.length - j, table[haystack[i]]);
|
||||
j = needle.length - 1;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
indexOf(subarray: VSBuffer | Uint8Array, offset = 0) {
|
||||
return binaryIndexOf(this.buffer, subarray instanceof VSBuffer ? subarray.buffer : subarray, offset);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Like String.indexOf, but works on Uint8Arrays.
|
||||
* Uses the boyer-moore-horspool algorithm to be reasonably speedy.
|
||||
*/
|
||||
export function binaryIndexOf(haystack: Uint8Array, needle: Uint8Array, offset = 0): number {
|
||||
const needleLen = needle.byteLength;
|
||||
const haystackLen = haystack.byteLength;
|
||||
|
||||
if (needleLen === 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (needleLen === 1) {
|
||||
return haystack.indexOf(needle[0]);
|
||||
}
|
||||
|
||||
if (needleLen > haystackLen - offset) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
// find index of the subarray using boyer-moore-horspool algorithm
|
||||
const table = indexOfTable.value;
|
||||
table.fill(needle.length);
|
||||
for (let i = 0; i < needle.length; i++) {
|
||||
table[needle[i]] = needle.length - i - 1;
|
||||
}
|
||||
|
||||
let i = offset + needle.length - 1;
|
||||
let j = i;
|
||||
let result = -1;
|
||||
while (i < haystackLen) {
|
||||
if (haystack[i] === needle[j]) {
|
||||
if (j === 0) {
|
||||
result = i;
|
||||
break;
|
||||
}
|
||||
|
||||
i--;
|
||||
j--;
|
||||
} else {
|
||||
i += Math.max(needle.length - j, table[haystack[i]]);
|
||||
j = needle.length - 1;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
export function readUInt16LE(source: Uint8Array, offset: number): number {
|
||||
return (
|
||||
((source[offset + 0] << 0) >>> 0) |
|
||||
|
||||
62
src/vs/base/node/nodeStreams.ts
Normal file
62
src/vs/base/node/nodeStreams.ts
Normal 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.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
import { Transform } from 'stream';
|
||||
import { binaryIndexOf } from 'vs/base/common/buffer';
|
||||
|
||||
/**
|
||||
* A Transform stream that splits the input on the "splitter" substring.
|
||||
* The resulting chunks will contain (and trail with) the splitter match.
|
||||
* The last chunk when the stream ends will be emitted even if a splitter
|
||||
* is not encountered.
|
||||
*/
|
||||
export class StreamSplitter extends Transform {
|
||||
private buffer: Buffer | undefined;
|
||||
private readonly splitter: Buffer | number;
|
||||
private readonly spitterLen: number;
|
||||
|
||||
constructor(splitter: string | number | Buffer) {
|
||||
super();
|
||||
if (typeof splitter === 'number') {
|
||||
this.splitter = splitter;
|
||||
this.spitterLen = 1;
|
||||
} else {
|
||||
const buf = Buffer.isBuffer(splitter) ? splitter : Buffer.from(splitter);
|
||||
this.splitter = buf.length === 1 ? buf[0] : buf;
|
||||
this.spitterLen = buf.length;
|
||||
}
|
||||
}
|
||||
|
||||
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 = typeof this.splitter === 'number'
|
||||
? this.buffer.indexOf(this.splitter, offset)
|
||||
: binaryIndexOf(this.buffer, this.splitter, offset);
|
||||
if (index === -1) {
|
||||
break;
|
||||
}
|
||||
|
||||
this.push(this.buffer.slice(offset, index + this.spitterLen));
|
||||
offset = index + this.spitterLen;
|
||||
}
|
||||
|
||||
this.buffer = offset === this.buffer.length ? undefined : this.buffer.slice(offset);
|
||||
callback();
|
||||
}
|
||||
|
||||
override _flush(callback: (error?: Error | null, data?: any) => void): void {
|
||||
if (this.buffer) {
|
||||
this.push(this.buffer);
|
||||
}
|
||||
|
||||
callback();
|
||||
}
|
||||
}
|
||||
51
src/vs/base/test/node/nodeStreams.test.ts
Normal file
51
src/vs/base/test/node/nodeStreams.test.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
|
||||
import { Writable } from 'stream';
|
||||
import * as assert from 'assert';
|
||||
import { StreamSplitter } from 'vs/base/node/nodeStreams';
|
||||
|
||||
suite('StreamSplitter', () => {
|
||||
test('should split a stream on a single character splitter', (done) => {
|
||||
const chunks: string[] = [];
|
||||
const splitter = new StreamSplitter('\n');
|
||||
const writable = new Writable({
|
||||
write(chunk, _encoding, callback) {
|
||||
chunks.push(chunk.toString());
|
||||
callback();
|
||||
},
|
||||
});
|
||||
|
||||
splitter.pipe(writable);
|
||||
splitter.write('hello\nwor');
|
||||
splitter.write('ld\n');
|
||||
splitter.write('foo\nbar\nz');
|
||||
splitter.end(() => {
|
||||
assert.deepStrictEqual(chunks, ['hello\n', 'world\n', 'foo\n', 'bar\n', 'z']);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
test('should split a stream on a multi-character splitter', (done) => {
|
||||
const chunks: string[] = [];
|
||||
const splitter = new StreamSplitter('---');
|
||||
const writable = new Writable({
|
||||
write(chunk, _encoding, callback) {
|
||||
chunks.push(chunk.toString());
|
||||
callback();
|
||||
},
|
||||
});
|
||||
|
||||
splitter.pipe(writable);
|
||||
splitter.write('hello---wor');
|
||||
splitter.write('ld---');
|
||||
splitter.write('foo---bar---z');
|
||||
splitter.end(() => {
|
||||
assert.deepStrictEqual(chunks, ['hello---', 'world---', 'foo---', 'bar---', 'z']);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -21,18 +21,33 @@ export interface IRemoteTunnelService {
|
||||
readonly onDidChangeTunnelStatus: Event<TunnelStatus>;
|
||||
getTunnelStatus(): Promise<TunnelStatus>;
|
||||
|
||||
getSession(): Promise<IRemoteTunnelSession | undefined>;
|
||||
readonly onDidChangeSession: Event<IRemoteTunnelSession | undefined>;
|
||||
getMode(): Promise<TunnelMode>;
|
||||
readonly onDidChangeMode: Event<TunnelMode>;
|
||||
|
||||
readonly onDidTokenFailed: Event<IRemoteTunnelSession | undefined>;
|
||||
initialize(session: IRemoteTunnelSession | undefined): Promise<TunnelStatus>;
|
||||
initialize(mode: TunnelMode): Promise<TunnelStatus>;
|
||||
|
||||
startTunnel(session: IRemoteTunnelSession): Promise<TunnelStatus>;
|
||||
startTunnel(mode: ActiveTunnelMode): Promise<TunnelStatus>;
|
||||
stopTunnel(): Promise<void>;
|
||||
getTunnelName(): Promise<string | undefined>;
|
||||
|
||||
}
|
||||
|
||||
export interface ActiveTunnelMode {
|
||||
readonly active: true;
|
||||
readonly session: IRemoteTunnelSession;
|
||||
readonly asService: boolean;
|
||||
}
|
||||
|
||||
export interface InactiveTunnelMode {
|
||||
readonly active: false;
|
||||
}
|
||||
|
||||
export const INACTIVE_TUNNEL_MODE: InactiveTunnelMode = { active: false };
|
||||
|
||||
/** Saved mode for the tunnel. */
|
||||
export type TunnelMode = ActiveTunnelMode | InactiveTunnelMode;
|
||||
|
||||
export type TunnelStatus = TunnelStates.Connected | TunnelStates.Disconnected | TunnelStates.Connecting | TunnelStates.Uninitialized;
|
||||
|
||||
export namespace TunnelStates {
|
||||
@@ -46,13 +61,14 @@ export namespace TunnelStates {
|
||||
export interface Connected {
|
||||
readonly type: 'connected';
|
||||
readonly info: ConnectionInfo;
|
||||
readonly serviceInstallFailed: boolean;
|
||||
}
|
||||
export interface Disconnected {
|
||||
readonly type: 'disconnected';
|
||||
readonly onTokenFailed?: IRemoteTunnelSession;
|
||||
}
|
||||
export const disconnected = (onTokenFailed?: IRemoteTunnelSession): Disconnected => ({ type: 'disconnected', onTokenFailed });
|
||||
export const connected = (info: ConnectionInfo): Connected => ({ type: 'connected', info });
|
||||
export const connected = (info: ConnectionInfo, serviceInstallFailed: boolean): Connected => ({ type: 'connected', info, serviceInstallFailed });
|
||||
export const connecting = (progress?: string): Connecting => ({ type: 'connecting', progress });
|
||||
export const uninitialized: Uninitialized = { type: 'uninitialized' };
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { CONFIGURATION_KEY_HOST_NAME, CONFIGURATION_KEY_PREVENT_SLEEP, ConnectionInfo, IRemoteTunnelSession, IRemoteTunnelService, LOGGER_NAME, LOG_ID, TunnelStates, TunnelStatus } from 'vs/platform/remoteTunnel/common/remoteTunnel';
|
||||
import { CONFIGURATION_KEY_HOST_NAME, CONFIGURATION_KEY_PREVENT_SLEEP, ConnectionInfo, IRemoteTunnelSession, IRemoteTunnelService, LOGGER_NAME, LOG_ID, TunnelStates, TunnelStatus, TunnelMode, INACTIVE_TUNNEL_MODE, ActiveTunnelMode } from 'vs/platform/remoteTunnel/common/remoteTunnel';
|
||||
import { Emitter } from 'vs/base/common/event';
|
||||
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
|
||||
import { INativeEnvironmentService } from 'vs/platform/environment/common/environment';
|
||||
@@ -20,15 +20,18 @@ import { localize } from 'vs/nls';
|
||||
import { hostname, homedir } from 'os';
|
||||
import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage';
|
||||
import { isString } from 'vs/base/common/types';
|
||||
import { StreamSplitter } from 'vs/base/node/nodeStreams';
|
||||
|
||||
type RemoteTunnelEnablementClassification = {
|
||||
owner: 'aeschli';
|
||||
comment: 'Reporting when Remote Tunnel access is turned on or off';
|
||||
enabled?: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'Flag indicating if Remote Tunnel Access is enabled or not' };
|
||||
service?: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'Flag indicating if Remote Tunnel Access is installed as a service' };
|
||||
};
|
||||
|
||||
type RemoteTunnelEnablementEvent = {
|
||||
enabled: boolean;
|
||||
service: boolean;
|
||||
};
|
||||
|
||||
const restartTunnelOnConfigurationChanges: readonly string[] = [
|
||||
@@ -40,6 +43,8 @@ const restartTunnelOnConfigurationChanges: readonly string[] = [
|
||||
// if set, the remote tunnel access is currently enabled.
|
||||
// if not set, the remote tunnel access is currently disabled.
|
||||
const TUNNEL_ACCESS_SESSION = 'remoteTunnelSession';
|
||||
// Boolean indicating whether the tunnel should be installed as a service.
|
||||
const TUNNEL_ACCESS_IS_SERVICE = 'remoteTunnelIsService';
|
||||
|
||||
/**
|
||||
* This service runs on the shared service. It is running the `code-tunnel` command
|
||||
@@ -55,12 +60,19 @@ export class RemoteTunnelService extends Disposable implements IRemoteTunnelServ
|
||||
private readonly _onDidChangeTunnelStatusEmitter = new Emitter<TunnelStatus>();
|
||||
public readonly onDidChangeTunnelStatus = this._onDidChangeTunnelStatusEmitter.event;
|
||||
|
||||
private readonly _onDidChangeSessionEmitter = new Emitter<IRemoteTunnelSession | undefined>();
|
||||
public readonly onDidChangeSession = this._onDidChangeSessionEmitter.event;
|
||||
private readonly _onDidChangeModeEmitter = new Emitter<TunnelMode>();
|
||||
public readonly onDidChangeMode = this._onDidChangeModeEmitter.event;
|
||||
|
||||
private readonly _logger: ILogger;
|
||||
|
||||
private _session: IRemoteTunnelSession | undefined;
|
||||
/**
|
||||
* "Mode" in the terminal state we want to get to -- started, stopped, and
|
||||
* the attributes associated with each.
|
||||
*
|
||||
* At any given time, work may be ongoing to get `_tunnelStatus` into a
|
||||
* state that reflects the desired `mode`.
|
||||
*/
|
||||
private _mode: TunnelMode = INACTIVE_TUNNEL_MODE;
|
||||
|
||||
private _tunnelProcess: CancelablePromise<any> | undefined;
|
||||
|
||||
@@ -98,7 +110,7 @@ export class RemoteTunnelService extends Disposable implements IRemoteTunnelServ
|
||||
}
|
||||
}));
|
||||
|
||||
this._session = this._restoreSession();
|
||||
this._mode = this._restoreMode();
|
||||
this._tunnelStatus = TunnelStates.uninitialized;
|
||||
}
|
||||
|
||||
@@ -111,32 +123,34 @@ export class RemoteTunnelService extends Disposable implements IRemoteTunnelServ
|
||||
this._onDidChangeTunnelStatusEmitter.fire(tunnelStatus);
|
||||
}
|
||||
|
||||
private setSession(session: IRemoteTunnelSession | undefined) {
|
||||
if (!isSameSession(session, this._session)) {
|
||||
this._session = session;
|
||||
this._onDidChangeSessionEmitter.fire(session);
|
||||
this._storeSession(session);
|
||||
if (session) {
|
||||
this._logger.info(`Session updated: ${session.accountLabel} (${session.providerId})`);
|
||||
if (session.token) {
|
||||
this._logger.info(`Session token updated: ${session.accountLabel} (${session.providerId})`);
|
||||
}
|
||||
} else {
|
||||
this._logger.info(`Session reset`);
|
||||
private setMode(mode: TunnelMode) {
|
||||
if (isSameMode(this._mode, mode)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._mode = mode;
|
||||
this._storeMode(mode);
|
||||
this._onDidChangeModeEmitter.fire(this._mode);
|
||||
if (mode.active) {
|
||||
this._logger.info(`Session updated: ${mode.session.accountLabel} (${mode.session.providerId}) (service=${mode.asService})`);
|
||||
if (mode.session.token) {
|
||||
this._logger.info(`Session token updated: ${mode.session.accountLabel} (${mode.session.providerId})`);
|
||||
}
|
||||
} else {
|
||||
this._logger.info(`Session reset`);
|
||||
}
|
||||
}
|
||||
|
||||
async getSession(): Promise<IRemoteTunnelSession | undefined> {
|
||||
return this._session;
|
||||
getMode(): Promise<TunnelMode> {
|
||||
return Promise.resolve(this._mode);
|
||||
}
|
||||
|
||||
async initialize(session: IRemoteTunnelSession | undefined): Promise<TunnelStatus> {
|
||||
async initialize(mode: TunnelMode): Promise<TunnelStatus> {
|
||||
if (this._initialized) {
|
||||
return this._tunnelStatus;
|
||||
}
|
||||
this._initialized = true;
|
||||
this.setSession(session);
|
||||
this.setMode(mode);
|
||||
try {
|
||||
await this._startTunnelProcessDelayer.trigger(() => this.updateTunnelProcess());
|
||||
} catch (e) {
|
||||
@@ -145,6 +159,14 @@ export class RemoteTunnelService extends Disposable implements IRemoteTunnelServ
|
||||
return this._tunnelStatus;
|
||||
}
|
||||
|
||||
private readonly defaultOnOutput = (a: string, isErr: boolean) => {
|
||||
if (isErr) {
|
||||
this._logger.error(a);
|
||||
} else {
|
||||
this._logger.info(a);
|
||||
}
|
||||
};
|
||||
|
||||
private getTunnelCommandLocation() {
|
||||
if (!this._tunnelCommand) {
|
||||
let binParentLocation;
|
||||
@@ -164,11 +186,12 @@ export class RemoteTunnelService extends Disposable implements IRemoteTunnelServ
|
||||
return this._tunnelCommand;
|
||||
}
|
||||
|
||||
async startTunnel(session: IRemoteTunnelSession): Promise<TunnelStatus> {
|
||||
if (isSameSession(session, this._session) && this._tunnelStatus.type !== 'disconnected') {
|
||||
async startTunnel(mode: ActiveTunnelMode): Promise<TunnelStatus> {
|
||||
if (isSameMode(this._mode, mode) && this._tunnelStatus.type !== 'disconnected') {
|
||||
return this._tunnelStatus;
|
||||
}
|
||||
this.setSession(session);
|
||||
|
||||
this.setMode(mode);
|
||||
|
||||
try {
|
||||
await this._startTunnelProcessDelayer.trigger(() => this.updateTunnelProcess());
|
||||
@@ -180,41 +203,49 @@ export class RemoteTunnelService extends Disposable implements IRemoteTunnelServ
|
||||
|
||||
|
||||
async stopTunnel(): Promise<void> {
|
||||
this.setSession(undefined);
|
||||
|
||||
if (this._tunnelProcess) {
|
||||
this._tunnelProcess.cancel();
|
||||
this._tunnelProcess = undefined;
|
||||
}
|
||||
|
||||
const onOutput = (a: string, isErr: boolean) => {
|
||||
if (isErr) {
|
||||
this._logger.error(a);
|
||||
} else {
|
||||
this._logger.info(a);
|
||||
}
|
||||
};
|
||||
if (!this._mode.active) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Be careful to only uninstall the service if we're the ones who installed it:
|
||||
const needsServiceUninstall = this._mode.asService;
|
||||
this.setMode(INACTIVE_TUNNEL_MODE);
|
||||
|
||||
try {
|
||||
await this.runCodeTunnelCommand('stop', ['kill'], onOutput);
|
||||
if (needsServiceUninstall) {
|
||||
this.runCodeTunnelCommand('uninstallService', ['service', 'uninstall']);
|
||||
}
|
||||
} catch (e) {
|
||||
this._logger.error(e);
|
||||
}
|
||||
this.setTunnelStatus(TunnelStates.disconnected());
|
||||
|
||||
try {
|
||||
await this.runCodeTunnelCommand('stop', ['kill']);
|
||||
} catch (e) {
|
||||
this._logger.error(e);
|
||||
}
|
||||
|
||||
this.setTunnelStatus(TunnelStates.disconnected());
|
||||
}
|
||||
|
||||
private async updateTunnelProcess(): Promise<void> {
|
||||
this.telemetryService.publicLog2<RemoteTunnelEnablementEvent, RemoteTunnelEnablementClassification>('remoteTunnel.enablement', { enabled: !!this._session });
|
||||
|
||||
this.telemetryService.publicLog2<RemoteTunnelEnablementEvent, RemoteTunnelEnablementClassification>('remoteTunnel.enablement', {
|
||||
enabled: this._mode.active,
|
||||
service: this._mode.active && this._mode.asService,
|
||||
});
|
||||
|
||||
if (this._tunnelProcess) {
|
||||
this._tunnelProcess.cancel();
|
||||
this._tunnelProcess = undefined;
|
||||
}
|
||||
|
||||
let isAttached = false;
|
||||
let output = '';
|
||||
|
||||
let isServiceInstalled = false;
|
||||
const onOutput = (a: string, isErr: boolean) => {
|
||||
if (isErr) {
|
||||
this._logger.error(a);
|
||||
@@ -241,22 +272,26 @@ export class RemoteTunnelService extends Disposable implements IRemoteTunnelServ
|
||||
tunnel: object | null;
|
||||
} = JSON.parse(output.trim().split('\n').find(l => l.startsWith('{'))!);
|
||||
|
||||
isAttached = !!status.tunnel;
|
||||
this._logger.info(isAttached ? 'Other tunnel running, attaching...' : 'No other tunnel running');
|
||||
if (!isAttached && !this._session) {
|
||||
this._tunnelProcess = undefined;
|
||||
isServiceInstalled = status.service_installed;
|
||||
this._logger.info(status.tunnel ? 'Other tunnel running, attaching...' : 'No other tunnel running');
|
||||
|
||||
// If a tunnel is running but the mode isn't "active", we'll still attach
|
||||
// to the tunnel to show its state in the UI. If neither are true, disconnect
|
||||
if (!status.tunnel && !this._mode.active) {
|
||||
this.setTunnelStatus(TunnelStates.disconnected());
|
||||
return;
|
||||
}
|
||||
} catch (e) {
|
||||
this._logger.error(e);
|
||||
this._tunnelProcess = undefined;
|
||||
this.setTunnelStatus(TunnelStates.disconnected());
|
||||
return;
|
||||
} finally {
|
||||
if (this._tunnelProcess === statusProcess) {
|
||||
this._tunnelProcess = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
const session = this._session;
|
||||
|
||||
const session = this._mode.active ? this._mode.session : undefined;
|
||||
if (session && session.token) {
|
||||
const token = session.token;
|
||||
this.setTunnelStatus(TunnelStates.connecting(localize({ key: 'remoteTunnelService.authorizing', comment: ['{0} is a user account name, {1} a provider name (e.g. Github)'] }, 'Connecting as {0} ({1})', session.accountLabel, session.providerId)));
|
||||
@@ -286,25 +321,67 @@ export class RemoteTunnelService extends Disposable implements IRemoteTunnelServ
|
||||
} else {
|
||||
this.setTunnelStatus(TunnelStates.connecting(localize('remoteTunnelService.openTunnel', 'Opening tunnel')));
|
||||
}
|
||||
const args = ['--parent-process-id', String(process.pid), '--accept-server-license-terms', '--log', LogLevelToString(this._logger.getLevel())];
|
||||
const args = ['--accept-server-license-terms', '--log', LogLevelToString(this._logger.getLevel())];
|
||||
if (hostName) {
|
||||
args.push('--name', hostName);
|
||||
} else {
|
||||
args.push('--random-name');
|
||||
}
|
||||
|
||||
let serviceInstallFailed = false;
|
||||
if (this._mode.active && this._mode.asService && !isServiceInstalled) {
|
||||
// I thought about calling `code tunnel kill` here, but having multiple
|
||||
// tunnel processes running is pretty much idempotent. If there's
|
||||
// another tunnel process running, the service process will
|
||||
// take over when it exits, no hard feelings.
|
||||
serviceInstallFailed = await this.installTunnelService(args) === false;
|
||||
}
|
||||
|
||||
return this.serverOrAttachTunnel(session, args, serviceInstallFailed);
|
||||
}
|
||||
|
||||
private async installTunnelService(args: readonly string[]) {
|
||||
let status: number;
|
||||
try {
|
||||
status = await this.runCodeTunnelCommand('serviceInstall', ['service', 'install', ...args]);
|
||||
} catch (e) {
|
||||
this._logger.error(e);
|
||||
status = 1;
|
||||
}
|
||||
|
||||
if (status !== 0) {
|
||||
const msg = localize('remoteTunnelService.serviceInstallFailed', 'Failed to install tunnel as a service, starting in session...');
|
||||
this._logger.warn(msg);
|
||||
this.setTunnelStatus(TunnelStates.connecting(msg));
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private async serverOrAttachTunnel(session: IRemoteTunnelSession | undefined, args: string[], serviceInstallFailed: boolean) {
|
||||
args.push('--parent-process-id', String(process.pid));
|
||||
|
||||
if (this._preventSleep()) {
|
||||
args.push('--no-sleep');
|
||||
}
|
||||
|
||||
let isAttached = false;
|
||||
const serveCommand = this.runCodeTunnelCommand('tunnel', args, (message: string, isErr: boolean) => {
|
||||
if (isErr) {
|
||||
this._logger.error(message);
|
||||
} else {
|
||||
this._logger.info(message);
|
||||
}
|
||||
|
||||
if (message.includes('Connected to an existing tunnel process')) {
|
||||
isAttached = true;
|
||||
}
|
||||
|
||||
const m = message.match(/Open this link in your browser (https:\/\/([^\/\s]+)\/([^\/\s]+)\/([^\/\s]+))/);
|
||||
if (m) {
|
||||
const info: ConnectionInfo = { link: m[1], domain: m[2], tunnelName: m[4], isAttached };
|
||||
this.setTunnelStatus(TunnelStates.connected(info));
|
||||
this.setTunnelStatus(TunnelStates.connected(info, serviceInstallFailed));
|
||||
} else if (message.match(/error refreshing token/)) {
|
||||
serveCommand.cancel();
|
||||
this._onDidTokenFailedEmitter.fire(session);
|
||||
@@ -317,14 +394,14 @@ export class RemoteTunnelService extends Disposable implements IRemoteTunnelServ
|
||||
// process exited unexpectedly
|
||||
this._logger.info(`tunnel process terminated`);
|
||||
this._tunnelProcess = undefined;
|
||||
this._session = undefined;
|
||||
this._mode = INACTIVE_TUNNEL_MODE;
|
||||
|
||||
this.setTunnelStatus(TunnelStates.disconnected());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private runCodeTunnelCommand(logLabel: string, commandArgs: string[], onOutput: (message: string, isError: boolean) => void = () => { }): CancelablePromise<number> {
|
||||
private runCodeTunnelCommand(logLabel: string, commandArgs: string[], onOutput: (message: string, isError: boolean) => void = this.defaultOnOutput): CancelablePromise<number> {
|
||||
return createCancelablePromise<number>(token => {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (token.isCancellationRequested) {
|
||||
@@ -350,13 +427,13 @@ export class RemoteTunnelService extends Disposable implements IRemoteTunnelServ
|
||||
tunnelProcess = spawn(tunnelCommand, ['tunnel', ...commandArgs], { cwd: homedir(), stdio });
|
||||
}
|
||||
|
||||
tunnelProcess.stdout!.on('data', data => {
|
||||
tunnelProcess.stdout!.pipe(new StreamSplitter('\n')).on('data', data => {
|
||||
if (tunnelProcess) {
|
||||
const message = data.toString();
|
||||
onOutput(message, false);
|
||||
}
|
||||
});
|
||||
tunnelProcess.stderr!.on('data', data => {
|
||||
tunnelProcess.stderr!.pipe(new StreamSplitter('\n')).on('data', data => {
|
||||
if (tunnelProcess) {
|
||||
const message = data.toString();
|
||||
onOutput(message, true);
|
||||
@@ -394,30 +471,33 @@ export class RemoteTunnelService extends Disposable implements IRemoteTunnelServ
|
||||
return name || undefined;
|
||||
}
|
||||
|
||||
private _restoreSession(): IRemoteTunnelSession | undefined {
|
||||
private _restoreMode(): TunnelMode {
|
||||
try {
|
||||
const tunnelAccessSession = this.storageService.get(TUNNEL_ACCESS_SESSION, StorageScope.APPLICATION);
|
||||
const asService = this.storageService.getBoolean(TUNNEL_ACCESS_IS_SERVICE, StorageScope.APPLICATION, false);
|
||||
if (tunnelAccessSession) {
|
||||
const session = JSON.parse(tunnelAccessSession) as IRemoteTunnelSession;
|
||||
if (session && isString(session.accountLabel) && isString(session.sessionId) && isString(session.providerId)) {
|
||||
return session;
|
||||
return { active: true, session, asService };
|
||||
}
|
||||
this._logger.error('Problems restoring session from storage, invalid format', session);
|
||||
}
|
||||
} catch (e) {
|
||||
this._logger.error('Problems restoring session from storage', e);
|
||||
}
|
||||
return undefined;
|
||||
return INACTIVE_TUNNEL_MODE;
|
||||
}
|
||||
|
||||
private _storeSession(session: IRemoteTunnelSession | undefined): void {
|
||||
if (session) {
|
||||
private _storeMode(mode: TunnelMode): void {
|
||||
if (mode.active) {
|
||||
const sessionWithoutToken = {
|
||||
providerId: session.providerId, sessionId: session.sessionId, accountLabel: session.accountLabel
|
||||
providerId: mode.session.providerId, sessionId: mode.session.sessionId, accountLabel: mode.session.accountLabel
|
||||
};
|
||||
this.storageService.store(TUNNEL_ACCESS_SESSION, JSON.stringify(sessionWithoutToken), StorageScope.APPLICATION, StorageTarget.MACHINE);
|
||||
this.storageService.store(TUNNEL_ACCESS_IS_SERVICE, mode.asService, StorageScope.APPLICATION, StorageTarget.MACHINE);
|
||||
} else {
|
||||
this.storageService.remove(TUNNEL_ACCESS_SESSION, StorageScope.APPLICATION);
|
||||
this.storageService.remove(TUNNEL_ACCESS_IS_SERVICE, StorageScope.APPLICATION);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -429,3 +509,12 @@ function isSameSession(a1: IRemoteTunnelSession | undefined, a2: IRemoteTunnelSe
|
||||
return a1 === a2;
|
||||
}
|
||||
|
||||
const isSameMode = (a: TunnelMode, b: TunnelMode) => {
|
||||
if (a.active !== b.active) {
|
||||
return false;
|
||||
} else if (a.active && b.active) {
|
||||
return a.asService === b.asService && isSameSession(a.session, b.session);
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -3,40 +3,39 @@
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Disposable, DisposableStore } from 'vs/base/common/lifecycle';
|
||||
import { Action2, MenuId, registerAction2 } from 'vs/platform/actions/common/actions';
|
||||
import { IProductService } from 'vs/platform/product/common/productService';
|
||||
import { CONFIGURATION_KEY_HOST_NAME, CONFIGURATION_KEY_PREFIX, CONFIGURATION_KEY_PREVENT_SLEEP, ConnectionInfo, IRemoteTunnelSession, IRemoteTunnelService, LOGGER_NAME, LOG_ID } from 'vs/platform/remoteTunnel/common/remoteTunnel';
|
||||
import { AuthenticationSession, IAuthenticationService } from 'vs/workbench/services/authentication/common/authentication';
|
||||
import { localize } from 'vs/nls';
|
||||
import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions, IWorkbenchContribution } from 'vs/workbench/common/contributions';
|
||||
import { Registry } from 'vs/platform/registry/common/platform';
|
||||
import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle';
|
||||
import { ContextKeyExpr, IContextKey, IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey';
|
||||
import { ILocalizedString } from 'vs/platform/action/common/action';
|
||||
import { IDialogService } from 'vs/platform/dialogs/common/dialogs';
|
||||
import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage';
|
||||
import { ILogger, ILoggerService, ILogService } from 'vs/platform/log/common/log';
|
||||
import { INativeEnvironmentService } from 'vs/platform/environment/common/environment';
|
||||
import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions';
|
||||
import { IQuickInputService, IQuickPickItem, IQuickPickSeparator, QuickPickItem } from 'vs/platform/quickinput/common/quickInput';
|
||||
import { IOutputService } from 'vs/workbench/services/output/common/output';
|
||||
import { IFileService } from 'vs/platform/files/common/files';
|
||||
import { IConfigurationRegistry, Extensions as ConfigurationExtensions, ConfigurationScope } from 'vs/platform/configuration/common/configurationRegistry';
|
||||
import { IProgress, IProgressService, IProgressStep, ProgressLocation } from 'vs/platform/progress/common/progress';
|
||||
import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { INotificationService, Severity } from 'vs/platform/notification/common/notification';
|
||||
import { ICommandService } from 'vs/platform/commands/common/commands';
|
||||
import { IPreferencesService } from 'vs/workbench/services/preferences/common/preferences';
|
||||
import { IOpenerService } from 'vs/platform/opener/common/opener';
|
||||
import { Action } from 'vs/base/common/actions';
|
||||
import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService';
|
||||
import { IWorkspaceContextService, isUntitledWorkspace } from 'vs/platform/workspace/common/workspace';
|
||||
import { Disposable, DisposableStore } from 'vs/base/common/lifecycle';
|
||||
import { Schemas } from 'vs/base/common/network';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { joinPath } from 'vs/base/common/resources';
|
||||
import { ITunnelApplicationConfig } from 'vs/base/common/product';
|
||||
import { joinPath } from 'vs/base/common/resources';
|
||||
import { isNumber, isObject, isString } from 'vs/base/common/types';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { localize } from 'vs/nls';
|
||||
import { ILocalizedString } from 'vs/platform/action/common/action';
|
||||
import { Action2, MenuId, registerAction2 } from 'vs/platform/actions/common/actions';
|
||||
import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService';
|
||||
import { ICommandService } from 'vs/platform/commands/common/commands';
|
||||
import { Extensions as ConfigurationExtensions, ConfigurationScope, IConfigurationRegistry } from 'vs/platform/configuration/common/configurationRegistry';
|
||||
import { ContextKeyExpr, IContextKey, IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey';
|
||||
import { IDialogService } from 'vs/platform/dialogs/common/dialogs';
|
||||
import { INativeEnvironmentService } from 'vs/platform/environment/common/environment';
|
||||
import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { ILogger, ILoggerService } from 'vs/platform/log/common/log';
|
||||
import { INotificationService, Severity } from 'vs/platform/notification/common/notification';
|
||||
import { IOpenerService } from 'vs/platform/opener/common/opener';
|
||||
import { IProductService } from 'vs/platform/product/common/productService';
|
||||
import { IProgress, IProgressService, IProgressStep, ProgressLocation } from 'vs/platform/progress/common/progress';
|
||||
import { IQuickInputService, IQuickPickItem, IQuickPickSeparator, QuickPickItem } from 'vs/platform/quickinput/common/quickInput';
|
||||
import { Registry } from 'vs/platform/registry/common/platform';
|
||||
import { CONFIGURATION_KEY_HOST_NAME, CONFIGURATION_KEY_PREFIX, CONFIGURATION_KEY_PREVENT_SLEEP, ConnectionInfo, INACTIVE_TUNNEL_MODE, IRemoteTunnelService, IRemoteTunnelSession, LOGGER_NAME, LOG_ID, TunnelStatus } from 'vs/platform/remoteTunnel/common/remoteTunnel';
|
||||
import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage';
|
||||
import { IWorkspaceContextService, isUntitledWorkspace } from 'vs/platform/workspace/common/workspace';
|
||||
import { IWorkbenchContribution, IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions } from 'vs/workbench/common/contributions';
|
||||
import { AuthenticationSession, IAuthenticationService } from 'vs/workbench/services/authentication/common/authentication';
|
||||
import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions';
|
||||
import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle';
|
||||
import { IOutputService } from 'vs/workbench/services/output/common/output';
|
||||
import { IPreferencesService } from 'vs/workbench/services/preferences/common/preferences';
|
||||
|
||||
export const REMOTE_TUNNEL_CATEGORY: ILocalizedString = {
|
||||
original: 'Remote-Tunnels',
|
||||
@@ -103,10 +102,8 @@ export class RemoteTunnelWorkbenchContribution extends Disposable implements IWo
|
||||
@IProductService productService: IProductService,
|
||||
@IStorageService private readonly storageService: IStorageService,
|
||||
@ILoggerService loggerService: ILoggerService,
|
||||
@ILogService logService: ILogService,
|
||||
@IQuickInputService private readonly quickInputService: IQuickInputService,
|
||||
@INativeEnvironmentService private environmentService: INativeEnvironmentService,
|
||||
@IFileService fileService: IFileService,
|
||||
@IRemoteTunnelService private remoteTunnelService: IRemoteTunnelService,
|
||||
@ICommandService private commandService: ICommandService,
|
||||
@IWorkspaceContextService private workspaceContextService: IWorkspaceContextService,
|
||||
@@ -127,20 +124,7 @@ export class RemoteTunnelWorkbenchContribution extends Disposable implements IWo
|
||||
}
|
||||
this.serverConfiguration = serverConfiguration;
|
||||
|
||||
this._register(this.remoteTunnelService.onDidChangeTunnelStatus(status => {
|
||||
this.connectionInfo = undefined;
|
||||
if (status.type === 'disconnected') {
|
||||
if (status.onTokenFailed) {
|
||||
this.expiredSessions.add(status.onTokenFailed.sessionId);
|
||||
}
|
||||
this.connectionStateContext.set('disconnected');
|
||||
} else if (status.type === 'connecting') {
|
||||
this.connectionStateContext.set('connecting');
|
||||
} else if (status.type === 'connected') {
|
||||
this.connectionInfo = status.info;
|
||||
this.connectionStateContext.set('connected');
|
||||
}
|
||||
}));
|
||||
this._register(this.remoteTunnelService.onDidChangeTunnelStatus(s => this.handleTunnelStatusUpdate(s)));
|
||||
|
||||
this.registerCommands();
|
||||
|
||||
@@ -149,6 +133,21 @@ export class RemoteTunnelWorkbenchContribution extends Disposable implements IWo
|
||||
this.recommendRemoteExtensionIfNeeded();
|
||||
}
|
||||
|
||||
private handleTunnelStatusUpdate(status: TunnelStatus) {
|
||||
this.connectionInfo = undefined;
|
||||
if (status.type === 'disconnected') {
|
||||
if (status.onTokenFailed) {
|
||||
this.expiredSessions.add(status.onTokenFailed.sessionId);
|
||||
}
|
||||
this.connectionStateContext.set('disconnected');
|
||||
} else if (status.type === 'connecting') {
|
||||
this.connectionStateContext.set('connecting');
|
||||
} else if (status.type === 'connected') {
|
||||
this.connectionInfo = status.info;
|
||||
this.connectionStateContext.set('connected');
|
||||
}
|
||||
}
|
||||
|
||||
private async recommendRemoteExtensionIfNeeded() {
|
||||
await this.extensionService.whenInstalledExtensionsRegistered();
|
||||
|
||||
@@ -228,10 +227,17 @@ export class RemoteTunnelWorkbenchContribution extends Disposable implements IWo
|
||||
}
|
||||
|
||||
private async initialize(): Promise<void> {
|
||||
const session = await this.remoteTunnelService.getSession();
|
||||
if (session && session.token) {
|
||||
const [mode, status] = await Promise.all([
|
||||
this.remoteTunnelService.getMode(),
|
||||
this.remoteTunnelService.getTunnelStatus(),
|
||||
]);
|
||||
|
||||
this.handleTunnelStatusUpdate(status);
|
||||
|
||||
if (mode.active && mode.session.token) {
|
||||
return; // already initialized, token available
|
||||
}
|
||||
|
||||
return await this.progressService.withProgress(
|
||||
{
|
||||
location: ProgressLocation.Window,
|
||||
@@ -248,13 +254,13 @@ export class RemoteTunnelWorkbenchContribution extends Disposable implements IWo
|
||||
}
|
||||
});
|
||||
let newSession: IRemoteTunnelSession | undefined;
|
||||
if (session) {
|
||||
const token = await this.getSessionToken(session);
|
||||
if (mode.active) {
|
||||
const token = await this.getSessionToken(mode.session);
|
||||
if (token) {
|
||||
newSession = { ...session, token };
|
||||
newSession = { ...mode.session, token };
|
||||
}
|
||||
}
|
||||
const status = await this.remoteTunnelService.initialize(newSession);
|
||||
const status = await this.remoteTunnelService.initialize(mode.active && newSession ? { ...mode, session: newSession } : INACTIVE_TUNNEL_MODE);
|
||||
listener.dispose();
|
||||
|
||||
if (status.type === 'connected') {
|
||||
@@ -267,7 +273,7 @@ export class RemoteTunnelWorkbenchContribution extends Disposable implements IWo
|
||||
}
|
||||
|
||||
|
||||
private async startTunnel(): Promise<ConnectionInfo | undefined> {
|
||||
private async startTunnel(asService: boolean): Promise<ConnectionInfo | undefined> {
|
||||
if (this.connectionInfo) {
|
||||
return this.connectionInfo;
|
||||
}
|
||||
@@ -301,6 +307,19 @@ export class RemoteTunnelWorkbenchContribution extends Disposable implements IWo
|
||||
listener.dispose();
|
||||
completed = true;
|
||||
s(status.info);
|
||||
if (status.serviceInstallFailed) {
|
||||
this.notificationService.notify({
|
||||
severity: Severity.Warning,
|
||||
message: localize(
|
||||
{
|
||||
key: 'remoteTunnel.serviceInstallFailed',
|
||||
comment: ['{Locked="](command:{0})"}']
|
||||
},
|
||||
"Installation as a service failed, and we fell back to running the tunnel for this session. See the [error log](command:{0}) for details.",
|
||||
RemoteTunnelCommandIds.showLog,
|
||||
),
|
||||
});
|
||||
}
|
||||
break;
|
||||
case 'disconnected':
|
||||
listener.dispose();
|
||||
@@ -312,7 +331,7 @@ export class RemoteTunnelWorkbenchContribution extends Disposable implements IWo
|
||||
});
|
||||
const token = authenticationSession.session.idToken ?? authenticationSession.session.accessToken;
|
||||
const account: IRemoteTunnelSession = { sessionId: authenticationSession.session.id, token, providerId: authenticationSession.providerId, accountLabel: authenticationSession.session.account.label };
|
||||
this.remoteTunnelService.startTunnel(account).then(status => {
|
||||
this.remoteTunnelService.startTunnel({ active: true, asService, session: account }).then(status => {
|
||||
if (!completed && (status.type === 'connected' || status.type === 'disconnected')) {
|
||||
listener.dispose();
|
||||
if (status.type === 'connected') {
|
||||
@@ -403,7 +422,7 @@ export class RemoteTunnelWorkbenchContribution extends Disposable implements IWo
|
||||
private async getAllSessions(): Promise<ExistingSessionItem[]> {
|
||||
const authenticationProviders = await this.getAuthenticationProviders();
|
||||
const accounts = new Map<string, ExistingSessionItem>();
|
||||
const currentAccount = await this.remoteTunnelService.getSession();
|
||||
const currentAccount = await this.remoteTunnelService.getMode();
|
||||
let currentSession: ExistingSessionItem | undefined;
|
||||
|
||||
for (const provider of authenticationProviders) {
|
||||
@@ -413,7 +432,7 @@ export class RemoteTunnelWorkbenchContribution extends Disposable implements IWo
|
||||
if (!this.expiredSessions.has(session.id)) {
|
||||
const item = this.createExistingSessionItem(session, provider.id);
|
||||
accounts.set(item.session.account.id, item);
|
||||
if (currentAccount && currentAccount.sessionId === session.id) {
|
||||
if (currentAccount.active && currentAccount.session.sessionId === session.id) {
|
||||
currentSession = item;
|
||||
}
|
||||
}
|
||||
@@ -483,6 +502,8 @@ export class RemoteTunnelWorkbenchContribution extends Disposable implements IWo
|
||||
const commandService = accessor.get(ICommandService);
|
||||
const storageService = accessor.get(IStorageService);
|
||||
const dialogService = accessor.get(IDialogService);
|
||||
const quickInputService = accessor.get(IQuickInputService);
|
||||
const productService = accessor.get(IProductService);
|
||||
|
||||
const didNotifyPreview = storageService.getBoolean(REMOTE_TUNNEL_PROMPTED_PREVIEW_STORAGE_KEY, StorageScope.APPLICATION, false);
|
||||
if (!didNotifyPreview) {
|
||||
@@ -497,12 +518,33 @@ export class RemoteTunnelWorkbenchContribution extends Disposable implements IWo
|
||||
storageService.store(REMOTE_TUNNEL_PROMPTED_PREVIEW_STORAGE_KEY, true, StorageScope.APPLICATION, StorageTarget.USER);
|
||||
}
|
||||
|
||||
const connectionInfo = await that.startTunnel();
|
||||
const disposables = new DisposableStore();
|
||||
const quickPick = quickInputService.createQuickPick<IQuickPickItem & { service: boolean }>();
|
||||
quickPick.placeholder = localize('tunnel.enable.placeholder', 'Select how you want to enable access');
|
||||
quickPick.items = [
|
||||
{ service: false, label: localize('tunnel.enable.session', 'Turn on for this session'), description: localize('tunnel.enable.session.description', 'Run whenever {0} is open', productService.nameShort) },
|
||||
{ service: true, label: localize('tunnel.enable.service', 'Install as a service'), description: localize('tunnel.enable.service.description', 'Run whenever you\'re logged in') }
|
||||
];
|
||||
|
||||
const asService = await new Promise<boolean | undefined>(resolve => {
|
||||
disposables.add(quickPick.onDidAccept(() => resolve(quickPick.selectedItems[0]?.service)));
|
||||
disposables.add(quickPick.onDidHide(() => resolve(undefined)));
|
||||
quickPick.show();
|
||||
});
|
||||
|
||||
quickPick.dispose();
|
||||
|
||||
if (asService === undefined) {
|
||||
return; // no-op
|
||||
}
|
||||
|
||||
const connectionInfo = await that.startTunnel(/* installAsService= */ asService);
|
||||
|
||||
if (connectionInfo) {
|
||||
const linkToOpen = that.getLinkToOpen(connectionInfo);
|
||||
const remoteExtension = that.serverConfiguration.extension;
|
||||
const linkToOpenForMarkdown = linkToOpen.toString(false).replace(/\)/g, '%29');
|
||||
await notificationService.notify({
|
||||
notificationService.notify({
|
||||
severity: Severity.Info,
|
||||
message:
|
||||
localize(
|
||||
@@ -525,7 +567,7 @@ export class RemoteTunnelWorkbenchContribution extends Disposable implements IWo
|
||||
const usedOnHostMessage: UsedOnHostMessage = { hostName: connectionInfo.tunnelName, timeStamp: new Date().getTime() };
|
||||
storageService.store(REMOTE_TUNNEL_USED_STORAGE_KEY, JSON.stringify(usedOnHostMessage), StorageScope.APPLICATION, StorageTarget.USER);
|
||||
} else {
|
||||
await notificationService.notify({
|
||||
notificationService.notify({
|
||||
severity: Severity.Info,
|
||||
message: localize('progress.turnOn.failed',
|
||||
"Unable to turn on the remote tunnel access. Check the Remote Tunnel Service log for details."),
|
||||
@@ -699,7 +741,7 @@ export class RemoteTunnelWorkbenchContribution extends Disposable implements IWo
|
||||
|
||||
|
||||
private async showManageOptions() {
|
||||
const account = await this.remoteTunnelService.getSession();
|
||||
const account = await this.remoteTunnelService.getMode();
|
||||
|
||||
return new Promise<void>((c, e) => {
|
||||
const disposables = new DisposableStore();
|
||||
@@ -721,7 +763,7 @@ export class RemoteTunnelWorkbenchContribution extends Disposable implements IWo
|
||||
items.push({ id: RemoteTunnelCommandIds.showLog, label: localize('manage.showLog', 'Show Log') });
|
||||
items.push({ type: 'separator' });
|
||||
items.push({ id: RemoteTunnelCommandIds.configure, label: localize('manage.tunnelName', 'Change Tunnel Name'), description: this.connectionInfo?.tunnelName });
|
||||
items.push({ id: RemoteTunnelCommandIds.turnOff, label: RemoteTunnelCommandLabels.turnOff, description: account ? `${account.accountLabel} (${account.providerId})` : undefined });
|
||||
items.push({ id: RemoteTunnelCommandIds.turnOff, label: RemoteTunnelCommandLabels.turnOff, description: account.active ? `${account.session.accountLabel} (${account.session.providerId})` : undefined });
|
||||
|
||||
quickPick.items = items;
|
||||
disposables.add(quickPick.onDidAccept(() => {
|
||||
|
||||
Reference in New Issue
Block a user