Skip to content

Commit fbb5c9e

Browse files
authored
Fix ctrl-c handling (#2200)
1 parent ad36d13 commit fbb5c9e

File tree

9 files changed

+161
-70
lines changed

9 files changed

+161
-70
lines changed

Cargo.lock

+2
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

+1
Original file line numberDiff line numberDiff line change
@@ -261,6 +261,7 @@ uuid = { version = "1.2.1", features = ["v4"] }
261261
walkdir = "2.2.5"
262262
wasmbin = "0.6"
263263
webbrowser = "1.0.2"
264+
windows-sys = "0.59"
264265
xdg = "2.5"
265266

266267
# Vendor the openssl we rely on, rather than depend on a

crates/cli/Cargo.toml

+3
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,9 @@ wasmbin.workspace = true
6969
wasmtime.workspace = true
7070
webbrowser.workspace = true
7171

72+
[target.'cfg(windows)'.dependencies]
73+
windows-sys = { workspace = true, features = ["Win32_System_Console"] }
74+
7275
[dev-dependencies]
7376
insta.workspace = true
7477
fs-err.workspace = true

crates/cli/src/lib.rs

-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ mod config;
44
pub(crate) mod detect;
55
mod edit_distance;
66
mod errors;
7-
mod start;
87
mod subcommands;
98
mod tasks;
109
pub mod util;

crates/cli/src/start.rs

-64
This file was deleted.

crates/cli/src/subcommands/mod.rs

+1
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ pub mod publish;
1414
pub mod repl;
1515
pub mod server;
1616
pub mod sql;
17+
pub mod start;
1718
pub mod subscribe;
1819
pub mod upgrade;
1920
pub mod version;

crates/cli/src/subcommands/start.rs

+108
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
use std::ffi::OsString;
2+
use std::io;
3+
use std::process::{Command, ExitCode};
4+
5+
use anyhow::Context;
6+
use clap::{Arg, ArgMatches};
7+
use spacetimedb_paths::SpacetimePaths;
8+
9+
pub fn cli() -> clap::Command {
10+
clap::Command::new("start")
11+
.about("Start a local SpacetimeDB instance")
12+
.long_about(
13+
"\
14+
Start a local SpacetimeDB instance
15+
16+
Run `spacetime start --help` to see all options.",
17+
)
18+
.disable_help_flag(true)
19+
.arg(
20+
Arg::new("edition")
21+
.long("edition")
22+
.help("The edition of SpacetimeDB to start up")
23+
.value_parser(clap::value_parser!(Edition))
24+
.default_value("standalone"),
25+
)
26+
.arg(
27+
Arg::new("args")
28+
.help("The args to pass to `spacetimedb-{edition} start`")
29+
.value_parser(clap::value_parser!(OsString))
30+
.allow_hyphen_values(true)
31+
.num_args(0..),
32+
)
33+
}
34+
35+
#[derive(clap::ValueEnum, Clone, Copy)]
36+
enum Edition {
37+
Standalone,
38+
Cloud,
39+
}
40+
41+
pub async fn exec(paths: &SpacetimePaths, args: &ArgMatches) -> anyhow::Result<ExitCode> {
42+
let edition = args.get_one::<Edition>("edition").unwrap();
43+
let args = args.get_many::<OsString>("args").unwrap_or_default();
44+
let bin_name = match edition {
45+
Edition::Standalone => "spacetimedb-standalone",
46+
Edition::Cloud => "spacetimedb-cloud",
47+
};
48+
let resolved_exe = std::env::current_exe().context("could not retrieve current exe")?;
49+
let bin_path = resolved_exe
50+
.parent()
51+
.unwrap()
52+
.join(bin_name)
53+
.with_extension(std::env::consts::EXE_EXTENSION);
54+
let mut cmd = Command::new(&bin_path);
55+
cmd.arg("start")
56+
.arg("--data-dir")
57+
.arg(&paths.data_dir)
58+
.arg("--jwt-key-dir")
59+
.arg(&paths.cli_config_dir)
60+
.args(args);
61+
62+
exec_replace(&mut cmd).with_context(|| format!("exec failed for {}", bin_path.display()))
63+
}
64+
65+
// implementation based on and docs taken verbatim from `cargo_util::ProcessBuilder::exec_replace`
66+
//
67+
/// Replaces the current process with the target process.
68+
///
69+
/// On Unix, this executes the process using the Unix syscall `execvp`, which will block
70+
/// this process, and will only return if there is an error.
71+
///
72+
/// On Windows this isn't technically possible. Instead we emulate it to the best of our
73+
/// ability. One aspect we fix here is that we specify a handler for the Ctrl-C handler.
74+
/// In doing so (and by effectively ignoring it) we should emulate proxying Ctrl-C
75+
/// handling to the application at hand, which will either terminate or handle it itself.
76+
/// According to Microsoft's documentation at
77+
/// <https://docs.microsoft.com/en-us/windows/console/ctrl-c-and-ctrl-break-signals>.
78+
/// the Ctrl-C signal is sent to all processes attached to a terminal, which should
79+
/// include our child process. If the child terminates then we'll reap them in Cargo
80+
/// pretty quickly, and if the child handles the signal then we won't terminate
81+
/// (and we shouldn't!) until the process itself later exits.
82+
fn exec_replace(cmd: &mut Command) -> io::Result<ExitCode> {
83+
#[cfg(unix)]
84+
{
85+
use std::os::unix::process::CommandExt;
86+
// if exec() succeeds, it diverges, so the function just returns an io::Error
87+
let err = cmd.exec();
88+
Err(err)
89+
}
90+
#[cfg(windows)]
91+
{
92+
use windows_sys::Win32::Foundation::{BOOL, FALSE, TRUE};
93+
use windows_sys::Win32::System::Console::SetConsoleCtrlHandler;
94+
95+
unsafe extern "system" fn ctrlc_handler(_: u32) -> BOOL {
96+
// Do nothing. Let the child process handle it.
97+
TRUE
98+
}
99+
unsafe {
100+
if SetConsoleCtrlHandler(Some(ctrlc_handler), TRUE) == FALSE {
101+
return Err(io::Error::new(io::ErrorKind::Other, "Unable to set console handler"));
102+
}
103+
}
104+
105+
cmd.status()
106+
.map(|status| ExitCode::from(status.code().unwrap_or(1).try_into().unwrap_or(1)))
107+
}
108+
}

crates/update/Cargo.toml

+3
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,6 @@ anyhow.workspace = true
1111
clap.workspace = true
1212
semver.workspace = true
1313
tracing = { workspace = true, features = ["release_max_level_off"] }
14+
15+
[target.'cfg(windows)'.dependencies]
16+
windows-sys = { workspace = true, features = ["Win32_System_Console"] }

crates/update/src/proxy.rs

+43-5
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ use anyhow::Context;
22
use spacetimedb_paths::{FromPathUnchecked, RootDir, SpacetimePaths};
33
use std::ffi::OsStr;
44
use std::ffi::OsString;
5+
use std::io;
56
use std::path::PathBuf;
67
use std::process::Command;
78
use std::process::ExitCode;
@@ -34,15 +35,52 @@ pub(crate) fn run_cli(paths: &SpacetimePaths, argv0: Option<&OsStr>, args: Vec<O
3435
if let Some(argv0) = argv0 {
3536
cmd.arg0(argv0);
3637
}
38+
}
39+
exec_replace(&mut cmd).with_context(|| format!("exec failed for {}", cli_path.display()))
40+
}
41+
42+
// implementation based on and docs taken verbatim from `cargo_util::ProcessBuilder::exec_replace`
43+
//
44+
/// Replaces the current process with the target process.
45+
///
46+
/// On Unix, this executes the process using the Unix syscall `execvp`, which will block
47+
/// this process, and will only return if there is an error.
48+
///
49+
/// On Windows this isn't technically possible. Instead we emulate it to the best of our
50+
/// ability. One aspect we fix here is that we specify a handler for the Ctrl-C handler.
51+
/// In doing so (and by effectively ignoring it) we should emulate proxying Ctrl-C
52+
/// handling to the application at hand, which will either terminate or handle it itself.
53+
/// According to Microsoft's documentation at
54+
/// <https://docs.microsoft.com/en-us/windows/console/ctrl-c-and-ctrl-break-signals>.
55+
/// the Ctrl-C signal is sent to all processes attached to a terminal, which should
56+
/// include our child process. If the child terminates then we'll reap them in Cargo
57+
/// pretty quickly, and if the child handles the signal then we won't terminate
58+
/// (and we shouldn't!) until the process itself later exits.
59+
fn exec_replace(cmd: &mut Command) -> io::Result<ExitCode> {
60+
#[cfg(unix)]
61+
{
62+
use std::os::unix::process::CommandExt;
63+
// if exec() succeeds, it diverges, so the function just returns an io::Error
3764
let err = cmd.exec();
38-
Err(err).context(format!("exec failed for {}", cli_path.display()))
65+
Err(err)
3966
}
4067
#[cfg(windows)]
4168
{
42-
let status = cmd
43-
.status()
44-
.with_context(|| format!("failed to run {}", cli_path.display()))?;
45-
Ok(ExitCode::from(status.code().unwrap_or(1) as u8))
69+
use windows_sys::Win32::Foundation::{BOOL, FALSE, TRUE};
70+
use windows_sys::Win32::System::Console::SetConsoleCtrlHandler;
71+
72+
unsafe extern "system" fn ctrlc_handler(_: u32) -> BOOL {
73+
// Do nothing. Let the child process handle it.
74+
TRUE
75+
}
76+
unsafe {
77+
if SetConsoleCtrlHandler(Some(ctrlc_handler), TRUE) == FALSE {
78+
return Err(io::Error::new(io::ErrorKind::Other, "Unable to set console handler"));
79+
}
80+
}
81+
82+
cmd.status()
83+
.map(|status| ExitCode::from(status.code().unwrap_or(1).try_into().unwrap_or(1)))
4684
}
4785
}
4886

0 commit comments

Comments
 (0)