diff --git a/Cargo.lock b/Cargo.lock index e89833e..9a82be0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -87,6 +87,16 @@ dependencies = [ "os_str_bytes", ] +[[package]] +name = "ctrlc" +version = "3.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbcf33c2a618cbe41ee43ae6e9f2e48368cd9f9db2896f10167d8d762679f639" +dependencies = [ + "nix", + "windows-sys 0.45.0", +] + [[package]] name = "errno" version = "0.2.8" @@ -177,7 +187,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "46112a93252b123d31a119a8d1a1ac19deac4fac6e0e8b0df58f0d4e5870e63c" dependencies = [ "libc", - "windows-sys", + "windows-sys 0.42.0", ] [[package]] @@ -189,7 +199,7 @@ dependencies = [ "hermit-abi", "io-lifetimes", "rustix", - "windows-sys", + "windows-sys 0.42.0", ] [[package]] @@ -256,6 +266,18 @@ dependencies = [ "serde_json", ] +[[package]] +name = "nix" +version = "0.26.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfdda3d196821d6af13126e40375cdf7da646a96114af134d5f417a9a1dc8e1a" +dependencies = [ + "bitflags", + "cfg-if", + "libc", + "static_assertions", +] + [[package]] name = "nu-ansi-term" version = "0.46.0" @@ -412,7 +434,7 @@ dependencies = [ "io-lifetimes", "libc", "linux-raw-sys", - "windows-sys", + "windows-sys 0.42.0", ] [[package]] @@ -476,6 +498,12 @@ version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0" +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + [[package]] name = "strsim" version = "0.10.0" @@ -518,6 +546,7 @@ version = "0.1.0" dependencies = [ "anyhow", "clap", + "ctrlc", "fish", "mino", "rand", @@ -696,46 +725,70 @@ dependencies = [ ] [[package]] -name = "windows_aarch64_gnullvm" -version = "0.42.0" +name = "windows-sys" +version = "0.45.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41d2aa71f6f0cbe00ae5167d90ef3cfe66527d6f613ca78ac8024c3ccab9a19e" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" [[package]] name = "windows_aarch64_msvc" -version = "0.42.0" +version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd0f252f5a35cac83d6311b2e795981f5ee6e67eb1f9a7f64eb4500fbc4dcdb4" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" [[package]] name = "windows_i686_gnu" -version = "0.42.0" +version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fbeae19f6716841636c28d695375df17562ca208b2b7d0dc47635a50ae6c5de7" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" [[package]] name = "windows_i686_msvc" -version = "0.42.0" +version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84c12f65daa39dd2babe6e442988fc329d6243fdce47d7d2d155b8d874862246" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" [[package]] name = "windows_x86_64_gnu" -version = "0.42.0" +version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf7b1b21b5362cbc318f686150e5bcea75ecedc74dd157d874d754a2ca44b0ed" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" [[package]] name = "windows_x86_64_gnullvm" -version = "0.42.0" +version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09d525d2ba30eeb3297665bd434a54297e4170c7f1a44cad4ef58095b4cd2028" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" [[package]] name = "windows_x86_64_msvc" -version = "0.42.0" +version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f40009d85759725a34da6d89a94e63d7bdc50a862acf0dbc7c8e488f1edcb6f5" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" [[package]] name = "winnow" diff --git a/tidepool/Cargo.toml b/tidepool/Cargo.toml index 263230c..37d1d71 100644 --- a/tidepool/Cargo.toml +++ b/tidepool/Cargo.toml @@ -20,3 +20,4 @@ toml = { version = "0.7" } serde_json = { version = "1.0" } clap = { version = "4.0", features = ["derive"] } anyhow = { version = "1.0" } +ctrlc = { version = "3.2" } diff --git a/tidepool/src/main.rs b/tidepool/src/main.rs index 10fcf98..a5c35f1 100644 --- a/tidepool/src/main.rs +++ b/tidepool/src/main.rs @@ -2,6 +2,8 @@ use anyhow::{Context as _, Result}; use rand::RngCore as _; use std::io::{Read, Write}; use std::path::Path; +use std::sync::{atomic, mpsc, Arc}; +use std::time::Duration; use fish::bot; use tidepool::{cli, config, output, sim}; @@ -18,36 +20,81 @@ fn main() -> Result<()> { } } +#[derive(Debug)] +enum Msg { + Interrupt, + Heartbeat(u64), + Status(u64, Box), + Done(u64, Box), +} + +#[derive(Debug)] +struct Status { + pieces: usize, + lines_left: usize, + time: Duration, +} + +fn create_mailbox() -> (mpsc::SyncSender, mpsc::Receiver) { + let (tx, rx) = mpsc::sync_channel(128); + + let tx2 = tx.clone(); + let on_ctrlc = move || { + if tx2.send(Msg::Interrupt).is_err() { + tracing::warn!("nobody handled ctrl-c, aborting"); + std::process::exit(128 + ctrlc::Signal::SIGINT as i32); + } + }; + if let Err(err) = ctrlc::set_handler(on_ctrlc) { + tracing::warn!("could not set signal handler: {err}"); + } + + (tx, rx) +} + fn single(args: cli::SingleRun) -> Result<()> { let config = parse_config_file(&args.config_file)?; - let output = simulation(&args.io, &args.data, args.seed, config); + let (tx, rx) = create_mailbox(); + let exit_early = Arc::new(atomic::AtomicBool::new(false)); - let mut writer: Box = match &args.output_file { - Some(path) => { - if path.is_file() { - if prompt_yn(&args.io, "output file already exists, overwrite?") { - tracing::debug!("removing {path:?}"); - std::fs::remove_file(path)?; - } else { - tracing::warn!("output file already exists, exiting."); - return Ok(()); + let exit_early2 = exit_early.clone(); + let config2 = config.clone(); + std::thread::spawn(move || run_simulation(&args.data, config2, args.seed, 0, tx, exit_early2)); + + while let Ok(msg) = rx.recv() { + tracing::trace!(msg = debug(&msg)); + + match msg { + Msg::Interrupt => { + eprintln!("\ncaught interrupt."); + tracing::debug!("interrupted"); + exit_early.store(true, atomic::Ordering::Relaxed); + } + Msg::Heartbeat(_id) => { + tracing::debug!("heartbeat"); + } + Msg::Status(_id, status) => { + if !args.io.quiet { + let ps = status.pieces; + let ll = status.lines_left; + let pps = ps as f64 / status.time.as_secs_f64(); + eprintln!("#{ps}, {ll} lines left, {pps:.2} pps"); } } - Box::new( - std::fs::File::create(path) - .with_context(|| format!("error opening output file '{}'", path.display()))?, - ) - } - None => Box::new(std::io::stdout()), - }; + Msg::Done(_id, output) => { + std::mem::drop(rx); - let pretty = args.output_file.is_none() && !args.io.quiet; - if pretty { - serde_json::to_writer_pretty(&mut writer, &output).context("error writing output")?; - writeln!(&mut writer).context("error writing output")?; - } else { - serde_json::to_writer(&mut writer, &output).context("error writing output")?; + if output.cleared < config.game.goal { + if !prompt_yn(&args.io, "run did not finish, keep it?") { + break; + } + } + + write_output(&args.io, args.output_file.as_deref(), &output)?; + break; + } + } } Ok(()) @@ -67,11 +114,41 @@ fn parse_config_file(path: &Path) -> Result { toml::from_str(&contents).context("error parsing config file") } -fn prompt_yn(io: &cli::IoArgs, msg: &str) -> bool { - if io.noninteractive { - return false; +fn write_output(io_args: &cli::IoArgs, path: Option<&Path>, output: &output::Output) -> Result<()> { + let mut writer: Box = match &path { + Some(path) => { + if path.is_file() { + if prompt_yn(io_args, "output file already exists, overwrite?") { + tracing::debug!("removing {path:?}"); + std::fs::remove_file(path)?; + } else { + tracing::warn!("output file already exists, exiting."); + return Ok(()); + } + } + Box::new( + std::fs::File::create(path) + .with_context(|| format!("error opening output file '{}'", path.display()))?, + ) + } + None => Box::new(std::io::stdout()), + }; + + let pretty = path.is_none() && !io_args.quiet; + if pretty { + serde_json::to_writer_pretty(&mut writer, &output) + .context("error writing output") + .and_then(|_| writeln!(&mut writer).context("error writing output")) + } else { + serde_json::to_writer(&mut writer, &output).context("error writing output") } - let mut output = std::io::stdout().lock(); +} + +fn prompt_yn(io_args: &cli::IoArgs, msg: &str) -> bool { + if io_args.noninteractive { + return true; + } + let mut output = std::io::stderr().lock(); write!(output, "{msg} [y/N] ").unwrap(); output.flush().unwrap(); let mut user_input = String::new(); @@ -79,15 +156,15 @@ fn prompt_yn(io: &cli::IoArgs, msg: &str) -> bool { user_input.trim().eq_ignore_ascii_case("y") } -fn simulation( - io: &cli::IoArgs, - data: &cli::OutputDataArgs, - seed: Option, +fn run_simulation( + data_args: &cli::OutputDataArgs, config: config::Config, -) -> output::Output { - // TODO: status signals - - let mut moves = data.list_moves.then(Vec::new); + seed: Option, + id: u64, + tx: mpsc::SyncSender, + exit_early: Arc, +) { + let mut moves = data_args.list_moves.then(Vec::new); let seed = seed.unwrap_or_else(|| rand::thread_rng().next_u64()); let sim_opts = sim::Options { @@ -100,9 +177,8 @@ fn simulation( let time_start = std::time::Instant::now(); while sim.lines_left() > 0 { - // TODO: progress bar - if !io.quiet { - eprintln!("#{}, {} left", sim.pieces(), sim.lines_left()); + if exit_early.load(atomic::Ordering::Relaxed) { + break; } let mut bot = bot::Bot::new( @@ -112,34 +188,53 @@ fn simulation( for i in 0..config.bot.iters { bot.think(); - if i % 1000 == 999 { - tracing::debug!("iteration {i}"); + + // periodically check back with the main thread to see if we should exit + if i % 4096 == 4095 { + if exit_early.load(atomic::Ordering::Relaxed) { + break; + } + if tx.send(Msg::Heartbeat(id)).is_err() { + break; + } } } - if let Some(placement) = bot.suggest() { - sim.play(placement); + let Some(placement) = bot.suggest() else { + tracing::warn!("gave up :( pieces={}, id={id}", sim.pieces()); + break; + }; - if let Some(moves) = moves.as_mut() { - moves.push(output::Move { placement }) - } - } else { + sim.play(placement); + if let Some(moves) = moves.as_mut() { + moves.push(output::Move { placement }) + } + + let status = Status { + lines_left: sim.lines_left(), + pieces: sim.pieces(), + time: std::time::Instant::now() - time_start, + }; + if tx.send(Msg::Status(id, status.into())).is_err() { break; } } let time_end = std::time::Instant::now(); - let profile = data.profile.then(|| output::Profile { + let profile = data_args.profile.then(|| output::Profile { time: time_end - time_start, }); - output::Output { + let output = output::Output { seed, cleared: config.game.goal - sim.lines_left(), pieces: sim.pieces(), config: config.game, moves, profile, + }; + if tx.send(Msg::Done(id, output.into())).is_err() { + tracing::debug!("nobody received output id={id}"); } }