1 // Copyright 2020-2021 Ian Jackson and contributors to Otter
2 // SPDX-License-Identifier: AGPL-3.0-or-later
3 // There is NO WARRANTY.
5 //! Otter game system (part thereeof)
7 //! <https://www.chiark.greenend.org.uk/~ianmdlvl/otter/docs/README.html>
9 //! This crate is intended for use only by other parts of Otter.
11 // ==================== namespace preparation ====================
15 pub use otter::crates::*;
21 pub use otter::prelude::*;
23 pub use std::cell::{RefCell, RefMut};
25 pub use num_traits::NumCast;
26 pub use serde_json::json;
27 pub use structopt::StructOpt;
30 pub type MgmtChannel = ClientMgmtChannel;
32 pub type JsV = serde_json::Value;
33 pub type MC = MgmtCommand;
35 // -------------------- private crates ----------
37 use otter_support::config::DAEMON_STARTUP_REPORT;
39 // ==================== public constants ====================
41 pub const TABLE: &str = "server::dummy";
42 pub const CONFIG: &str = "server-config.toml";
44 pub const URL: &str = "http://localhost:8000";
46 #[derive(Copy,Clone,Debug,Eq,PartialEq,Ord,PartialOrd)]
47 #[derive(FromPrimitive,EnumIter,IntoStaticStr,EnumProperty)]
48 #[strum(serialize_all = "snake_case")]
50 #[strum(props(Token="kmqAKPwK4TfReFjMor8MJhdRPBcwIBpe"))] Alice,
51 #[strum(props(Token="ccg9kzoTh758QrVE1xMY7BQWB36dNJTx"))] Bob,
54 // ==================== principal public structs ====================
56 #[derive(Debug,Clone)]
59 #[structopt(long="--as-if")]
60 pub as_if: Option<String>,
62 #[structopt(long="--no-bwrap")]
65 #[structopt(long="--tmp-dir", default_value="tmp")]
68 #[structopt(long="--pause", default_value="0ms")]
69 pub pause: humantime::Duration,
72 pub tests: WantedTestsOpt,
74 #[structopt(long="--test")]
75 test_name: Option<String>,
79 pub struct SetupCore {
81 pub mgmt_conn: RefCell<MgmtChannelForGame>,
82 pub server_child: Child,
83 pub wanted_tests: TrackWantedTests,
84 pub cln: cleanup_notify::Handle,
87 #[derive(Clone,Debug)]
91 pub start_dir: String,
95 pub struct Instance(pub InstanceName);
97 // ==================== Facilities for tests ====================
99 impl AsRef<Opts> for Opts { fn as_ref(&self) -> &Opts { self } }
103 impl<'e, E:Into<Box<dyn Error + 'e>>> From<E> for Explode {
104 fn from(e: E) -> Explode {
105 let mut m = "exploding on error".to_string();
106 let e: Box<dyn Error> = e.into();
107 let mut e: Option<&dyn Error> = Some(&*e);
108 while let Some(te) = e {
109 m += &format!(": {}", &te);
115 impl From<Explode> for anyhow::Error {
116 fn from(e: Explode) -> AE { match e { } }
118 #[ext(pub, name=ResultExplodeExt)]
119 impl<T> Result<T,Explode> {
120 fn y(self) -> T { match self { Ok(y) => y, Err(n) => match n { } } }
121 fn did(self, msg: &'static str) -> anyhow::Result<T> {
122 ResultGenDidExt::<_,AE>::did(Ok(self.y()), msg)
127 impl<E:Error> From<Explode> for E {
128 fn from(e: Explode) -> E { match e { } }
133 fn set<K: Into<String>>(&mut self, k: K, v: &JsV) {
134 self.as_object_mut().unwrap().insert(k.into(), v.clone());
137 fn extend<I,K,V>(&mut self, i: I)
138 where I: IntoIterator<Item=(K, V)>,
142 let i = i.into_iter().map(|(k,v)| (k.into(), v.borrow().clone()));
143 self.as_object_mut().unwrap().extend(i);
147 fn tree_walk<F,E>(&self, #[allow(unused_mut,unused_variables)] mut f: F)
148 where F: FnMut(&[String], &JsV) -> Result<(),E>
151 fn recurse<F,E>(kl: &mut Vec<String>, v: &JsV, f: &mut F)
152 where F: FnMut(&[String], &JsV) -> Result<(),E> {
154 if let Some(o) = v.as_object() {
156 kl.push(k.to_owned());
157 let y = recurse(kl, v, f);
161 } else if let Some(a) = v.as_array() {
162 for (k,v) in a.iter().enumerate() {
163 kl.push(k.to_string());
164 let y = recurse(kl, v, f);
172 recurse(&mut kl, self, &mut f)?
176 // -------------------- Substition --------------------
178 pub trait Substitutor {
179 fn get(&self, kw: &str) -> Option<String>;
181 fn also<L: Into<Subst>>(&self, xl: L) -> ExtendedSubst<Self, Subst>
182 where Self: Clone + Sized {
183 ExtendedSubst(self.clone(), xl.into())
187 fn subst(&self, s: &str) -> String {
188 let re = Regex::new(r"@(\w+)@").expect("bad re!");
189 let mut errs = vec![];
190 let out = re.replace_all(s, |caps: ®ex::Captures| {
191 let kw = caps.get(1).expect("$1 missing!").as_str();
192 if kw == "" { return "".to_owned() }
193 let v = self.get(kw);
195 errs.push(kw.to_owned());
199 if ! errs.is_empty() {
200 throw!(anyhow!("bad substitution(s) {:?} in {:?}",
207 fn ss(&self, s: &str) -> Vec<String> {
211 .filter(|s| !s.is_empty())
217 fn gss(&self, s: &str) -> Vec<String> {
218 self.ss(&format!("-g @table@ {}", s))?
222 #[derive(Clone,Debug)]
223 pub struct Subst(HashMap<String,String>);
225 impl Substitutor for Subst {
226 fn get(&self, kw: &str) -> Option<String> {
227 self.0.get(kw).map(String::clone)
234 L: IntoIterator<Item=&'i (T, U)>>
237 fn from(l: L) -> Subst {
238 let map = l.into_iter()
239 .map(|(k,v)| (k.as_ref().to_owned(), v.as_ref().to_owned())).collect();
244 #[derive(Clone,Debug)]
245 pub struct ExtendedSubst<B: Substitutor, X: Substitutor>(B, X);
247 impl<B:Substitutor, X:Substitutor> Substitutor for ExtendedSubst<B, X> {
248 fn get(&self, kw: &str) -> Option<String> {
249 self.1.get(kw).or_else(|| self.0.get(kw))
253 impl Substitutor for DirSubst {
254 fn get(&self, kw: &str) -> Option<String> {
256 "url" => URL.to_owned(),
257 "src" => self.src.clone(),
258 "build" => self.start_dir.clone(),
259 "abstmp" => self.abstmp.clone(),
260 "target" => format!("{}/target", &self.start_dir),
261 "specs" => self.specs_dir(),
262 "table" => TABLE.to_owned(),
263 "command_socket" => "command.socket".to_owned(),
264 "examples" => format!("{}/examples", &self.src),
270 // ---------- requested/available test tracking ----------
272 #[derive(Clone,Debug)]
274 pub struct WantedTestsOpt {
279 pub struct TrackWantedTests {
280 wanted: WantedTestsOpt,
281 found: BTreeSet<String>,
284 impl WantedTestsOpt {
285 pub fn track(&self) -> TrackWantedTests {
286 TrackWantedTests { wanted: self.clone(), found: default() }
290 impl TrackWantedTests {
291 pub fn wantp(&mut self, tname: &str) -> bool {
292 self.found.insert(tname.to_owned());
294 self.wanted.tests.is_empty() ||
295 self.wanted.tests.iter().any(|s| s==tname);
300 impl Drop for TrackWantedTests {
302 let missing_tests = self.wanted.tests.iter().cloned()
303 .filter(|s| !self.found.contains(s))
304 .collect::<Vec<_>>();
306 if !missing_tests.is_empty() && !self.found.is_empty() {
307 for f in &self.found {
308 eprintln!("fyi: test that exists: {}", f);
310 for m in &missing_tests {
311 eprintln!("warning: unknown test requested: {}", m);
318 macro_rules! usual_wanted_tests {
319 ($ctx:ty, $su:ident) => {
321 fn wanted_tests(&mut self) -> &mut TrackWantedTests {
322 &mut self.su.wanted_tests
330 ($c:expr, $tname:expr, $s:stmt) => {
331 if $c.wanted_tests().wantp($tname) {
332 debug!("==================== {} starting ====================", $tname);
334 info!("==================== {} completed ====================", $tname);
336 trace!("= = = {} skipped = = =", $tname);
341 // -------------------- Extra anyhow result handling --------------------
343 pub trait PropagateDid {
344 fn propagate_did<T>(self, msg: &'static str) -> anyhow::Result<T>;
347 #[ext(pub, name=ResultGenDidExt)]
348 impl<T,E> Result<T,E> where Result<T,E>: anyhow::Context<T,E> {
349 fn did(self, msg: &'static str) -> anyhow::Result<T>
352 Ok(y) => { info!("did {}.", msg); Ok(y) }
353 n@ Err(_) => n.context(msg),
359 impl<T,E> Result<T,E> {
360 fn just_warn(self) -> Option<T>
373 // -------------------- cleanup_notify (signaling) --------------------
375 pub mod cleanup_notify {
376 use super::crates::*;
377 use otter_support::crates::*;
379 pub use super::Void; // TODO remove the need for this
382 use fehler::{throw, throws};
384 use nix::{unistd::*, fcntl::OFlag};
385 use nix::sys::signal::*;
386 use nix::Error as NE;
388 use std::os::unix::io::RawFd;
389 use std::panic::catch_unwind;
390 use std::process::Command;
393 pub struct Handle(RawFd);
396 fn mkpipe() -> (RawFd,RawFd) {
397 pipe2(OFlag::O_CLOEXEC).map_err(nix2io)?
401 fn read_await(fd: RawFd) {
403 let mut buf = [0u8; 1];
404 match nix::unistd::read(fd, &mut buf) {
406 Ok(_) => throw!(io::Error::from_raw_os_error(libc::EINVAL)),
407 Err(NE::EINTR) => continue,
408 _ => throw!(io::Error::last_os_error()),
413 fn nix2io(_n: nix::Error) -> io::Error {
414 io::Error::last_os_error()
419 pub fn new() -> Self {
420 let (reading_end, _writing_end) = mkpipe()
421 .context("create cleanup notify pipe")?;
422 // we leak the writing end, keeping it open only in this process
427 pub fn arm_hook(&self, cmd: &mut Command) { unsafe {
428 use std::os::unix::process::CommandExt;
430 let notify_writing_end = self.0;
431 let all_signals = nix::sys::signal::SigSet::all();
433 cmd.pre_exec(move || -> Result<(), io::Error> {
434 let semidaemon = nix::unistd::getpid();
435 let (reading_end, writing_end) = mkpipe()?;
437 match fork().map_err(nix2io)? {
438 ForkResult::Child => {
439 let _ = catch_unwind(move || -> Void {
441 SigmaskHow::SIG_BLOCK,
446 let _ = close(writing_end);
447 let _ = nix::unistd::dup2(2, 1);
450 if fd == notify_writing_end { continue }
452 if fd > writing_end && matches!(r, Err(NE::EBADF)) {
456 let _ = read_await(notify_writing_end);
457 let _ = kill(semidaemon, SIGTERM);
458 let _ = kill(semidaemon, SIGCONT);
461 let _ = raise(SIGABRT);
464 ForkResult::Parent{..} => {
466 close(writing_end).map_err(nix2io)?;
467 read_await(reading_end)?;
477 // -------------------- generalised daemon startup --------------------
480 pub fn fork_something_which_prints(mut cmd: Command,
481 cln: &cleanup_notify::Handle,
486 cmd.stdout(Stdio::piped());
487 cln.arm_hook(&mut cmd)?;
488 let mut child = cmd.spawn().context("spawn")?;
489 let mut report = BufReader::new(child.stdout.take().unwrap())
492 let l = report.next();
494 let s = child.try_wait().context("check on spawned child")?;
496 throw!(anyhow!("failed to start: wait status = {}", &e));
501 None => throw!(anyhow!("EOF (but it's still running?")),
502 Some(Err(e)) => throw!(AE::from(e).context("failed to read")),
505 let what = what.to_owned();
506 thread::spawn(move|| (||{
508 let l: Result<String, io::Error> = l;
509 let l = l.context("reading further output")?;
510 const MAXLEN: usize = 300;
511 if l.len() <= MAXLEN {
512 println!("{} {}", what, l);
514 println!("{} {}...", what, &l[..MAXLEN-3]);
518 })().context(what).just_warn()
521 Ok::<_,AE>((l, child))
522 })().with_context(|| what.to_owned())?
525 // ==================== principal actual setup code ====================
527 pub type EarlyArgPredicate<'f> = &'f mut dyn FnMut(&OsStr) -> bool;
530 pub fn reinvoke_via_bwrap(_opts: &Opts, current_exe: &str,
531 early: EarlyArgPredicate<'_>) -> Void {
532 debug!("running bwrap");
534 let mut bcmd = Command::new("bwrap");
536 .args("--unshare-net \
539 --die-with-parent".split(' '))
542 let (early, late) = {
543 let mut still_early = true;
544 env::args_os().skip(1)
545 .partition::<Vec<_>,_>(|s| {
546 still_early &= early(s);
551 bcmd.arg("--no-bwrap");
554 std::io::stdout().flush().context("flush stdout")?;
555 let e: AE = bcmd.exec().into();
556 throw!(e.context("exec bwrap"));
560 pub fn prepare_tmpdir<'x>(opts: &'x Opts, mut current_exe: &'x str) -> DirSubst {
562 fn getcwd() -> String {
566 .ok_or_else(|| anyhow!("path is not UTF-8"))?
570 if let Some(as_if) = &opts.as_if {
572 } else if let Some(test_name) = &opts.test_name {
573 current_exe = test_name;
576 let start_dir = getcwd()
577 .context("canonicalise our invocation directory (getcwd)")?;
580 match fs::metadata(&opts.tmp_dir) {
583 throw!(anyhow!("existing object is not a directory"));
585 if (m.st_mode() & 0o01002) != 0 {
587 "existing directory mode {:#o} is sticky or world-writeable. \
588 We use predictable pathnames so that would be a tmp race",
593 Err(e) if e.kind() == ErrorKind::NotFound => {
594 fs::create_dir(&opts.tmp_dir)
598 let e: AE = e.into();
599 throw!(e.context("stat existing directory"))
603 env::set_current_dir(&opts.tmp_dir)
604 .context("chdir into it")?;
608 .with_context(|| opts.tmp_dir.to_owned())
609 .context("prepare/create tmp-dir")?;
611 let leaf = current_exe.rsplitn(2, '/').next().unwrap();
612 let our_tmpdir = format!("{}/{}", &opts.tmp_dir, &leaf);
614 match fs::remove_dir_all(&leaf) {
616 Err(e) if e.kind() == ErrorKind::NotFound => {},
617 Err(e) => throw!(AE::from(e).context("remove previous directory")),
620 fs::DirBuilder::new().create(&leaf)
621 .context("create fresh subdirectory")?;
623 env::set_current_dir(&leaf)
624 .context("chdir into it")?;
628 .with_context(|| our_tmpdir.to_owned())
629 .context("prepare/create our tmp subdir")?;
632 getcwd().context("canonicalise our tmp subdir (getcwd)")?;
634 env::set_var("HOME", &abstmp);
635 env::set_var("TMPDIR", &abstmp);
636 env::set_var("OTTER_APITEST_START_DIR", &start_dir);
637 for v in "http_proxy https_proxy XAUTHORITY CDPATH \
638 SSH_AGENT_PID SSH_AUTH_SOCK WINDOWID WWW_HOME".split(' ')
643 let manifest_var = "CARGO_MANIFEST_DIR";
644 let src: String = (|| Ok::<_,AE>(match env::var(manifest_var) {
646 Err(env::VarError::NotPresent) => start_dir.clone(),
647 e@ Err(_) => throw!(e.context(manifest_var).err().unwrap()),
649 .context("find source code")?;
660 pub fn prepare_gameserver(cln: &cleanup_notify::Handle, ds: &DirSubst)
661 -> (MgmtChannelForGame, Child) {
662 let config = ds.subst(r##"
663 change_directory = "@abstmp@"
668 command_socket = "@command_socket@"
669 template_dir = "@src@/templates"
670 specs_dir = "@src@/specs"
671 nwtemplate_dir = "@src@/nwtemplates"
672 bundled_sources = "@target@/bundled-sources"
673 wasm_dir = "@target@/packed-wasm"
674 shapelibs = [ "@src@/library/*.toml" ]
675 libexec_dir = "@target@/debug"
676 usvg_bin = "@target@/release/usvg"
678 authorized_keys = "@abstmp@/authorized_keys"
679 ssh_proxy_command = "@target@/debug/otter-ssh-proxy --config @abstmp@/server-config.toml"
681 debug_js_inject_file = "@src@/templates/test-inject.js"
682 check_bundled_sources = false # For testing only! see LICENCE!
688 global_level = 'debug'
692 'hyper::server' = 'info'
693 "game::debugreader" = 'info'
694 "otter::updates" = 'trace'
695 "otter::hidden" = 'trace'
698 fs::write(CONFIG, &config)
699 .context(CONFIG).context("create server config")?;
701 start_gameserver(cln, ds)?
705 fn start_gameserver(cln: &cleanup_notify::Handle, ds: &DirSubst)
706 -> (MgmtChannelForGame, Child) {
707 let server_exe = ds.subst("@target@/debug/daemon-otter")?;
708 let mut cmd = Command::new(&server_exe);
710 .arg("--report-startup")
714 let (l,child) = fork_something_which_prints(cmd, cln, &server_exe)?;
715 if l != DAEMON_STARTUP_REPORT {
716 throw!(anyhow!("otter-daemon startup report {:?}, expected {:?}",
717 &l, DAEMON_STARTUP_REPORT));
721 .context("game server")?;
723 let mut mgmt_conn = MgmtChannel::connect(
724 &ds.subst("@command_socket@")?
727 mgmt_conn.cmd(&MgmtCommand::SetSuperuser(true))?;
728 mgmt_conn.cmd(&MgmtCommand::SelectAccount("server:".parse()?))?;
730 let mgmt_conn = mgmt_conn.for_game(
732 MgmtGameUpdateMode::Online
740 pub fn restart_gameserver(&mut self) {
741 let (mgmt_conn, child) = start_gameserver(&self.cln, &self.ds)?;
742 self.mgmt_conn = RefCell::new(mgmt_conn);
743 self.server_child = child;
747 // ---------- game spec ----------
749 #[derive(Copy,Clone,Error,Debug)]
750 #[error("wait status: {0}")]
751 pub struct ExitStatusError(pub std::process::ExitStatus);
754 pub struct OtterOutput {
755 output: Option<NamedTempFile>,
757 impl Deref for OtterOutput {
758 type Target = fs::File;
759 fn deref(&self) -> &fs::File { self.output.as_ref().unwrap().as_file() }
761 impl DerefMut for OtterOutput {
762 fn deref_mut(&mut self) -> &mut fs::File {
763 self.output.as_mut().unwrap().as_file_mut()
766 impl From<OtterOutput> for String {
767 fn from(mut oo: OtterOutput) -> String {
768 let mut s = String::new();
769 let mut o = oo.output.take().unwrap();
771 o.read_to_string(&mut s).unwrap();
775 impl From<&mut OtterOutput> for String {
776 fn from(oo: &mut OtterOutput) -> String {
777 let mut s = String::new();
778 let o = oo.output.as_mut().unwrap();
780 o.read_to_string(&mut s).unwrap();
784 impl Drop for OtterOutput {
786 if let Some(mut o) = self.output.take() {
787 io::copy(&mut o, &mut io::stdout()).expect("copy otter stdout");
792 pub trait OtterArgsSpec {
793 fn to_args(&self, ds: &dyn Substitutor) -> Vec<String>;
796 impl<S> OtterArgsSpec for [S] where for <'s> &'s S: Into<String> {
797 fn to_args(&self, _: &dyn Substitutor) -> Vec<String> {
798 self.iter().map(|s| s.into()).collect()
801 impl<S> OtterArgsSpec for Vec<S> where for <'s> &'s S: Into<String> {
802 fn to_args(&self, ds: &dyn Substitutor) -> Vec<String> {
803 self.as_slice().to_args(ds)
806 impl OtterArgsSpec for &str {
807 fn to_args(&self, ds: &dyn Substitutor) -> Vec<String> {
808 ds.ss(self).expect(self)
811 impl OtterArgsSpec for G<&str> {
812 fn to_args(&self, ds: &dyn Substitutor) -> Vec<String> {
813 ds.gss(self.0).expect(self.0)
816 #[derive(Debug,Clone)]
817 pub struct G<T>(pub T);
820 pub fn specs_dir(&self) -> String {
821 format!("{}/specs" , &self.src)
824 pub fn example_bundle(&self) -> String {
825 self.subst("@examples@/test-bundle.zip").unwrap()
829 pub fn otter(&self, xargs: &dyn OtterArgsSpec) -> OtterOutput
831 self.otter_prctx(&default(), xargs)?
835 pub fn otter_prctx(&self, prctx: &PathResolveContext,
836 xargs: &dyn OtterArgsSpec)
840 let exe = ds.subst("@target@/debug/otter")?;
841 let specs = self.subst("@src@/specs")?;
842 let mut args: Vec<String> = vec![];
843 args.push("--config" .to_owned()); args.push(prctx.resolve(CONFIG));
844 args.push("--spec-dir".to_owned()); args.push(prctx.resolve(&specs) );
845 args.extend(xargs.to_args(ds));
846 let dbg = format!("running {} {:?}", &exe, &args);
847 let mut output = NamedTempFile::new_in(
848 ds.subst("@abstmp@").unwrap()
852 let mut cmd = Command::new(&exe);
854 cmd.stdout(output.as_file().try_clone().unwrap());
856 .spawn().context("spawn")?
857 .wait().context("wait")?;
859 throw!(ExitStatusError(st));
864 .context("run otter client")?;
866 output.rewind().unwrap();
867 OtterOutput { output: Some(output) }
871 pub fn game_spec_path(&self) -> String {
872 self.subst("@specs@/demo.game.toml")?
876 pub fn game_spec_data(&self) -> GameSpec {
877 let path = self.game_spec_path()?;
879 let data = fs::read(&path).context("read")?;
880 let data = std::str::from_utf8(&data).context("convert from UTF-8")?;
881 let data: toml::Value = data.parse().context("parse TOM")?;
883 let data = toml_de::from_value(&data).context("interperet TOML")?;
887 .context("game spec")?
892 pub fn prepare_game(ds: &DirSubst, prctx: &PathResolveContext, table: &str)
894 let game_spec = ds.game_spec_path()?;
895 let subst = ds.also(&[
896 ("table", table.to_owned()),
897 ("game_spec", prctx.resolve(&game_spec)),
899 ds.otter_prctx(prctx, &subst.ss(
900 "--account server: --game @table@ \
902 --reset-table @specs@/test.table.toml \
904 ")?).context("reset table")?;
906 let instance: InstanceName = table.parse()
907 .with_context(|| table.to_owned())
908 .context("parse table name")?;
913 // ==================== post-setup facilities ====================
915 // -------------------- static users --------------------
917 pub struct StaticUserSetup {
918 pub nick: &'static str,
920 pub player: PlayerId,
925 pub fn setup_static_users(&self, mgmt_conn: &mut MgmtChannelForGame,
926 layout: PresentationLayout)
927 -> Vec<StaticUserSetup>
930 fn mk(su: &DirSubst, mgmt_conn: &mut MgmtChannelForGame,
931 layout: PresentationLayout, u: StaticUser)
934 let nick: &str = u.into();
935 let token = u.get_str("Token").expect("StaticUser missing Token");
936 let pl = AbbrevPresentationLayout(layout).to_string();
937 let subst = su.also([
944 .ss("--super -g@table@ \
945 --account server:@nick@ \
946 --fixed-token @token@ \
949 let player = mgmt_conn.has_player(
950 &subst.subst("server:@nick@")?.parse()?
953 let url = subst.subst("@url@/@pl@?@token@")?;
954 StaticUserSetup { nick, url, player }
957 StaticUser::iter().map(
959 let ssu = mk(self, mgmt_conn, layout, u).context("create")?;
962 .with_context(|| format!("{:?}", u))
963 .context("make static user")
965 .collect::<Result<Vec<StaticUserSetup>,AE>>()?
969 // -------------------- concurrency management --------------------
971 pub struct OtterPauseable(nix::unistd::Pid);
972 pub struct OtterPaused(nix::unistd::Pid);
975 pub fn otter_pauseable(&self) -> OtterPauseable {
976 OtterPauseable(nix::unistd::Pid::from_raw(
977 self.server_child.id() as nix::libc::pid_t
982 pub fn pause_otter(&self) -> OtterPaused {
983 self.otter_pauseable().pause()?
986 pub fn mgmt_conn<'m>(&'m self) -> RefMut<'m, MgmtChannelForGame> {
987 self.mgmt_conn.borrow_mut()
991 impl OtterPauseable {
993 pub fn pause(self) -> OtterPaused {
994 nix::sys::signal::kill(self.0, nix::sys::signal::SIGSTOP)?;
1001 pub fn resume(self) -> OtterPauseable {
1002 nix::sys::signal::kill(self.0, nix::sys::signal::SIGCONT)?;
1003 OtterPauseable(self.0)
1007 impl Drop for OtterPaused {
1008 fn drop(&mut self) {
1009 debug!("note, otter server pid={} was still paused", self.0);
1013 // -------------------- utilities --------------------
1018 fn game_synch(&mut self, game: InstanceName) -> Generation {
1019 let cmd = MgmtCommand::AlterGame {
1020 how: MgmtGameUpdateMode::Online,
1021 insns: vec![ MgmtGameInstruction::SynchLog ],
1024 let gen = if_chain!{
1025 let resp = self.cmd(&cmd)?;
1026 if let MgmtResponse::AlterGame {
1030 if let [MgmtGameResponse::Synch(gen)] = responses[..];
1032 else { throw!(anyhow!("unexpected resp to synch {:?}", resp)) }
1034 trace!("gen={} ...", gen);
1038 fn fakerng_load(&mut self, values: &[&dyn ToString]) -> Result<(),AE> {
1039 let values = values.iter().map(|v| v.to_string()).collect();
1040 self.cmd(&MC::LoadFakeRng(values))?;
1043 fn fakerng_unfake(&mut self) -> Result<(),AE> {
1044 self.cmd(&MC::LoadFakeRng(vec![]))?;
1049 // ==================== core entrypoint, for wdriver too ====================
1052 pub fn setup_core<O>(module_paths: &[&str]) ->
1053 (O, Instance, SetupCore)
1054 where O: StructOpt + AsRef<Opts>
1056 let mut builder = env_logger::Builder::new();
1058 .format_timestamp_micros()
1059 .format_level(true);
1060 for too_verbose in &[
1061 "html5ever::tokenizer",
1062 "html5ever::tree_builder",
1063 "selectors::matching",
1065 "hyper::client::pool",
1067 builder.filter_module(too_verbose, log::LevelFilter::Info);
1070 for module in module_paths {
1072 .filter_module(module, log::LevelFilter::Debug);
1076 .filter_level(log::LevelFilter::Debug)
1077 .parse_env("OTTER_TEST_LOG")
1081 let caller_opts = O::from_args();
1082 let opts = caller_opts.as_ref();
1084 let current_exe: String = env::current_exe()
1085 .context("find current executable")?
1087 .ok_or_else(|| anyhow!("current executable path is not UTF-8 !"))?
1093 &mut |s: &OsStr| s.to_str().unwrap().starts_with("--test=")
1095 .context("reinvoke via bwrap")?;
1098 info!("pid = {}", nix::unistd::getpid());
1099 sleep(opts.pause.into());
1101 let cln = cleanup_notify::Handle::new()?;
1102 let ds = prepare_tmpdir(opts, ¤t_exe)?;
1104 let (mgmt_conn, server_child) =
1105 prepare_gameserver(&cln, &ds).did("setup game server")?;
1108 prepare_game(&ds, &default(), TABLE).context("setup game")?;
1110 let wanted_tests = opts.tests.track();
1118 mgmt_conn: mgmt_conn.into(),
1124 pub struct PortmanteauMember {
1125 pub path: &'static str,
1126 pub f: fn() -> Result<(), Explode>,
1128 inventory::collect!(PortmanteauMember);
1131 macro_rules! portmanteau_has {
1132 ($path:literal, $mod:ident) => {
1133 #[path = $path] mod $mod;
1134 inventory::submit!(PortmanteauMember { path: $path, f: $mod::main });
1139 pub fn portmanteau_main(prefix: &str){
1140 let arg = 'arg: loop {
1141 for (ai, s) in env::args().enumerate() {
1142 let plausible = |s: &str| s.starts_with(&format!("{}-",prefix));
1144 break 'arg if ai == 0 {
1145 let s = s.rsplitn(2,'/').next().unwrap();
1146 if ! plausible(s) { continue }
1149 let s = s.strip_prefix("--test=")
1151 "found non-long-option looking for --test={}-*: {:?}",
1154 panic!("found non --no-bwrap --{}-* option looking for --{}-*",
1160 panic!("ran out of options looking for --test={}-*", prefix);
1163 let f = inventory::iter::<PortmanteauMember>.into_iter()
1165 let n = pm.path.strip_suffix(".rs").unwrap();
1166 if n == arg { Some(pm.f) } else { None }
1168 .expect("unrecognosed {wdt,at}-* portanteau member");