spacetimedb_cli/subcommands/
start.rs

1use std::ffi::OsString;
2use std::io;
3use std::process::{Command, ExitCode};
4
5use anyhow::Context;
6use clap::{Arg, ArgMatches};
7use spacetimedb_paths::SpacetimePaths;
8
9pub fn cli() -> clap::Command {
10    clap::Command::new("start")
11        .about("Start a local SpacetimeDB instance")
12        .long_about(
13            "\
14Start a local SpacetimeDB instance
15
16Run `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)]
36enum Edition {
37    Standalone,
38    Cloud,
39}
40
41pub 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.
82pub(crate) 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}