mirror of
https://github.com/microsoft/vscode.git
synced 2025-12-24 20:26:08 +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.
|
/// If set, the user accepts the server license terms and the server will be started without a user prompt.
|
||||||
#[clap(long)]
|
#[clap(long)]
|
||||||
pub accept_server_license_terms: bool,
|
pub accept_server_license_terms: bool,
|
||||||
|
|
||||||
|
/// Sets the machine name for port forwarding service
|
||||||
|
#[clap(long)]
|
||||||
|
pub name: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Args, Debug, Clone)]
|
#[derive(Args, Debug, Clone)]
|
||||||
|
|||||||
@@ -135,10 +135,17 @@ pub async fn service(
|
|||||||
let manager = create_service_manager(ctx.log.clone(), &ctx.paths);
|
let manager = create_service_manager(ctx.log.clone(), &ctx.paths);
|
||||||
match service_args {
|
match service_args {
|
||||||
TunnelServiceSubCommands::Install(args) => {
|
TunnelServiceSubCommands::Install(args) => {
|
||||||
// ensure logged in, otherwise subsequent serving will fail
|
let auth = Auth::new(&ctx.paths, ctx.log.clone());
|
||||||
Auth::new(&ctx.paths, ctx.log.clone())
|
|
||||||
.get_credential()
|
if let Some(name) = &args.name {
|
||||||
.await?;
|
// 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
|
// likewise for license consent
|
||||||
legal::require_consent(&ctx.paths, args.accept_server_license_terms)?;
|
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)
|
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> {
|
pub async fn rename(ctx: CommandContext, rename_args: TunnelRenameArgs) -> Result<i32, AnyError> {
|
||||||
let auth = Auth::new(&ctx.paths, ctx.log.clone());
|
let auth = Auth::new(&ctx.paths, ctx.log.clone());
|
||||||
let mut dt = dev_tunnels::DevTunnels::new(&ctx.log, auth, &ctx.paths);
|
let mut dt = dev_tunnels::DevTunnels::new(&ctx.log, auth, &ctx.paths);
|
||||||
dt.rename_tunnel(&rename_args.name).await?;
|
dt.rename_tunnel(&rename_args.name).await?;
|
||||||
ctx.log.result(format!(
|
ctx.log.result(format!(
|
||||||
"Successfully renamed this gateway to {}",
|
"Successfully renamed this tunnel to {}",
|
||||||
&rename_args.name
|
&rename_args.name
|
||||||
));
|
));
|
||||||
|
|
||||||
Ok(0)
|
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> {
|
pub async fn unregister(ctx: CommandContext) -> Result<i32, AnyError> {
|
||||||
let auth = Auth::new(&ctx.paths, ctx.log.clone());
|
let auth = Auth::new(&ctx.paths, ctx.log.clone());
|
||||||
let mut dt = dev_tunnels::DevTunnels::new(&ctx.log, auth, &ctx.paths);
|
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,
|
Ok(p) => p,
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
return id.map(|id| {
|
return id.map(|id| {
|
||||||
serial.serialize(&ErrorResponse {
|
serial.serialize(ErrorResponse {
|
||||||
id,
|
id,
|
||||||
error: ResponseError {
|
error: ResponseError {
|
||||||
code: 0,
|
code: 0,
|
||||||
@@ -131,7 +131,7 @@ impl<S: Serialization, C: Send + Sync + 'static> RpcMethodBuilder<S, C> {
|
|||||||
match callback(param.params, &context) {
|
match callback(param.params, &context) {
|
||||||
Ok(result) => id.map(|id| serial.serialize(&SuccessResponse { id, result })),
|
Ok(result) => id.map(|id| serial.serialize(&SuccessResponse { id, result })),
|
||||||
Err(err) => id.map(|id| {
|
Err(err) => id.map(|id| {
|
||||||
serial.serialize(&ErrorResponse {
|
serial.serialize(ErrorResponse {
|
||||||
id,
|
id,
|
||||||
error: ResponseError {
|
error: ResponseError {
|
||||||
code: -1,
|
code: -1,
|
||||||
@@ -161,7 +161,7 @@ impl<S: Serialization, C: Send + Sync + 'static> RpcMethodBuilder<S, C> {
|
|||||||
Ok(p) => p,
|
Ok(p) => p,
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
return future::ready(id.map(|id| {
|
return future::ready(id.map(|id| {
|
||||||
serial.serialize(&ErrorResponse {
|
serial.serialize(ErrorResponse {
|
||||||
id,
|
id,
|
||||||
error: ResponseError {
|
error: ResponseError {
|
||||||
code: 0,
|
code: 0,
|
||||||
@@ -182,7 +182,7 @@ impl<S: Serialization, C: Send + Sync + 'static> RpcMethodBuilder<S, C> {
|
|||||||
id.map(|id| serial.serialize(&SuccessResponse { id, result }))
|
id.map(|id| serial.serialize(&SuccessResponse { id, result }))
|
||||||
}
|
}
|
||||||
Err(err) => id.map(|id| {
|
Err(err) => id.map(|id| {
|
||||||
serial.serialize(&ErrorResponse {
|
serial.serialize(ErrorResponse {
|
||||||
id,
|
id,
|
||||||
error: ResponseError {
|
error: ResponseError {
|
||||||
code: -1,
|
code: -1,
|
||||||
@@ -222,7 +222,7 @@ impl<S: Serialization, C: Send + Sync + 'static> RpcMethodBuilder<S, C> {
|
|||||||
return (
|
return (
|
||||||
None,
|
None,
|
||||||
future::ready(id.map(|id| {
|
future::ready(id.map(|id| {
|
||||||
serial.serialize(&ErrorResponse {
|
serial.serialize(ErrorResponse {
|
||||||
id,
|
id,
|
||||||
error: ResponseError {
|
error: ResponseError {
|
||||||
code: 0,
|
code: 0,
|
||||||
@@ -255,7 +255,7 @@ impl<S: Serialization, C: Send + Sync + 'static> RpcMethodBuilder<S, C> {
|
|||||||
match callback(servers, param.params, context).await {
|
match callback(servers, param.params, context).await {
|
||||||
Ok(r) => id.map(|id| serial.serialize(&SuccessResponse { id, result: r })),
|
Ok(r) => id.map(|id| serial.serialize(&SuccessResponse { id, result: r })),
|
||||||
Err(err) => id.map(|id| {
|
Err(err) => id.map(|id| {
|
||||||
serial.serialize(&ErrorResponse {
|
serial.serialize(ErrorResponse {
|
||||||
id,
|
id,
|
||||||
error: ResponseError {
|
error: ResponseError {
|
||||||
code: -1,
|
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::Async(callback)) => MaybeSync::Future(callback(id, body)),
|
||||||
Some(Method::Duplex(callback)) => MaybeSync::Stream(callback(id, body)),
|
Some(Method::Duplex(callback)) => MaybeSync::Stream(callback(id, body)),
|
||||||
None => MaybeSync::Sync(id.map(|id| {
|
None => MaybeSync::Sync(id.map(|id| {
|
||||||
self.serializer.serialize(&ErrorResponse {
|
self.serializer.serialize(ErrorResponse {
|
||||||
id,
|
id,
|
||||||
error: ResponseError {
|
error: ResponseError {
|
||||||
code: -1,
|
code: -1,
|
||||||
|
|||||||
@@ -275,7 +275,9 @@ impl DevTunnels {
|
|||||||
|
|
||||||
/// Renames the current tunnel to the new name.
|
/// Renames the current tunnel to the new name.
|
||||||
pub async fn rename_tunnel(&mut self, name: &str) -> Result<(), AnyError> {
|
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.
|
/// Updates the name of the existing persisted tunnel to the new name.
|
||||||
@@ -286,28 +288,34 @@ impl DevTunnels {
|
|||||||
name: &str,
|
name: &str,
|
||||||
) -> Result<(Tunnel, PersistedTunnel), AnyError> {
|
) -> Result<(Tunnel, PersistedTunnel), AnyError> {
|
||||||
let name = name.to_ascii_lowercase();
|
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 {
|
let (mut full_tunnel, mut persisted, is_new) = match persisted {
|
||||||
Some(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)
|
self.get_or_create_tunnel(persisted, Some(&name), NO_REQUEST_OPTIONS)
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
None => self
|
None => {
|
||||||
.create_tunnel(&name, NO_REQUEST_OPTIONS)
|
debug!(self.log, "Creating a new tunnel with the requested name");
|
||||||
.await
|
self.create_tunnel(&name, NO_REQUEST_OPTIONS)
|
||||||
.map(|(pt, t)| (t, pt, true)),
|
.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));
|
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,
|
||||||
self.log.span("dev-tunnel.tag.update"),
|
self.log.span("dev-tunnel.tag.update"),
|
||||||
self.client.update_tunnel(&full_tunnel, NO_REQUEST_OPTIONS)
|
self.client.update_tunnel(&full_tunnel, NO_REQUEST_OPTIONS)
|
||||||
@@ -317,7 +325,7 @@ impl DevTunnels {
|
|||||||
persisted.name = name;
|
persisted.name = name;
|
||||||
self.launcher_tunnel.save(Some(persisted.clone()))?;
|
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.
|
/// Gets the persisted tunnel from the service, or creates a new one.
|
||||||
@@ -443,6 +451,8 @@ impl DevTunnels {
|
|||||||
) -> Result<(PersistedTunnel, Tunnel), AnyError> {
|
) -> Result<(PersistedTunnel, Tunnel), AnyError> {
|
||||||
info!(self.log, "Creating tunnel with the name: {}", name);
|
info!(self.log, "Creating tunnel with the name: {}", name);
|
||||||
|
|
||||||
|
self.check_is_name_free(name).await?;
|
||||||
|
|
||||||
let mut tried_recycle = false;
|
let mut tried_recycle = false;
|
||||||
|
|
||||||
let new_tunnel = Tunnel {
|
let new_tunnel = Tunnel {
|
||||||
@@ -527,7 +537,7 @@ impl DevTunnels {
|
|||||||
options: &TunnelRequestOptions,
|
options: &TunnelRequestOptions,
|
||||||
) -> Result<Tunnel, AnyError> {
|
) -> Result<Tunnel, AnyError> {
|
||||||
let new_tags = self.get_tags(name);
|
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);
|
return Ok(tunnel);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -610,7 +620,7 @@ impl DevTunnels {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn check_is_name_free(&mut self, name: &str) -> Result<(), AnyError> {
|
async fn check_is_name_free(&mut self, name: &str) -> Result<(), AnyError> {
|
||||||
let existing = spanf!(
|
let existing: Vec<Tunnel> = spanf!(
|
||||||
self.log,
|
self.log,
|
||||||
self.log.span("dev-tunnel.rename.search"),
|
self.log.span("dev-tunnel.rename.search"),
|
||||||
self.client.list_all_tunnels(&TunnelRequestOptions {
|
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() {
|
if a.len() != b.len() {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -78,6 +78,7 @@ impl CliServiceManager for WindowsService {
|
|||||||
cmd.stderr(Stdio::null());
|
cmd.stderr(Stdio::null());
|
||||||
cmd.stdout(Stdio::null());
|
cmd.stdout(Stdio::null());
|
||||||
cmd.stdin(Stdio::null());
|
cmd.stdin(Stdio::null());
|
||||||
|
cmd.creation_flags(CREATE_NEW_PROCESS_GROUP | DETACHED_PROCESS);
|
||||||
cmd.spawn()
|
cmd.spawn()
|
||||||
.map_err(|e| wrapdbg(e, "error starting service"))?;
|
.map_err(|e| wrapdbg(e, "error starting service"))?;
|
||||||
|
|
||||||
@@ -121,8 +122,12 @@ impl CliServiceManager for WindowsService {
|
|||||||
|
|
||||||
async fn unregister(&self) -> Result<(), AnyError> {
|
async fn unregister(&self) -> Result<(), AnyError> {
|
||||||
let key = WindowsService::open_key()?;
|
let key = WindowsService::open_key()?;
|
||||||
key.delete_value(TUNNEL_ACTIVITY_NAME)
|
match key.delete_value(TUNNEL_ACTIVITY_NAME) {
|
||||||
.map_err(|e| AnyError::from(wrap(e, "error deleting registry key")))?;
|
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");
|
info!(self.log, "Tunnel service uninstalled");
|
||||||
|
|
||||||
let r = do_single_rpc_call::<_, ()>(
|
let r = do_single_rpc_call::<_, ()>(
|
||||||
|
|||||||
@@ -172,53 +172,59 @@ export class VSBuffer {
|
|||||||
writeUInt8(this.buffer, value, offset);
|
writeUInt8(this.buffer, value, offset);
|
||||||
}
|
}
|
||||||
|
|
||||||
indexOf(subarray: VSBuffer | Uint8Array) {
|
indexOf(subarray: VSBuffer | Uint8Array, offset = 0) {
|
||||||
const needle = subarray instanceof VSBuffer ? subarray.buffer : subarray;
|
return binaryIndexOf(this.buffer, subarray instanceof VSBuffer ? subarray.buffer : subarray, offset);
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 {
|
export function readUInt16LE(source: Uint8Array, offset: number): number {
|
||||||
return (
|
return (
|
||||||
((source[offset + 0] << 0) >>> 0) |
|
((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>;
|
readonly onDidChangeTunnelStatus: Event<TunnelStatus>;
|
||||||
getTunnelStatus(): Promise<TunnelStatus>;
|
getTunnelStatus(): Promise<TunnelStatus>;
|
||||||
|
|
||||||
getSession(): Promise<IRemoteTunnelSession | undefined>;
|
getMode(): Promise<TunnelMode>;
|
||||||
readonly onDidChangeSession: Event<IRemoteTunnelSession | undefined>;
|
readonly onDidChangeMode: Event<TunnelMode>;
|
||||||
|
|
||||||
readonly onDidTokenFailed: Event<IRemoteTunnelSession | undefined>;
|
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>;
|
stopTunnel(): Promise<void>;
|
||||||
getTunnelName(): Promise<string | undefined>;
|
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 type TunnelStatus = TunnelStates.Connected | TunnelStates.Disconnected | TunnelStates.Connecting | TunnelStates.Uninitialized;
|
||||||
|
|
||||||
export namespace TunnelStates {
|
export namespace TunnelStates {
|
||||||
@@ -46,13 +61,14 @@ export namespace TunnelStates {
|
|||||||
export interface Connected {
|
export interface Connected {
|
||||||
readonly type: 'connected';
|
readonly type: 'connected';
|
||||||
readonly info: ConnectionInfo;
|
readonly info: ConnectionInfo;
|
||||||
|
readonly serviceInstallFailed: boolean;
|
||||||
}
|
}
|
||||||
export interface Disconnected {
|
export interface Disconnected {
|
||||||
readonly type: 'disconnected';
|
readonly type: 'disconnected';
|
||||||
readonly onTokenFailed?: IRemoteTunnelSession;
|
readonly onTokenFailed?: IRemoteTunnelSession;
|
||||||
}
|
}
|
||||||
export const disconnected = (onTokenFailed?: IRemoteTunnelSession): Disconnected => ({ type: 'disconnected', onTokenFailed });
|
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 connecting = (progress?: string): Connecting => ({ type: 'connecting', progress });
|
||||||
export const uninitialized: Uninitialized = { type: 'uninitialized' };
|
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.
|
* 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 { Emitter } from 'vs/base/common/event';
|
||||||
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
|
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
|
||||||
import { INativeEnvironmentService } from 'vs/platform/environment/common/environment';
|
import { INativeEnvironmentService } from 'vs/platform/environment/common/environment';
|
||||||
@@ -20,15 +20,18 @@ import { localize } from 'vs/nls';
|
|||||||
import { hostname, homedir } from 'os';
|
import { hostname, homedir } from 'os';
|
||||||
import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage';
|
import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage';
|
||||||
import { isString } from 'vs/base/common/types';
|
import { isString } from 'vs/base/common/types';
|
||||||
|
import { StreamSplitter } from 'vs/base/node/nodeStreams';
|
||||||
|
|
||||||
type RemoteTunnelEnablementClassification = {
|
type RemoteTunnelEnablementClassification = {
|
||||||
owner: 'aeschli';
|
owner: 'aeschli';
|
||||||
comment: 'Reporting when Remote Tunnel access is turned on or off';
|
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' };
|
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 = {
|
type RemoteTunnelEnablementEvent = {
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
|
service: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
const restartTunnelOnConfigurationChanges: readonly string[] = [
|
const restartTunnelOnConfigurationChanges: readonly string[] = [
|
||||||
@@ -40,6 +43,8 @@ const restartTunnelOnConfigurationChanges: readonly string[] = [
|
|||||||
// if set, the remote tunnel access is currently enabled.
|
// if set, the remote tunnel access is currently enabled.
|
||||||
// if not set, the remote tunnel access is currently disabled.
|
// if not set, the remote tunnel access is currently disabled.
|
||||||
const TUNNEL_ACCESS_SESSION = 'remoteTunnelSession';
|
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
|
* 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>();
|
private readonly _onDidChangeTunnelStatusEmitter = new Emitter<TunnelStatus>();
|
||||||
public readonly onDidChangeTunnelStatus = this._onDidChangeTunnelStatusEmitter.event;
|
public readonly onDidChangeTunnelStatus = this._onDidChangeTunnelStatusEmitter.event;
|
||||||
|
|
||||||
private readonly _onDidChangeSessionEmitter = new Emitter<IRemoteTunnelSession | undefined>();
|
private readonly _onDidChangeModeEmitter = new Emitter<TunnelMode>();
|
||||||
public readonly onDidChangeSession = this._onDidChangeSessionEmitter.event;
|
public readonly onDidChangeMode = this._onDidChangeModeEmitter.event;
|
||||||
|
|
||||||
private readonly _logger: ILogger;
|
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;
|
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;
|
this._tunnelStatus = TunnelStates.uninitialized;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -111,32 +123,34 @@ export class RemoteTunnelService extends Disposable implements IRemoteTunnelServ
|
|||||||
this._onDidChangeTunnelStatusEmitter.fire(tunnelStatus);
|
this._onDidChangeTunnelStatusEmitter.fire(tunnelStatus);
|
||||||
}
|
}
|
||||||
|
|
||||||
private setSession(session: IRemoteTunnelSession | undefined) {
|
private setMode(mode: TunnelMode) {
|
||||||
if (!isSameSession(session, this._session)) {
|
if (isSameMode(this._mode, mode)) {
|
||||||
this._session = session;
|
return;
|
||||||
this._onDidChangeSessionEmitter.fire(session);
|
}
|
||||||
this._storeSession(session);
|
|
||||||
if (session) {
|
this._mode = mode;
|
||||||
this._logger.info(`Session updated: ${session.accountLabel} (${session.providerId})`);
|
this._storeMode(mode);
|
||||||
if (session.token) {
|
this._onDidChangeModeEmitter.fire(this._mode);
|
||||||
this._logger.info(`Session token updated: ${session.accountLabel} (${session.providerId})`);
|
if (mode.active) {
|
||||||
}
|
this._logger.info(`Session updated: ${mode.session.accountLabel} (${mode.session.providerId}) (service=${mode.asService})`);
|
||||||
} else {
|
if (mode.session.token) {
|
||||||
this._logger.info(`Session reset`);
|
this._logger.info(`Session token updated: ${mode.session.accountLabel} (${mode.session.providerId})`);
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
this._logger.info(`Session reset`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async getSession(): Promise<IRemoteTunnelSession | undefined> {
|
getMode(): Promise<TunnelMode> {
|
||||||
return this._session;
|
return Promise.resolve(this._mode);
|
||||||
}
|
}
|
||||||
|
|
||||||
async initialize(session: IRemoteTunnelSession | undefined): Promise<TunnelStatus> {
|
async initialize(mode: TunnelMode): Promise<TunnelStatus> {
|
||||||
if (this._initialized) {
|
if (this._initialized) {
|
||||||
return this._tunnelStatus;
|
return this._tunnelStatus;
|
||||||
}
|
}
|
||||||
this._initialized = true;
|
this._initialized = true;
|
||||||
this.setSession(session);
|
this.setMode(mode);
|
||||||
try {
|
try {
|
||||||
await this._startTunnelProcessDelayer.trigger(() => this.updateTunnelProcess());
|
await this._startTunnelProcessDelayer.trigger(() => this.updateTunnelProcess());
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -145,6 +159,14 @@ export class RemoteTunnelService extends Disposable implements IRemoteTunnelServ
|
|||||||
return this._tunnelStatus;
|
return this._tunnelStatus;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private readonly defaultOnOutput = (a: string, isErr: boolean) => {
|
||||||
|
if (isErr) {
|
||||||
|
this._logger.error(a);
|
||||||
|
} else {
|
||||||
|
this._logger.info(a);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
private getTunnelCommandLocation() {
|
private getTunnelCommandLocation() {
|
||||||
if (!this._tunnelCommand) {
|
if (!this._tunnelCommand) {
|
||||||
let binParentLocation;
|
let binParentLocation;
|
||||||
@@ -164,11 +186,12 @@ export class RemoteTunnelService extends Disposable implements IRemoteTunnelServ
|
|||||||
return this._tunnelCommand;
|
return this._tunnelCommand;
|
||||||
}
|
}
|
||||||
|
|
||||||
async startTunnel(session: IRemoteTunnelSession): Promise<TunnelStatus> {
|
async startTunnel(mode: ActiveTunnelMode): Promise<TunnelStatus> {
|
||||||
if (isSameSession(session, this._session) && this._tunnelStatus.type !== 'disconnected') {
|
if (isSameMode(this._mode, mode) && this._tunnelStatus.type !== 'disconnected') {
|
||||||
return this._tunnelStatus;
|
return this._tunnelStatus;
|
||||||
}
|
}
|
||||||
this.setSession(session);
|
|
||||||
|
this.setMode(mode);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await this._startTunnelProcessDelayer.trigger(() => this.updateTunnelProcess());
|
await this._startTunnelProcessDelayer.trigger(() => this.updateTunnelProcess());
|
||||||
@@ -180,41 +203,49 @@ export class RemoteTunnelService extends Disposable implements IRemoteTunnelServ
|
|||||||
|
|
||||||
|
|
||||||
async stopTunnel(): Promise<void> {
|
async stopTunnel(): Promise<void> {
|
||||||
this.setSession(undefined);
|
|
||||||
|
|
||||||
if (this._tunnelProcess) {
|
if (this._tunnelProcess) {
|
||||||
this._tunnelProcess.cancel();
|
this._tunnelProcess.cancel();
|
||||||
this._tunnelProcess = undefined;
|
this._tunnelProcess = undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
const onOutput = (a: string, isErr: boolean) => {
|
if (!this._mode.active) {
|
||||||
if (isErr) {
|
return;
|
||||||
this._logger.error(a);
|
}
|
||||||
} else {
|
|
||||||
this._logger.info(a);
|
// 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 {
|
try {
|
||||||
await this.runCodeTunnelCommand('stop', ['kill'], onOutput);
|
if (needsServiceUninstall) {
|
||||||
|
this.runCodeTunnelCommand('uninstallService', ['service', 'uninstall']);
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this._logger.error(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> {
|
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) {
|
if (this._tunnelProcess) {
|
||||||
this._tunnelProcess.cancel();
|
this._tunnelProcess.cancel();
|
||||||
this._tunnelProcess = undefined;
|
this._tunnelProcess = undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
let isAttached = false;
|
|
||||||
let output = '';
|
let output = '';
|
||||||
|
let isServiceInstalled = false;
|
||||||
const onOutput = (a: string, isErr: boolean) => {
|
const onOutput = (a: string, isErr: boolean) => {
|
||||||
if (isErr) {
|
if (isErr) {
|
||||||
this._logger.error(a);
|
this._logger.error(a);
|
||||||
@@ -241,22 +272,26 @@ export class RemoteTunnelService extends Disposable implements IRemoteTunnelServ
|
|||||||
tunnel: object | null;
|
tunnel: object | null;
|
||||||
} = JSON.parse(output.trim().split('\n').find(l => l.startsWith('{'))!);
|
} = JSON.parse(output.trim().split('\n').find(l => l.startsWith('{'))!);
|
||||||
|
|
||||||
isAttached = !!status.tunnel;
|
isServiceInstalled = status.service_installed;
|
||||||
this._logger.info(isAttached ? 'Other tunnel running, attaching...' : 'No other tunnel running');
|
this._logger.info(status.tunnel ? 'Other tunnel running, attaching...' : 'No other tunnel running');
|
||||||
if (!isAttached && !this._session) {
|
|
||||||
this._tunnelProcess = undefined;
|
// 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());
|
this.setTunnelStatus(TunnelStates.disconnected());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this._logger.error(e);
|
this._logger.error(e);
|
||||||
this._tunnelProcess = undefined;
|
|
||||||
this.setTunnelStatus(TunnelStates.disconnected());
|
this.setTunnelStatus(TunnelStates.disconnected());
|
||||||
return;
|
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) {
|
if (session && session.token) {
|
||||||
const token = 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)));
|
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 {
|
} else {
|
||||||
this.setTunnelStatus(TunnelStates.connecting(localize('remoteTunnelService.openTunnel', 'Opening tunnel')));
|
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) {
|
if (hostName) {
|
||||||
args.push('--name', hostName);
|
args.push('--name', hostName);
|
||||||
} else {
|
} else {
|
||||||
args.push('--random-name');
|
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()) {
|
if (this._preventSleep()) {
|
||||||
args.push('--no-sleep');
|
args.push('--no-sleep');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let isAttached = false;
|
||||||
const serveCommand = this.runCodeTunnelCommand('tunnel', args, (message: string, isErr: boolean) => {
|
const serveCommand = this.runCodeTunnelCommand('tunnel', args, (message: string, isErr: boolean) => {
|
||||||
if (isErr) {
|
if (isErr) {
|
||||||
this._logger.error(message);
|
this._logger.error(message);
|
||||||
} else {
|
} else {
|
||||||
this._logger.info(message);
|
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]+))/);
|
const m = message.match(/Open this link in your browser (https:\/\/([^\/\s]+)\/([^\/\s]+)\/([^\/\s]+))/);
|
||||||
if (m) {
|
if (m) {
|
||||||
const info: ConnectionInfo = { link: m[1], domain: m[2], tunnelName: m[4], isAttached };
|
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/)) {
|
} else if (message.match(/error refreshing token/)) {
|
||||||
serveCommand.cancel();
|
serveCommand.cancel();
|
||||||
this._onDidTokenFailedEmitter.fire(session);
|
this._onDidTokenFailedEmitter.fire(session);
|
||||||
@@ -317,14 +394,14 @@ export class RemoteTunnelService extends Disposable implements IRemoteTunnelServ
|
|||||||
// process exited unexpectedly
|
// process exited unexpectedly
|
||||||
this._logger.info(`tunnel process terminated`);
|
this._logger.info(`tunnel process terminated`);
|
||||||
this._tunnelProcess = undefined;
|
this._tunnelProcess = undefined;
|
||||||
this._session = undefined;
|
this._mode = INACTIVE_TUNNEL_MODE;
|
||||||
|
|
||||||
this.setTunnelStatus(TunnelStates.disconnected());
|
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 createCancelablePromise<number>(token => {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
if (token.isCancellationRequested) {
|
if (token.isCancellationRequested) {
|
||||||
@@ -350,13 +427,13 @@ export class RemoteTunnelService extends Disposable implements IRemoteTunnelServ
|
|||||||
tunnelProcess = spawn(tunnelCommand, ['tunnel', ...commandArgs], { cwd: homedir(), stdio });
|
tunnelProcess = spawn(tunnelCommand, ['tunnel', ...commandArgs], { cwd: homedir(), stdio });
|
||||||
}
|
}
|
||||||
|
|
||||||
tunnelProcess.stdout!.on('data', data => {
|
tunnelProcess.stdout!.pipe(new StreamSplitter('\n')).on('data', data => {
|
||||||
if (tunnelProcess) {
|
if (tunnelProcess) {
|
||||||
const message = data.toString();
|
const message = data.toString();
|
||||||
onOutput(message, false);
|
onOutput(message, false);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
tunnelProcess.stderr!.on('data', data => {
|
tunnelProcess.stderr!.pipe(new StreamSplitter('\n')).on('data', data => {
|
||||||
if (tunnelProcess) {
|
if (tunnelProcess) {
|
||||||
const message = data.toString();
|
const message = data.toString();
|
||||||
onOutput(message, true);
|
onOutput(message, true);
|
||||||
@@ -394,30 +471,33 @@ export class RemoteTunnelService extends Disposable implements IRemoteTunnelServ
|
|||||||
return name || undefined;
|
return name || undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
private _restoreSession(): IRemoteTunnelSession | undefined {
|
private _restoreMode(): TunnelMode {
|
||||||
try {
|
try {
|
||||||
const tunnelAccessSession = this.storageService.get(TUNNEL_ACCESS_SESSION, StorageScope.APPLICATION);
|
const tunnelAccessSession = this.storageService.get(TUNNEL_ACCESS_SESSION, StorageScope.APPLICATION);
|
||||||
|
const asService = this.storageService.getBoolean(TUNNEL_ACCESS_IS_SERVICE, StorageScope.APPLICATION, false);
|
||||||
if (tunnelAccessSession) {
|
if (tunnelAccessSession) {
|
||||||
const session = JSON.parse(tunnelAccessSession) as IRemoteTunnelSession;
|
const session = JSON.parse(tunnelAccessSession) as IRemoteTunnelSession;
|
||||||
if (session && isString(session.accountLabel) && isString(session.sessionId) && isString(session.providerId)) {
|
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);
|
this._logger.error('Problems restoring session from storage, invalid format', session);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this._logger.error('Problems restoring session from storage', e);
|
this._logger.error('Problems restoring session from storage', e);
|
||||||
}
|
}
|
||||||
return undefined;
|
return INACTIVE_TUNNEL_MODE;
|
||||||
}
|
}
|
||||||
|
|
||||||
private _storeSession(session: IRemoteTunnelSession | undefined): void {
|
private _storeMode(mode: TunnelMode): void {
|
||||||
if (session) {
|
if (mode.active) {
|
||||||
const sessionWithoutToken = {
|
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_SESSION, JSON.stringify(sessionWithoutToken), StorageScope.APPLICATION, StorageTarget.MACHINE);
|
||||||
|
this.storageService.store(TUNNEL_ACCESS_IS_SERVICE, mode.asService, StorageScope.APPLICATION, StorageTarget.MACHINE);
|
||||||
} else {
|
} else {
|
||||||
this.storageService.remove(TUNNEL_ACCESS_SESSION, StorageScope.APPLICATION);
|
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;
|
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.
|
* 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 { Action } from 'vs/base/common/actions';
|
||||||
import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService';
|
import { Disposable, DisposableStore } from 'vs/base/common/lifecycle';
|
||||||
import { IWorkspaceContextService, isUntitledWorkspace } from 'vs/platform/workspace/common/workspace';
|
|
||||||
import { Schemas } from 'vs/base/common/network';
|
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 { ITunnelApplicationConfig } from 'vs/base/common/product';
|
||||||
|
import { joinPath } from 'vs/base/common/resources';
|
||||||
import { isNumber, isObject, isString } from 'vs/base/common/types';
|
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 = {
|
export const REMOTE_TUNNEL_CATEGORY: ILocalizedString = {
|
||||||
original: 'Remote-Tunnels',
|
original: 'Remote-Tunnels',
|
||||||
@@ -103,10 +102,8 @@ export class RemoteTunnelWorkbenchContribution extends Disposable implements IWo
|
|||||||
@IProductService productService: IProductService,
|
@IProductService productService: IProductService,
|
||||||
@IStorageService private readonly storageService: IStorageService,
|
@IStorageService private readonly storageService: IStorageService,
|
||||||
@ILoggerService loggerService: ILoggerService,
|
@ILoggerService loggerService: ILoggerService,
|
||||||
@ILogService logService: ILogService,
|
|
||||||
@IQuickInputService private readonly quickInputService: IQuickInputService,
|
@IQuickInputService private readonly quickInputService: IQuickInputService,
|
||||||
@INativeEnvironmentService private environmentService: INativeEnvironmentService,
|
@INativeEnvironmentService private environmentService: INativeEnvironmentService,
|
||||||
@IFileService fileService: IFileService,
|
|
||||||
@IRemoteTunnelService private remoteTunnelService: IRemoteTunnelService,
|
@IRemoteTunnelService private remoteTunnelService: IRemoteTunnelService,
|
||||||
@ICommandService private commandService: ICommandService,
|
@ICommandService private commandService: ICommandService,
|
||||||
@IWorkspaceContextService private workspaceContextService: IWorkspaceContextService,
|
@IWorkspaceContextService private workspaceContextService: IWorkspaceContextService,
|
||||||
@@ -127,20 +124,7 @@ export class RemoteTunnelWorkbenchContribution extends Disposable implements IWo
|
|||||||
}
|
}
|
||||||
this.serverConfiguration = serverConfiguration;
|
this.serverConfiguration = serverConfiguration;
|
||||||
|
|
||||||
this._register(this.remoteTunnelService.onDidChangeTunnelStatus(status => {
|
this._register(this.remoteTunnelService.onDidChangeTunnelStatus(s => this.handleTunnelStatusUpdate(s)));
|
||||||
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.registerCommands();
|
this.registerCommands();
|
||||||
|
|
||||||
@@ -149,6 +133,21 @@ export class RemoteTunnelWorkbenchContribution extends Disposable implements IWo
|
|||||||
this.recommendRemoteExtensionIfNeeded();
|
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() {
|
private async recommendRemoteExtensionIfNeeded() {
|
||||||
await this.extensionService.whenInstalledExtensionsRegistered();
|
await this.extensionService.whenInstalledExtensionsRegistered();
|
||||||
|
|
||||||
@@ -228,10 +227,17 @@ export class RemoteTunnelWorkbenchContribution extends Disposable implements IWo
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async initialize(): Promise<void> {
|
private async initialize(): Promise<void> {
|
||||||
const session = await this.remoteTunnelService.getSession();
|
const [mode, status] = await Promise.all([
|
||||||
if (session && session.token) {
|
this.remoteTunnelService.getMode(),
|
||||||
|
this.remoteTunnelService.getTunnelStatus(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
this.handleTunnelStatusUpdate(status);
|
||||||
|
|
||||||
|
if (mode.active && mode.session.token) {
|
||||||
return; // already initialized, token available
|
return; // already initialized, token available
|
||||||
}
|
}
|
||||||
|
|
||||||
return await this.progressService.withProgress(
|
return await this.progressService.withProgress(
|
||||||
{
|
{
|
||||||
location: ProgressLocation.Window,
|
location: ProgressLocation.Window,
|
||||||
@@ -248,13 +254,13 @@ export class RemoteTunnelWorkbenchContribution extends Disposable implements IWo
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
let newSession: IRemoteTunnelSession | undefined;
|
let newSession: IRemoteTunnelSession | undefined;
|
||||||
if (session) {
|
if (mode.active) {
|
||||||
const token = await this.getSessionToken(session);
|
const token = await this.getSessionToken(mode.session);
|
||||||
if (token) {
|
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();
|
listener.dispose();
|
||||||
|
|
||||||
if (status.type === 'connected') {
|
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) {
|
if (this.connectionInfo) {
|
||||||
return this.connectionInfo;
|
return this.connectionInfo;
|
||||||
}
|
}
|
||||||
@@ -301,6 +307,19 @@ export class RemoteTunnelWorkbenchContribution extends Disposable implements IWo
|
|||||||
listener.dispose();
|
listener.dispose();
|
||||||
completed = true;
|
completed = true;
|
||||||
s(status.info);
|
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;
|
break;
|
||||||
case 'disconnected':
|
case 'disconnected':
|
||||||
listener.dispose();
|
listener.dispose();
|
||||||
@@ -312,7 +331,7 @@ export class RemoteTunnelWorkbenchContribution extends Disposable implements IWo
|
|||||||
});
|
});
|
||||||
const token = authenticationSession.session.idToken ?? authenticationSession.session.accessToken;
|
const token = authenticationSession.session.idToken ?? authenticationSession.session.accessToken;
|
||||||
const account: IRemoteTunnelSession = { sessionId: authenticationSession.session.id, token, providerId: authenticationSession.providerId, accountLabel: authenticationSession.session.account.label };
|
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')) {
|
if (!completed && (status.type === 'connected' || status.type === 'disconnected')) {
|
||||||
listener.dispose();
|
listener.dispose();
|
||||||
if (status.type === 'connected') {
|
if (status.type === 'connected') {
|
||||||
@@ -403,7 +422,7 @@ export class RemoteTunnelWorkbenchContribution extends Disposable implements IWo
|
|||||||
private async getAllSessions(): Promise<ExistingSessionItem[]> {
|
private async getAllSessions(): Promise<ExistingSessionItem[]> {
|
||||||
const authenticationProviders = await this.getAuthenticationProviders();
|
const authenticationProviders = await this.getAuthenticationProviders();
|
||||||
const accounts = new Map<string, ExistingSessionItem>();
|
const accounts = new Map<string, ExistingSessionItem>();
|
||||||
const currentAccount = await this.remoteTunnelService.getSession();
|
const currentAccount = await this.remoteTunnelService.getMode();
|
||||||
let currentSession: ExistingSessionItem | undefined;
|
let currentSession: ExistingSessionItem | undefined;
|
||||||
|
|
||||||
for (const provider of authenticationProviders) {
|
for (const provider of authenticationProviders) {
|
||||||
@@ -413,7 +432,7 @@ export class RemoteTunnelWorkbenchContribution extends Disposable implements IWo
|
|||||||
if (!this.expiredSessions.has(session.id)) {
|
if (!this.expiredSessions.has(session.id)) {
|
||||||
const item = this.createExistingSessionItem(session, provider.id);
|
const item = this.createExistingSessionItem(session, provider.id);
|
||||||
accounts.set(item.session.account.id, item);
|
accounts.set(item.session.account.id, item);
|
||||||
if (currentAccount && currentAccount.sessionId === session.id) {
|
if (currentAccount.active && currentAccount.session.sessionId === session.id) {
|
||||||
currentSession = item;
|
currentSession = item;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -483,6 +502,8 @@ export class RemoteTunnelWorkbenchContribution extends Disposable implements IWo
|
|||||||
const commandService = accessor.get(ICommandService);
|
const commandService = accessor.get(ICommandService);
|
||||||
const storageService = accessor.get(IStorageService);
|
const storageService = accessor.get(IStorageService);
|
||||||
const dialogService = accessor.get(IDialogService);
|
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);
|
const didNotifyPreview = storageService.getBoolean(REMOTE_TUNNEL_PROMPTED_PREVIEW_STORAGE_KEY, StorageScope.APPLICATION, false);
|
||||||
if (!didNotifyPreview) {
|
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);
|
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) {
|
if (connectionInfo) {
|
||||||
const linkToOpen = that.getLinkToOpen(connectionInfo);
|
const linkToOpen = that.getLinkToOpen(connectionInfo);
|
||||||
const remoteExtension = that.serverConfiguration.extension;
|
const remoteExtension = that.serverConfiguration.extension;
|
||||||
const linkToOpenForMarkdown = linkToOpen.toString(false).replace(/\)/g, '%29');
|
const linkToOpenForMarkdown = linkToOpen.toString(false).replace(/\)/g, '%29');
|
||||||
await notificationService.notify({
|
notificationService.notify({
|
||||||
severity: Severity.Info,
|
severity: Severity.Info,
|
||||||
message:
|
message:
|
||||||
localize(
|
localize(
|
||||||
@@ -525,7 +567,7 @@ export class RemoteTunnelWorkbenchContribution extends Disposable implements IWo
|
|||||||
const usedOnHostMessage: UsedOnHostMessage = { hostName: connectionInfo.tunnelName, timeStamp: new Date().getTime() };
|
const usedOnHostMessage: UsedOnHostMessage = { hostName: connectionInfo.tunnelName, timeStamp: new Date().getTime() };
|
||||||
storageService.store(REMOTE_TUNNEL_USED_STORAGE_KEY, JSON.stringify(usedOnHostMessage), StorageScope.APPLICATION, StorageTarget.USER);
|
storageService.store(REMOTE_TUNNEL_USED_STORAGE_KEY, JSON.stringify(usedOnHostMessage), StorageScope.APPLICATION, StorageTarget.USER);
|
||||||
} else {
|
} else {
|
||||||
await notificationService.notify({
|
notificationService.notify({
|
||||||
severity: Severity.Info,
|
severity: Severity.Info,
|
||||||
message: localize('progress.turnOn.failed',
|
message: localize('progress.turnOn.failed',
|
||||||
"Unable to turn on the remote tunnel access. Check the Remote Tunnel Service log for details."),
|
"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() {
|
private async showManageOptions() {
|
||||||
const account = await this.remoteTunnelService.getSession();
|
const account = await this.remoteTunnelService.getMode();
|
||||||
|
|
||||||
return new Promise<void>((c, e) => {
|
return new Promise<void>((c, e) => {
|
||||||
const disposables = new DisposableStore();
|
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({ id: RemoteTunnelCommandIds.showLog, label: localize('manage.showLog', 'Show Log') });
|
||||||
items.push({ type: 'separator' });
|
items.push({ type: 'separator' });
|
||||||
items.push({ id: RemoteTunnelCommandIds.configure, label: localize('manage.tunnelName', 'Change Tunnel Name'), description: this.connectionInfo?.tunnelName });
|
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;
|
quickPick.items = items;
|
||||||
disposables.add(quickPick.onDidAccept(() => {
|
disposables.add(quickPick.onDidAccept(() => {
|
||||||
|
|||||||
Reference in New Issue
Block a user