Skip to content

Commit 02b5cca

Browse files
authored
Merge pull request microsoft#166411 from microsoft/cli/macos-service-monitoring
cli: add service integration for macos, observability
2 parents 816f31f + 4c25167 commit 02b5cca

File tree

13 files changed

+656
-73
lines changed

13 files changed

+656
-73
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: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ impl ServiceContainer for TunnelServiceContainer {
7474
&mut self,
7575
log: log::Logger,
7676
launcher_paths: LauncherPaths,
77-
shutdown_rx: mpsc::Receiver<ShutdownSignal>,
77+
shutdown_rx: mpsc::UnboundedReceiver<ShutdownSignal>,
7878
) -> Result<(), AnyError> {
7979
let csa = (&self.args).into();
8080
serve_with_csa(
@@ -116,19 +116,16 @@ 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 =
129127
std::env::current_exe().map_err(|e| wrap(e, "could not get current exe"))?;
130128

131-
println!("calling register");
132129
manager
133130
.register(
134131
current_exe,
@@ -147,6 +144,9 @@ pub async fn service(
147144
TunnelServiceSubCommands::Uninstall => {
148145
manager.unregister().await?;
149146
}
147+
TunnelServiceSubCommands::Log => {
148+
manager.show_logs().await?;
149+
}
150150
TunnelServiceSubCommands::InternalRun => {
151151
manager
152152
.run(ctx.paths.clone(), TunnelServiceContainer::new(ctx.args))
@@ -239,7 +239,7 @@ async fn serve_with_csa(
239239
log: Logger,
240240
gateway_args: TunnelServeArgs,
241241
csa: CodeServerArgs,
242-
shutdown_rx: Option<mpsc::Receiver<ShutdownSignal>>,
242+
shutdown_rx: Option<mpsc::UnboundedReceiver<ShutdownSignal>>,
243243
) -> Result<i32, AnyError> {
244244
// Intentionally read before starting the server. If the server updated and
245245
// respawn is requested, the old binary will get renamed, and then
@@ -259,7 +259,7 @@ async fn serve_with_csa(
259259
let shutdown_tx = if let Some(tx) = shutdown_rx {
260260
tx
261261
} else {
262-
let (tx, rx) = mpsc::channel::<ShutdownSignal>(2);
262+
let (tx, rx) = mpsc::unbounded_channel::<ShutdownSignal>();
263263
if let Some(process_id) = gateway_args.parent_process_id {
264264
match Pid::from_str(&process_id) {
265265
Ok(pid) => {
@@ -270,7 +270,7 @@ async fn serve_with_csa(
270270
while s.refresh_process(pid) {
271271
sleep(Duration::from_millis(2000)).await;
272272
}
273-
tx.send(ShutdownSignal::ParentProcessKilled).await.ok();
273+
tx.send(ShutdownSignal::ParentProcessKilled).ok();
274274
});
275275
}
276276
Err(_) => {
@@ -280,7 +280,7 @@ async fn serve_with_csa(
280280
}
281281
tokio::spawn(async move {
282282
tokio::signal::ctrl_c().await.ok();
283-
tx.send(ShutdownSignal::CtrlC).await.ok();
283+
tx.send(ShutdownSignal::CtrlC).ok();
284284
});
285285
rx
286286
};

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/control_server.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -167,7 +167,7 @@ pub async fn serve(
167167
launcher_paths: &LauncherPaths,
168168
code_server_args: &CodeServerArgs,
169169
platform: Platform,
170-
shutdown_rx: mpsc::Receiver<ShutdownSignal>,
170+
shutdown_rx: mpsc::UnboundedReceiver<ShutdownSignal>,
171171
) -> Result<ServerTermination, AnyError> {
172172
let mut port = tunnel.add_port_direct(CONTROL_PORT).await?;
173173
print_listening(log, &tunnel.name);

cli/src/tunnels/service.rs

Lines changed: 28 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,16 @@
33
* Licensed under the MIT License. See License.txt in the project root for license information.
44
*--------------------------------------------------------------------------------------------*/
55

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

88
use async_trait::async_trait;
99
use tokio::sync::mpsc;
1010

1111
use crate::commands::tunnels::ShutdownSignal;
1212
use crate::log;
1313
use crate::state::LauncherPaths;
14-
use crate::util::errors::AnyError;
14+
use crate::util::errors::{wrap, AnyError};
15+
use crate::util::io::{tailf, TailEvent};
1516

1617
pub const SERVICE_LOG_FILE_NAME: &str = "tunnel-service.log";
1718

@@ -21,7 +22,7 @@ pub trait ServiceContainer: Send {
2122
&mut self,
2223
log: log::Logger,
2324
launcher_paths: LauncherPaths,
24-
shutdown_rx: mpsc::Receiver<ShutdownSignal>,
25+
shutdown_rx: mpsc::UnboundedReceiver<ShutdownSignal>,
2526
) -> Result<(), AnyError>;
2627
}
2728

@@ -40,6 +41,9 @@ pub trait ServiceManager {
4041
handle: impl 'static + ServiceContainer,
4142
) -> Result<(), AnyError>;
4243

44+
/// Show logs from the running service to standard out.
45+
async fn show_logs(&self) -> Result<(), AnyError>;
46+
4347
/// Unregisters the current executable as a service.
4448
async fn unregister(&self) -> Result<(), AnyError>;
4549
}
@@ -50,50 +54,42 @@ pub type ServiceManagerImpl = super::service_windows::WindowsService;
5054
#[cfg(target_os = "linux")]
5155
pub type ServiceManagerImpl = super::service_linux::SystemdService;
5256

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

5660
#[allow(unreachable_code)]
5761
#[allow(unused_variables)]
5862
pub fn create_service_manager(log: log::Logger, paths: &LauncherPaths) -> ServiceManagerImpl {
63+
#[cfg(target_os = "macos")]
64+
{
65+
super::service_macos::LaunchdService::new(log, paths)
66+
}
5967
#[cfg(target_os = "windows")]
6068
{
61-
super::service_windows::WindowsService::new(log)
69+
super::service_windows::WindowsService::new(log, paths)
6270
}
6371
#[cfg(target_os = "linux")]
6472
{
6573
super::service_linux::SystemdService::new(log, paths.clone())
6674
}
67-
#[cfg(not(any(target_os = "windows", target_os = "linux")))]
68-
{
69-
UnimplementedServiceManager::new()
70-
}
7175
}
7276

73-
pub struct UnimplementedServiceManager();
74-
75-
#[allow(dead_code)]
76-
impl UnimplementedServiceManager {
77-
fn new() -> Self {
78-
Self()
77+
#[allow(dead_code)] // unused on Linux
78+
pub(crate) async fn tail_log_file(log_file: &Path) -> Result<(), AnyError> {
79+
if !log_file.exists() {
80+
println!("The tunnel service has not started yet.");
81+
return Ok(());
7982
}
80-
}
8183

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");
84+
let file = std::fs::File::open(log_file).map_err(|e| wrap(e, "error opening log file"))?;
85+
let mut rx = tailf(file, 20);
86+
while let Some(line) = rx.recv().await {
87+
match line {
88+
TailEvent::Line(l) => print!("{}", l),
89+
TailEvent::Reset => println!("== Tunnel service restarted =="),
90+
TailEvent::Err(e) => return Err(wrap(e, "error reading log file").into()),
91+
}
9492
}
9593

96-
async fn unregister(&self) -> Result<(), AnyError> {
97-
unimplemented!("Service management is not supported on this platform");
98-
}
94+
Ok(())
9995
}

cli/src/tunnels/service_linux.rs

Lines changed: 26 additions & 2 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;
@@ -106,15 +107,38 @@ impl ServiceManager for SystemdService {
106107
launcher_paths: crate::state::LauncherPaths,
107108
mut handle: impl 'static + super::ServiceContainer,
108109
) -> Result<(), crate::util::errors::AnyError> {
109-
let (tx, rx) = mpsc::channel::<ShutdownSignal>(1);
110+
let (tx, rx) = mpsc::unbounded_channel::<ShutdownSignal>();
110111
tokio::spawn(async move {
111112
tokio::signal::ctrl_c().await.ok();
112-
tx.send(ShutdownSignal::CtrlC).await.ok();
113+
tx.send(ShutdownSignal::CtrlC).ok();
113114
});
114115

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, "error running systemctl"))?;
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, "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)