diff --git a/cli/src/commands/args.rs b/cli/src/commands/args.rs index 92decf5ff0e..5d23bd634fb 100644 --- a/cli/src/commands/args.rs +++ b/cli/src/commands/args.rs @@ -6,7 +6,7 @@ use std::{fmt, path::PathBuf}; use crate::{constants, log, options, tunnels::code_server::CodeServerArgs}; -use clap::{ValueEnum, Args, Parser, Subcommand}; +use clap::{Args, Parser, Subcommand, ValueEnum}; use const_format::concatcp; const CLI_NAME: &str = concatcp!(constants::PRODUCT_NAME_LONG, " CLI"); diff --git a/cli/src/commands/tunnels.rs b/cli/src/commands/tunnels.rs index b8d7b0b60bd..3f29ee7020e 100644 --- a/cli/src/commands/tunnels.rs +++ b/cli/src/commands/tunnels.rs @@ -4,7 +4,8 @@ *--------------------------------------------------------------------------------------------*/ use async_trait::async_trait; -use base64::{Engine as _, engine::general_purpose as b64}; +use base64::{engine::general_purpose as b64, Engine as _}; +use serde::Serialize; use sha2::{Digest, Sha256}; use std::{str::FromStr, time::Duration}; use sysinfo::Pid; @@ -247,8 +248,14 @@ pub async fn kill(ctx: CommandContext) -> Result { .map_err(|e| e.into()) } +#[derive(Serialize)] +pub struct StatusOutput { + pub tunnel: Option, + pub service_installed: bool, +} + pub async fn status(ctx: CommandContext) -> Result { - let status = do_single_rpc_call::<_, protocol::singleton::Status>( + let tunnel_status = do_single_rpc_call::<_, protocol::singleton::Status>( &ctx.paths.tunnel_lockfile(), ctx.log.clone(), protocol::singleton::METHOD_STATUS, @@ -256,17 +263,24 @@ pub async fn status(ctx: CommandContext) -> Result { ) .await; - match status { - Err(CodeError::NoRunningTunnel) => { - ctx.log.result(CodeError::NoRunningTunnel.to_string()); - Ok(1) - } - Err(e) => Err(e.into()), - Ok(s) => { - ctx.log.result(serde_json::to_string(&s).unwrap()); - Ok(0) - } - } + let service_installed = create_service_manager(ctx.log.clone(), &ctx.paths) + .is_installed() + .await + .unwrap_or(false); + + ctx.log.result( + serde_json::to_string(&StatusOutput { + service_installed, + tunnel: match tunnel_status { + Ok(s) => Some(s.tunnel), + Err(CodeError::NoRunningTunnel) => None, + Err(e) => return Err(e.into()), + }, + }) + .unwrap(), + ); + + Ok(0) } /// Removes unused servers. diff --git a/cli/src/lib.rs b/cli/src/lib.rs index 93777852df2..b2e23cb4d69 100644 --- a/cli/src/lib.rs +++ b/cli/src/lib.rs @@ -18,8 +18,8 @@ pub mod tunnels; pub mod update_service; pub mod util; -mod download_cache; mod async_pipe; +mod download_cache; mod json_rpc; mod msgpack_rpc; mod rpc; diff --git a/cli/src/tunnels/service.rs b/cli/src/tunnels/service.rs index 31bf6890996..dba68f3b614 100644 --- a/cli/src/tunnels/service.rs +++ b/cli/src/tunnels/service.rs @@ -41,6 +41,9 @@ pub trait ServiceManager { /// Show logs from the running service to standard out. async fn show_logs(&self) -> Result<(), AnyError>; + /// Gets whether the tunnel service is installed. + async fn is_installed(&self) -> Result; + /// Unregisters the current executable as a service. async fn unregister(&self) -> Result<(), AnyError>; } diff --git a/cli/src/tunnels/service_linux.rs b/cli/src/tunnels/service_linux.rs index 725b72a8d6d..3d1258a469a 100644 --- a/cli/src/tunnels/service_linux.rs +++ b/cli/src/tunnels/service_linux.rs @@ -40,7 +40,7 @@ impl SystemdService { async fn connect() -> Result { let connection = Connection::session() .await - .map_err(|e| wrap(e, "error creating dbus session"))?; + .map_err(|e| wrap(e, "Error creating dbus session. This command uses systemd for managing services, you should check that systemd is installed and running as a user. If it's already installed, you may need to:\n\n- Install the `dbus-user-session` package and reboot\n- Start the user dbus session with `systemctl --user enable dbus --now`. \n\nThe error encountered was"))?; Ok(connection) } @@ -113,6 +113,20 @@ impl ServiceManager for SystemdService { Ok(()) } + async fn is_installed(&self) -> Result { + let connection = SystemdService::connect().await?; + let proxy = SystemdService::proxy(&connection).await?; + let state = proxy + .get_unit_file_state(SystemdService::service_name_string()) + .await; + + if let Ok(s) = state { + Ok(s == "enabled") + } else { + Ok(false) + } + } + async fn run( self, launcher_paths: crate::state::LauncherPaths, @@ -219,6 +233,8 @@ trait SystemdManagerDbus { force: bool, ) -> zbus::Result<(bool, Vec<(String, String, String)>)>; + fn get_unit_file_state(&self, file: String) -> zbus::Result; + fn link_unit_files( &self, files: Vec, diff --git a/cli/src/tunnels/service_macos.rs b/cli/src/tunnels/service_macos.rs index dcc676ffce5..2d0a23f8cb2 100644 --- a/cli/src/tunnels/service_macos.rs +++ b/cli/src/tunnels/service_macos.rs @@ -75,6 +75,11 @@ impl ServiceManager for LaunchdService { handle.run_service(self.log, launcher_paths).await } + async fn is_installed(&self) -> Result { + let cmd = capture_command_and_check_status("launchctl", &["list"]).await?; + Ok(String::from_utf8_lossy(&cmd.stdout).contains(&get_service_label())) + } + async fn unregister(&self) -> Result<(), crate::util::errors::AnyError> { let service_file = get_service_file_path()?; diff --git a/cli/src/tunnels/service_windows.rs b/cli/src/tunnels/service_windows.rs index 3404b4ed52c..427eddd620d 100644 --- a/cli/src/tunnels/service_windows.rs +++ b/cli/src/tunnels/service_windows.rs @@ -114,6 +114,11 @@ impl CliServiceManager for WindowsService { Ok(()) } + async fn is_installed(&self) -> Result { + let key = WindowsService::open_key()?; + Ok(key.get_raw_value(TUNNEL_ACTIVITY_NAME).is_ok()) + } + async fn unregister(&self) -> Result<(), AnyError> { let key = WindowsService::open_key()?; key.delete_value(TUNNEL_ACTIVITY_NAME) diff --git a/cli/src/util/tar.rs b/cli/src/util/tar.rs index 77dd67abbb0..248f63f9720 100644 --- a/cli/src/util/tar.rs +++ b/cli/src/util/tar.rs @@ -6,7 +6,7 @@ use crate::util::errors::{wrap, WrappedError}; use flate2::read::GzDecoder; use std::fs; -use std::io::{Seek, SeekFrom}; +use std::io::Seek; use std::path::{Path, PathBuf}; use tar::Archive; @@ -65,7 +65,7 @@ where // reset since skip logic read the tar already: tar_gz - .seek(SeekFrom::Start(0)) + .rewind() .map_err(|e| wrap(e, "error resetting seek position"))?; let tar = GzDecoder::new(tar_gz); diff --git a/cli/src/util/zipper.rs b/cli/src/util/zipper.rs index 84eec040b0e..45dd3b7e85d 100644 --- a/cli/src/util/zipper.rs +++ b/cli/src/util/zipper.rs @@ -88,8 +88,7 @@ where use std::io::Read; use std::os::unix::ffi::OsStringExt; - if matches!(file.unix_mode(), Some(mode) if mode & (S_IFLNK as u32) == (S_IFLNK as u32)) - { + if matches!(file.unix_mode(), Some(mode) if mode & S_IFLNK == S_IFLNK) { let mut link_to = Vec::new(); file.read_to_end(&mut link_to).map_err(|e| { wrap( diff --git a/src/vs/platform/remoteTunnel/node/remoteTunnelService.ts b/src/vs/platform/remoteTunnel/node/remoteTunnelService.ts index 9b73eece6ba..e3cee9778a2 100644 --- a/src/vs/platform/remoteTunnel/node/remoteTunnelService.ts +++ b/src/vs/platform/remoteTunnel/node/remoteTunnelService.ts @@ -165,7 +165,7 @@ export class RemoteTunnelService extends Disposable implements IRemoteTunnelServ } async startTunnel(session: IRemoteTunnelSession): Promise { - if (isSameSession(session, this._session)) { + if (isSameSession(session, this._session) && this._tunnelStatus.type !== 'disconnected') { return this._tunnelStatus; } this.setSession(session); @@ -195,7 +195,7 @@ export class RemoteTunnelService extends Disposable implements IRemoteTunnelServ } }; try { - await this.runCodeTunneCommand('stop', ['kill'], onOutput); + await this.runCodeTunnelCommand('stop', ['kill'], onOutput); } catch (e) { this._logger.error(e); } @@ -213,26 +213,35 @@ export class RemoteTunnelService extends Disposable implements IRemoteTunnelServ } let isAttached = false; + let output = ''; const onOutput = (a: string, isErr: boolean) => { if (isErr) { this._logger.error(a); } else { - this._logger.info(a); + output += a; } if (!this.environmentService.isBuilt && a.startsWith(' Compiling')) { this.setTunnelStatus(TunnelStates.connecting(localize('remoteTunnelService.building', 'Building CLI from sources'))); } }; - const statusProcess = this.runCodeTunneCommand('status', ['status'], onOutput); + const statusProcess = this.runCodeTunnelCommand('status', ['status'], onOutput); this._tunnelProcess = statusProcess; try { - const status = await statusProcess; + await statusProcess; if (this._tunnelProcess !== statusProcess) { return; } - isAttached = status === 0; + + // split and find the line, since in dev builds additional noise is + // added by cargo to the output. + const status: { + service_installed: boolean; + 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; @@ -255,7 +264,7 @@ export class RemoteTunnelService extends Disposable implements IRemoteTunnelServ a = a.replaceAll(token, '*'.repeat(4)); onOutput(a, isErr); }; - const loginProcess = this.runCodeTunneCommand('login', ['user', 'login', '--provider', session.providerId, '--access-token', token, '--log', LogLevelToString(this._logger.getLevel())], onLoginOutput); + const loginProcess = this.runCodeTunnelCommand('login', ['user', 'login', '--provider', session.providerId, '--access-token', token, '--log', LogLevelToString(this._logger.getLevel())], onLoginOutput); this._tunnelProcess = loginProcess; try { await loginProcess; @@ -286,7 +295,7 @@ export class RemoteTunnelService extends Disposable implements IRemoteTunnelServ if (this._preventSleep()) { args.push('--no-sleep'); } - const serveCommand = this.runCodeTunneCommand('tunnel', args, (message: string, isErr: boolean) => { + const serveCommand = this.runCodeTunnelCommand('tunnel', args, (message: string, isErr: boolean) => { if (isErr) { this._logger.error(message); } else { @@ -315,7 +324,7 @@ export class RemoteTunnelService extends Disposable implements IRemoteTunnelServ }); } - private runCodeTunneCommand(logLabel: string, commandArgs: string[], onOutput: (message: string, isError: boolean) => void = () => { }): CancelablePromise { + private runCodeTunnelCommand(logLabel: string, commandArgs: string[], onOutput: (message: string, isError: boolean) => void = () => { }): CancelablePromise { return createCancelablePromise(token => { return new Promise((resolve, reject) => { if (token.isCancellationRequested) {