Skip to content

Commit 1bf3323

Browse files
committed
cli: add service integration for macos, observability
Adds support for running the tunnel as a service on macOS via launchservices. It also hooks up observability (`code tunnel service log`) on macOS and Linux. On macOS--and later Windows, hence the manual implementation of `tail`--it saves output to a log file and watches it. On Linux, it simply delegates to journalctl. The "tailing" is implemented via simple polling of the file size. I didn't want to pull in a giant set of dependencies for inotify/kqueue/etc just for this use case; performance when polling a single log file is not a huge concern.
1 parent 2ab5e3f commit 1bf3323

File tree

12 files changed

+606
-52
lines changed

12 files changed

+606
-52
lines changed

cli/src/bin/code/main.rs

Lines changed: 29 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ use std::process::Command;
99
use clap::Parser;
1010
use cli::{
1111
commands::{args, tunnels, update, version, CommandContext},
12+
constants::get_default_user_agent,
1213
desktop, log as own_log,
1314
state::LauncherPaths,
1415
util::{
@@ -38,16 +39,12 @@ async fn main() -> Result<(), std::convert::Infallible> {
3839

3940
let core = parsed.core();
4041
let context = CommandContext {
41-
http: reqwest::Client::new(),
42+
http: reqwest::ClientBuilder::new()
43+
.user_agent(get_default_user_agent())
44+
.build()
45+
.unwrap(),
4246
paths: LauncherPaths::new(&core.global_options.cli_data_dir).unwrap(),
43-
log: own_log::Logger::new(
44-
SdkTracerProvider::builder().build().tracer("codecli"),
45-
if core.global_options.verbose {
46-
own_log::Level::Trace
47-
} else {
48-
core.global_options.log.unwrap_or(own_log::Level::Info)
49-
},
50-
),
47+
log: make_logger(core),
5148
args: core.clone(),
5249
};
5350

@@ -111,6 +108,23 @@ async fn main() -> Result<(), std::convert::Infallible> {
111108
}
112109
}
113110

111+
fn make_logger(core: &args::CliCore) -> own_log::Logger {
112+
let log_level = if core.global_options.verbose {
113+
own_log::Level::Trace
114+
} else {
115+
core.global_options.log.unwrap_or(own_log::Level::Info)
116+
};
117+
118+
let tracer = SdkTracerProvider::builder().build().tracer("codecli");
119+
let mut log = own_log::Logger::new(tracer, log_level);
120+
if let Some(f) = &core.global_options.log_to_file {
121+
log =
122+
log.tee(own_log::FileLogSink::new(log_level, f).expect("expected to make file logger"))
123+
}
124+
125+
log
126+
}
127+
114128
fn print_and_exit<E>(err: E) -> !
115129
where
116130
E: std::fmt::Display,
@@ -143,7 +157,12 @@ async fn start_code(context: CommandContext, args: Vec<String>) -> Result<i32, A
143157
.args(args)
144158
.status()
145159
.map(|s| s.code().unwrap_or(1))
146-
.map_err(|e| wrap(e, format!("error running VS Code from {}", binary.display())))?;
160+
.map_err(|e| {
161+
wrap(
162+
e,
163+
format!("error running VS Code from {}", binary.display()),
164+
)
165+
})?;
147166

148167
Ok(code)
149168
}

cli/src/commands/args.rs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
* Licensed under the MIT License. See License.txt in the project root for license information.
44
*--------------------------------------------------------------------------------------------*/
55

6-
use std::fmt;
6+
use std::{fmt, path::PathBuf};
77

88
use crate::{constants, log, options, tunnels::code_server::CodeServerArgs};
99
use clap::{ArgEnum, Args, Parser, Subcommand};
@@ -394,6 +394,10 @@ pub struct GlobalOptions {
394394
#[clap(long, global = true)]
395395
pub verbose: bool,
396396

397+
/// Log to a file in addition to stdout. Used when running as a service.
398+
#[clap(long, global = true, hide = true)]
399+
pub log_to_file: Option<PathBuf>,
400+
397401
/// Log level to use.
398402
#[clap(long, arg_enum, value_name = "level", global = true)]
399403
pub log: Option<log::Level>,
@@ -596,6 +600,9 @@ pub enum TunnelServiceSubCommands {
596600
/// Uninstalls and stops the tunnel service.
597601
Uninstall,
598602

603+
/// Shows logs for the running service.
604+
Log,
605+
599606
/// Internal command for running the service
600607
#[clap(hide = true)]
601608
InternalRun,

cli/src/commands/tunnels.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -116,13 +116,11 @@ pub async fn service(
116116
match service_args {
117117
TunnelServiceSubCommands::Install => {
118118
// ensure logged in, otherwise subsequent serving will fail
119-
println!("authing");
120119
Auth::new(&ctx.paths, ctx.log.clone())
121120
.get_credential()
122121
.await?;
123122

124123
// likewise for license consent
125-
println!("consent");
126124
legal::require_consent(&ctx.paths, false)?;
127125

128126
let current_exe =
@@ -147,6 +145,9 @@ pub async fn service(
147145
TunnelServiceSubCommands::Uninstall => {
148146
manager.unregister().await?;
149147
}
148+
TunnelServiceSubCommands::Log => {
149+
manager.show_logs().await?;
150+
}
150151
TunnelServiceSubCommands::InternalRun => {
151152
manager
152153
.run(ctx.paths.clone(), TunnelServiceContainer::new(ctx.args))

cli/src/state.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,11 @@ impl LauncherPaths {
137137
&self.root
138138
}
139139

140+
/// Suggested path for tunnel service logs, when using file logs
141+
pub fn service_log_file(&self) -> PathBuf {
142+
self.root.join("tunnel-service.log")
143+
}
144+
140145
/// Removes the launcher data directory.
141146
pub fn remove(&self) -> Result<(), WrappedError> {
142147
remove_dir_all(&self.root).map_err(|e| {

cli/src/tunnels.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ mod service;
2121
mod service_linux;
2222
#[cfg(target_os = "windows")]
2323
mod service_windows;
24+
#[cfg(target_os = "macos")]
25+
mod service_macos;
2426

2527
pub use control_server::serve;
2628
pub use service::{

cli/src/tunnels/service.rs

Lines changed: 9 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,9 @@ pub trait ServiceManager {
4040
handle: impl 'static + ServiceContainer,
4141
) -> Result<(), AnyError>;
4242

43+
/// Show logs from the running service to standard out.
44+
async fn show_logs(&self) -> Result<(), AnyError>;
45+
4346
/// Unregisters the current executable as a service.
4447
async fn unregister(&self) -> Result<(), AnyError>;
4548
}
@@ -50,12 +53,16 @@ pub type ServiceManagerImpl = super::service_windows::WindowsService;
5053
#[cfg(target_os = "linux")]
5154
pub type ServiceManagerImpl = super::service_linux::SystemdService;
5255

53-
#[cfg(not(any(target_os = "windows", target_os = "linux")))]
54-
pub type ServiceManagerImpl = UnimplementedServiceManager;
56+
#[cfg(target_os = "macos")]
57+
pub type ServiceManagerImpl = super::service_macos::LaunchdService;
5558

5659
#[allow(unreachable_code)]
5760
#[allow(unused_variables)]
5861
pub fn create_service_manager(log: log::Logger, paths: &LauncherPaths) -> ServiceManagerImpl {
62+
#[cfg(target_os = "macos")]
63+
{
64+
super::service_macos::LaunchdService::new(log, paths)
65+
}
5966
#[cfg(target_os = "windows")]
6067
{
6168
super::service_windows::WindowsService::new(log)
@@ -64,36 +71,4 @@ pub fn create_service_manager(log: log::Logger, paths: &LauncherPaths) -> Servic
6471
{
6572
super::service_linux::SystemdService::new(log, paths.clone())
6673
}
67-
#[cfg(not(any(target_os = "windows", target_os = "linux")))]
68-
{
69-
UnimplementedServiceManager::new()
70-
}
71-
}
72-
73-
pub struct UnimplementedServiceManager();
74-
75-
#[allow(dead_code)]
76-
impl UnimplementedServiceManager {
77-
fn new() -> Self {
78-
Self()
79-
}
80-
}
81-
82-
#[async_trait]
83-
impl ServiceManager for UnimplementedServiceManager {
84-
async fn register(&self, _exe: PathBuf, _args: &[&str]) -> Result<(), AnyError> {
85-
unimplemented!("Service management is not supported on this platform");
86-
}
87-
88-
async fn run(
89-
self,
90-
_launcher_paths: LauncherPaths,
91-
_handle: impl 'static + ServiceContainer,
92-
) -> Result<(), AnyError> {
93-
unimplemented!("Service management is not supported on this platform");
94-
}
95-
96-
async fn unregister(&self) -> Result<(), AnyError> {
97-
unimplemented!("Service management is not supported on this platform");
98-
}
9974
}

cli/src/tunnels/service_linux.rs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ use std::{
77
fs::File,
88
io::{self, Write},
99
path::PathBuf,
10+
process::Command,
1011
};
1112

1213
use async_trait::async_trait;
@@ -115,6 +116,29 @@ impl ServiceManager for SystemdService {
115116
handle.run_service(self.log, launcher_paths, rx).await
116117
}
117118

119+
async fn show_logs(&self) -> Result<(), AnyError> {
120+
// show the systemctl status header...
121+
Command::new("systemctl")
122+
.args([
123+
"--user",
124+
"status",
125+
"-n",
126+
"0",
127+
&SystemdService::service_name_string(),
128+
])
129+
.status()
130+
.map(|s| s.code().unwrap_or(1))
131+
.map_err(|e| wrap(e, format!("error running journalctl")))?;
132+
133+
// then follow log files
134+
Command::new("journalctl")
135+
.args(["--user", "-f", "-u", &SystemdService::service_name_string()])
136+
.status()
137+
.map(|s| s.code().unwrap_or(1))
138+
.map_err(|e| wrap(e, format!("error running journalctl")))?;
139+
Ok(())
140+
}
141+
118142
async fn unregister(&self) -> Result<(), crate::util::errors::AnyError> {
119143
let connection = SystemdService::connect().await?;
120144
let proxy = SystemdService::proxy(&connection).await?;

0 commit comments

Comments
 (0)