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;
36 use otter::prelude::define_index_type;
37 define_index_type!{ pub struct PIA = usize; }
38 define_index_type!{ pub struct PIB = usize; }
42 // -------------------- private crates ----------
44 use otter_support::config::DAEMON_STARTUP_REPORT;
46 // ==================== public constants ====================
48 pub const TABLE: &str = "server::dummy";
49 pub const CONFIG: &str = "server-config.toml";
51 pub const URL: &str = "http://localhost:8000";
53 #[derive(Copy,Clone,Debug,Eq,PartialEq,Ord,PartialOrd)]
54 #[derive(FromPrimitive,EnumIter,IntoStaticStr,EnumProperty)]
55 #[strum(serialize_all = "snake_case")]
57 #[strum(props(Token="kmqAKPwK4TfReFjMor8MJhdRPBcwIBpe"))] Alice,
58 #[strum(props(Token="ccg9kzoTh758QrVE1xMY7BQWB36dNJTx"))] Bob,
61 // ==================== principal public structs ====================
63 #[derive(Debug,Clone)]
66 #[structopt(long="--as-if")]
67 pub as_if: Option<String>,
69 #[structopt(long="--no-bwrap")]
72 #[structopt(long="--tmp-dir", default_value="tmp")]
75 #[structopt(long="--pause", default_value="0ms")]
76 pub pause: humantime::Duration,
79 pub tests: WantedTestsOpt,
81 #[structopt(long="--test")]
82 test_name: Option<String>,
86 pub struct SetupCore {
88 pub mgmt_conn: RefCell<MgmtChannelForGame>,
89 pub server_child: Child,
90 pub wanted_tests: TrackWantedTests,
91 pub cln: cleanup_notify::Handle,
94 #[derive(Clone,Debug)]
98 pub start_dir: String,
102 pub struct Instance(pub InstanceName);
104 // ==================== Facilities for tests ====================
106 impl AsRef<Opts> for Opts { fn as_ref(&self) -> &Opts { self } }
110 impl<'e, E:Into<Box<dyn Error + 'e>>> From<E> for Explode {
111 fn from(e: E) -> Explode {
112 let mut m = "exploding on error".to_string();
113 let e: Box<dyn Error> = e.into();
114 let mut e: Option<&dyn Error> = Some(&*e);
115 while let Some(te) = e {
116 m += &format!(": {}", &te);
122 impl From<Explode> for anyhow::Error {
123 fn from(e: Explode) -> AE { match e { } }
125 #[ext(pub, name=ResultExplodeExt)]
126 impl<T> Result<T,Explode> {
127 fn y(self) -> T { match self { Ok(y) => y, Err(n) => match n { } } }
128 fn did(self, msg: &'static str) -> anyhow::Result<T> {
129 ResultGenDidExt::<_,AE>::did(Ok(self.y()), msg)
134 impl<E:Error> From<Explode> for E {
135 fn from(e: Explode) -> E { match e { } }
140 fn set<K: Into<String>>(&mut self, k: K, v: &JsV) {
141 self.as_object_mut().unwrap().insert(k.into(), v.clone());
144 fn extend<I,K,V>(&mut self, i: I)
145 where I: IntoIterator<Item=(K, V)>,
149 let i = i.into_iter().map(|(k,v)| (k.into(), v.borrow().clone()));
150 self.as_object_mut().unwrap().extend(i);
154 fn tree_walk<F,E>(&self, #[allow(unused_mut,unused_variables)] mut f: F)
155 where F: FnMut(&[String], &JsV) -> Result<(),E>
158 fn recurse<F,E>(kl: &mut Vec<String>, v: &JsV, f: &mut F)
159 where F: FnMut(&[String], &JsV) -> Result<(),E> {
161 if let Some(o) = v.as_object() {
163 kl.push(k.to_owned());
164 let y = recurse(kl, v, f);
168 } else if let Some(a) = v.as_array() {
169 for (k,v) in a.iter().enumerate() {
170 kl.push(k.to_string());
171 let y = recurse(kl, v, f);
179 recurse(&mut kl, self, &mut f)?
183 // -------------------- Substition --------------------
185 pub trait Substitutor {
186 fn get(&self, kw: &str) -> Option<String>;
188 fn also<L: Into<Subst>>(&self, xl: L) -> ExtendedSubst<Self, Subst>
189 where Self: Clone + Sized {
190 ExtendedSubst(self.clone(), xl.into())
194 fn subst(&self, s: &str) -> String {
195 let re = Regex::new(r"@(\w+)@").expect("bad re!");
196 let mut errs = vec![];
197 let out = re.replace_all(s, |caps: ®ex::Captures| {
198 let kw = caps.get(1).expect("$1 missing!").as_str();
199 if kw == "" { return "".to_owned() }
200 let v = self.get(kw);
202 errs.push(kw.to_owned());
206 if ! errs.is_empty() {
207 throw!(anyhow!("bad substitution(s) {:?} in {:?}",
214 fn ss(&self, s: &str) -> Vec<String> {
218 .filter(|s| !s.is_empty())
224 fn gss(&self, s: &str) -> Vec<String> {
225 self.ss(&format!("-g @table@ {}", s))?
229 #[derive(Clone,Debug)]
230 pub struct Subst(HashMap<String,String>);
232 impl Substitutor for Subst {
233 fn get(&self, kw: &str) -> Option<String> {
234 self.0.get(kw).map(String::clone)
241 L: IntoIterator<Item=&'i (T, U)>>
244 fn from(l: L) -> Subst {
245 let map = l.into_iter()
246 .map(|(k,v)| (k.as_ref().to_owned(), v.as_ref().to_owned())).collect();
251 #[derive(Clone,Debug)]
252 pub struct ExtendedSubst<B: Substitutor, X: Substitutor>(B, X);
254 impl<B:Substitutor, X:Substitutor> Substitutor for ExtendedSubst<B, X> {
255 fn get(&self, kw: &str) -> Option<String> {
256 self.1.get(kw).or_else(|| self.0.get(kw))
260 impl Substitutor for DirSubst {
261 fn get(&self, kw: &str) -> Option<String> {
263 "url" => URL.to_owned(),
264 "src" => self.src.clone(),
265 "build" => self.start_dir.clone(),
266 "abstmp" => self.abstmp.clone(),
267 "target" => format!("{}/target", &self.start_dir),
268 "specs" => self.specs_dir(),
269 "table" => TABLE.to_owned(),
270 "command_socket" => "command.socket".to_owned(),
271 "examples" => format!("{}/examples", &self.src),
277 // ---------- requested/available test tracking ----------
279 #[derive(Clone,Debug)]
281 pub struct WantedTestsOpt {
286 pub struct TrackWantedTests {
287 wanted: WantedTestsOpt,
288 found: BTreeSet<String>,
291 impl WantedTestsOpt {
292 pub fn track(&self) -> TrackWantedTests {
293 TrackWantedTests { wanted: self.clone(), found: default() }
297 impl TrackWantedTests {
298 pub fn wantp(&mut self, tname: &str) -> bool {
299 self.found.insert(tname.to_owned());
301 self.wanted.tests.is_empty() ||
302 self.wanted.tests.iter().any(|s| s==tname);
307 impl Drop for TrackWantedTests {
309 let missing_tests = self.wanted.tests.iter().cloned()
310 .filter(|s| !self.found.contains(s))
311 .collect::<Vec<_>>();
313 if !missing_tests.is_empty() && !self.found.is_empty() {
314 for f in &self.found {
315 eprintln!("fyi: test that exists: {}", f);
317 for m in &missing_tests {
318 eprintln!("warning: unknown test requested: {}", m);
325 macro_rules! usual_wanted_tests {
326 ($ctx:ty, $su:ident) => {
328 fn wanted_tests(&mut self) -> &mut TrackWantedTests {
329 &mut self.su.wanted_tests
337 ($c:expr, $tname:expr, $s:stmt) => {
338 if $c.wanted_tests().wantp($tname) {
339 debug!("==================== {} starting ====================", $tname);
341 info!("==================== {} completed ====================", $tname);
343 trace!("= = = {} skipped = = =", $tname);
348 // -------------------- Extra anyhow result handling --------------------
350 pub trait PropagateDid {
351 fn propagate_did<T>(self, msg: &'static str) -> anyhow::Result<T>;
354 #[ext(pub, name=ResultGenDidExt)]
355 impl<T,E> Result<T,E> where Result<T,E>: anyhow::Context<T,E> {
356 fn did(self, msg: &'static str) -> anyhow::Result<T>
359 Ok(y) => { info!("did {}.", msg); Ok(y) }
360 n@ Err(_) => n.context(msg),
366 impl<T,E> Result<T,E> {
367 fn just_warn(self) -> Option<T>
380 // -------------------- cleanup_notify (signaling) --------------------
382 pub mod cleanup_notify {
383 use super::crates::*;
384 use otter_support::crates::*;
386 pub use super::Void; // TODO remove the need for this
389 use fehler::{throw, throws};
391 use nix::{unistd::*, fcntl::OFlag};
392 use nix::sys::signal::*;
393 use nix::Error as NE;
395 use std::os::unix::io::RawFd;
396 use std::panic::catch_unwind;
397 use std::process::Command;
400 pub struct Handle(RawFd);
403 fn mkpipe() -> (RawFd,RawFd) {
404 pipe2(OFlag::O_CLOEXEC).map_err(nix2io)?
408 fn read_await(fd: RawFd) {
410 let mut buf = [0u8; 1];
411 match nix::unistd::read(fd, &mut buf) {
413 Ok(_) => throw!(io::Error::from_raw_os_error(libc::EINVAL)),
414 Err(NE::EINTR) => continue,
415 _ => throw!(io::Error::last_os_error()),
420 fn nix2io(_n: nix::Error) -> io::Error {
421 io::Error::last_os_error()
426 pub fn new() -> Self {
427 let (reading_end, _writing_end) = mkpipe()
428 .context("create cleanup notify pipe")?;
429 // we leak the writing end, keeping it open only in this process
434 pub fn arm_hook(&self, cmd: &mut Command) { unsafe {
435 use std::os::unix::process::CommandExt;
437 let notify_writing_end = self.0;
438 let all_signals = nix::sys::signal::SigSet::all();
440 cmd.pre_exec(move || -> Result<(), io::Error> {
441 let semidaemon = nix::unistd::getpid();
442 let (reading_end, writing_end) = mkpipe()?;
444 match fork().map_err(nix2io)? {
445 ForkResult::Child => {
446 let _ = catch_unwind(move || -> Void {
448 SigmaskHow::SIG_BLOCK,
453 let _ = close(writing_end);
454 let _ = nix::unistd::dup2(2, 1);
457 if fd == notify_writing_end { continue }
459 if fd > writing_end && matches!(r, Err(NE::EBADF)) {
463 let _ = read_await(notify_writing_end);
464 let _ = kill(semidaemon, SIGTERM);
465 let _ = kill(semidaemon, SIGCONT);
468 let _ = raise(SIGABRT);
471 ForkResult::Parent{..} => {
473 close(writing_end).map_err(nix2io)?;
474 read_await(reading_end)?;
484 // -------------------- generalised daemon startup --------------------
487 pub fn fork_something_which_prints(mut cmd: Command,
488 cln: &cleanup_notify::Handle,
493 cmd.stdout(Stdio::piped());
494 cln.arm_hook(&mut cmd)?;
495 let mut child = cmd.spawn().context("spawn")?;
496 let mut report = BufReader::new(child.stdout.take().unwrap())
499 let l = report.next();
501 let s = child.try_wait().context("check on spawned child")?;
503 throw!(anyhow!("failed to start: wait status = {}", &e));
508 None => throw!(anyhow!("EOF (but it's still running?")),
509 Some(Err(e)) => throw!(AE::from(e).context("failed to read")),
512 let what = what.to_owned();
513 thread::spawn(move|| (||{
515 let l: Result<String, io::Error> = l;
516 let l = l.context("reading further output")?;
517 const MAXLEN: usize = 300;
518 if l.len() <= MAXLEN {
519 println!("{} {}", what, l);
521 println!("{} {}...", what, &l[..MAXLEN-3]);
525 })().context(what).just_warn()
528 Ok::<_,AE>((l, child))
529 })().with_context(|| what.to_owned())?
532 // ==================== principal actual setup code ====================
534 pub type EarlyArgPredicate<'f> = &'f mut dyn FnMut(&OsStr) -> bool;
537 pub fn reinvoke_via_bwrap(_opts: &Opts, current_exe: &str,
538 early: EarlyArgPredicate<'_>) -> Void {
539 debug!("running bwrap");
541 let mut bcmd = Command::new("bwrap");
543 .args("--unshare-net \
546 --die-with-parent".split(' '))
549 let (early, late) = {
550 let mut still_early = true;
551 env::args_os().skip(1)
552 .partition::<Vec<_>,_>(|s| {
553 still_early &= early(s);
558 bcmd.arg("--no-bwrap");
561 std::io::stdout().flush().context("flush stdout")?;
562 let e: AE = bcmd.exec().into();
563 throw!(e.context("exec bwrap"));
567 pub fn prepare_tmpdir<'x>(opts: &'x Opts, mut current_exe: &'x str) -> DirSubst {
569 fn getcwd() -> String {
573 .ok_or_else(|| anyhow!("path is not UTF-8"))?
577 if let Some(as_if) = &opts.as_if {
579 } else if let Some(test_name) = &opts.test_name {
580 current_exe = test_name;
583 let start_dir = getcwd()
584 .context("canonicalise our invocation directory (getcwd)")?;
587 match fs::metadata(&opts.tmp_dir) {
590 throw!(anyhow!("existing object is not a directory"));
592 if (m.st_mode() & 0o01002) != 0 {
594 "existing directory mode {:#o} is sticky or world-writeable. \
595 We use predictable pathnames so that would be a tmp race",
600 Err(e) if e.kind() == ErrorKind::NotFound => {
601 fs::create_dir(&opts.tmp_dir)
605 let e: AE = e.into();
606 throw!(e.context("stat existing directory"))
610 env::set_current_dir(&opts.tmp_dir)
611 .context("chdir into it")?;
615 .with_context(|| opts.tmp_dir.to_owned())
616 .context("prepare/create tmp-dir")?;
618 let leaf = current_exe.rsplitn(2, '/').next().unwrap();
619 let our_tmpdir = format!("{}/{}", &opts.tmp_dir, &leaf);
621 match fs::remove_dir_all(&leaf) {
623 Err(e) if e.kind() == ErrorKind::NotFound => {},
624 Err(e) => throw!(AE::from(e).context("remove previous directory")),
627 fs::DirBuilder::new().create(&leaf)
628 .context("create fresh subdirectory")?;
630 env::set_current_dir(&leaf)
631 .context("chdir into it")?;
635 .with_context(|| our_tmpdir.to_owned())
636 .context("prepare/create our tmp subdir")?;
639 getcwd().context("canonicalise our tmp subdir (getcwd)")?;
641 env::set_var("HOME", &abstmp);
642 env::set_var("TMPDIR", &abstmp);
643 env::set_var("OTTER_APITEST_START_DIR", &start_dir);
644 for v in "http_proxy https_proxy XAUTHORITY CDPATH \
645 SSH_AGENT_PID SSH_AUTH_SOCK WINDOWID WWW_HOME".split(' ')
650 let manifest_var = "CARGO_MANIFEST_DIR";
651 let src: String = (|| Ok::<_,AE>(match env::var(manifest_var) {
653 Err(env::VarError::NotPresent) => start_dir.clone(),
654 e@ Err(_) => throw!(e.context(manifest_var).err().unwrap()),
656 .context("find source code")?;
667 pub fn prepare_gameserver(cln: &cleanup_notify::Handle, ds: &DirSubst)
668 -> (MgmtChannelForGame, Child) {
669 let config = ds.subst(r##"
670 change_directory = "@abstmp@"
675 command_socket = "@command_socket@"
676 template_dir = "@src@/templates"
677 specs_dir = "@src@/specs"
678 nwtemplate_dir = "@src@/nwtemplates"
679 bundled_sources = "@target@/bundled-sources"
680 wasm_dir = "@target@/packed-wasm"
681 shapelibs = [ "@src@/library/*.toml" ]
682 libexec_dir = "@target@/debug"
683 usvg_bin = "@target@/release/usvg"
685 authorized_keys = "@abstmp@/authorized_keys"
686 ssh_proxy_command = "@target@/debug/otter-ssh-proxy --config @abstmp@/server-config.toml"
688 debug_js_inject_file = "@src@/templates/test-inject.js"
689 check_bundled_sources = false # For testing only! see LICENCE!
695 global_level = 'debug'
699 'hyper::server' = 'info'
700 "game::debugreader" = 'info'
701 "otter::updates" = 'trace'
702 "otter::hidden" = 'trace'
705 fs::write(CONFIG, &config)
706 .context(CONFIG).context("create server config")?;
708 start_gameserver(cln, ds)?
712 fn start_gameserver(cln: &cleanup_notify::Handle, ds: &DirSubst)
713 -> (MgmtChannelForGame, Child) {
714 let server_exe = ds.subst("@target@/debug/daemon-otter")?;
715 let mut cmd = Command::new(&server_exe);
717 .arg("--report-startup")
721 let (l,child) = fork_something_which_prints(cmd, cln, &server_exe)?;
722 if l != DAEMON_STARTUP_REPORT {
723 throw!(anyhow!("otter-daemon startup report {:?}, expected {:?}",
724 &l, DAEMON_STARTUP_REPORT));
728 .context("game server")?;
730 let mut mgmt_conn = MgmtChannel::connect(
731 &ds.subst("@command_socket@")?
734 mgmt_conn.cmd(&MgmtCommand::SetSuperuser(true))?;
735 mgmt_conn.cmd(&MgmtCommand::SelectAccount("server:".parse()?))?;
737 let mgmt_conn = mgmt_conn.for_game(
739 MgmtGameUpdateMode::Online
747 pub fn restart_gameserver(&mut self) {
748 let (mgmt_conn, child) = start_gameserver(&self.cln, &self.ds)?;
749 self.mgmt_conn = RefCell::new(mgmt_conn);
750 self.server_child = child;
754 // ---------- game spec ----------
756 #[derive(Copy,Clone,Error,Debug)]
757 #[error("wait status: {0}")]
758 pub struct ExitStatusError(pub std::process::ExitStatus);
761 pub struct OtterOutput {
762 output: Option<NamedTempFile>,
764 impl Deref for OtterOutput {
765 type Target = fs::File;
766 fn deref(&self) -> &fs::File { self.output.as_ref().unwrap().as_file() }
768 impl DerefMut for OtterOutput {
769 fn deref_mut(&mut self) -> &mut fs::File {
770 self.output.as_mut().unwrap().as_file_mut()
773 impl From<OtterOutput> for String {
774 fn from(mut oo: OtterOutput) -> String {
775 let mut s = String::new();
776 let mut o = oo.output.take().unwrap();
778 o.read_to_string(&mut s).unwrap();
782 impl From<&mut OtterOutput> for String {
783 fn from(oo: &mut OtterOutput) -> String {
784 let mut s = String::new();
785 let o = oo.output.as_mut().unwrap();
787 o.read_to_string(&mut s).unwrap();
791 impl Drop for OtterOutput {
793 if let Some(mut o) = self.output.take() {
794 io::copy(&mut o, &mut io::stdout()).expect("copy otter stdout");
799 pub trait OtterArgsSpec {
800 fn to_args(&self, ds: &dyn Substitutor) -> Vec<String>;
803 impl<S> OtterArgsSpec for [S] where for <'s> &'s S: Into<String> {
804 fn to_args(&self, _: &dyn Substitutor) -> Vec<String> {
805 self.iter().map(|s| s.into()).collect()
808 impl<S> OtterArgsSpec for Vec<S> where for <'s> &'s S: Into<String> {
809 fn to_args(&self, ds: &dyn Substitutor) -> Vec<String> {
810 self.as_slice().to_args(ds)
813 impl OtterArgsSpec for &str {
814 fn to_args(&self, ds: &dyn Substitutor) -> Vec<String> {
815 ds.ss(self).expect(self)
818 impl OtterArgsSpec for G<&str> {
819 fn to_args(&self, ds: &dyn Substitutor) -> Vec<String> {
820 ds.gss(self.0).expect(self.0)
823 #[derive(Debug,Clone)]
824 pub struct G<T>(pub T);
827 pub fn specs_dir(&self) -> String {
828 format!("{}/specs" , &self.src)
831 pub fn example_bundle(&self) -> String {
832 self.subst("@examples@/test-bundle.zip").unwrap()
836 pub fn otter(&self, xargs: &dyn OtterArgsSpec) -> OtterOutput
838 self.otter_prctx(&default(), xargs)?
842 pub fn otter_prctx(&self, prctx: &PathResolveContext,
843 xargs: &dyn OtterArgsSpec)
847 let exe = ds.subst("@target@/debug/otter")?;
848 let specs = self.subst("@src@/specs")?;
849 let mut args: Vec<String> = vec![];
850 args.push("--config" .to_owned()); args.push(prctx.resolve(CONFIG));
851 args.push("--spec-dir".to_owned()); args.push(prctx.resolve(&specs) );
852 args.extend(xargs.to_args(ds));
853 let dbg = format!("running {} {:?}", &exe, &args);
854 let mut output = NamedTempFile::new_in(
855 ds.subst("@abstmp@").unwrap()
859 let mut cmd = Command::new(&exe);
861 cmd.stdout(output.as_file().try_clone().unwrap());
863 .spawn().context("spawn")?
864 .wait().context("wait")?;
866 throw!(ExitStatusError(st));
871 .context("run otter client")?;
873 output.rewind().unwrap();
874 OtterOutput { output: Some(output) }
878 pub fn game_spec_path(&self) -> String {
879 self.subst("@specs@/demo.game.toml")?
883 pub fn game_spec_data(&self) -> GameSpec {
884 let path = self.game_spec_path()?;
886 let data = fs::read(&path).context("read")?;
887 let data = std::str::from_utf8(&data).context("convert from UTF-8")?;
888 let data: toml::Value = data.parse().context("parse TOM")?;
890 let data = toml_de::from_value(&data).context("interperet TOML")?;
894 .context("game spec")?
899 pub fn prepare_game(ds: &DirSubst, prctx: &PathResolveContext, table: &str)
901 let game_spec = ds.game_spec_path()?;
902 let subst = ds.also(&[
903 ("table", table.to_owned()),
904 ("game_spec", prctx.resolve(&game_spec)),
906 ds.otter_prctx(prctx, &subst.ss(
907 "--account server: --game @table@ \
909 --reset-table @specs@/test.table.toml \
911 ")?).context("reset table")?;
913 let instance: InstanceName = table.parse()
914 .with_context(|| table.to_owned())
915 .context("parse table name")?;
920 // ==================== post-setup facilities ====================
922 // -------------------- static users --------------------
924 pub struct StaticUserSetup {
925 pub nick: &'static str,
927 pub player: PlayerId,
932 pub fn setup_static_users(&self, mgmt_conn: &mut MgmtChannelForGame,
933 layout: PresentationLayout)
934 -> Vec<StaticUserSetup>
937 fn mk(su: &DirSubst, mgmt_conn: &mut MgmtChannelForGame,
938 layout: PresentationLayout, u: StaticUser)
941 let nick: &str = u.into();
942 let token = u.get_str("Token").expect("StaticUser missing Token");
943 let pl = AbbrevPresentationLayout(layout).to_string();
944 let subst = su.also([
951 .ss("--super -g@table@ \
952 --account server:@nick@ \
953 --fixed-token @token@ \
956 let player = mgmt_conn.has_player(
957 &subst.subst("server:@nick@")?.parse()?
960 let url = subst.subst("@url@/@pl@?@token@")?;
961 StaticUserSetup { nick, url, player }
964 StaticUser::iter().map(
966 let ssu = mk(self, mgmt_conn, layout, u).context("create")?;
969 .with_context(|| format!("{:?}", u))
970 .context("make static user")
972 .collect::<Result<Vec<StaticUserSetup>,AE>>()?
976 // -------------------- concurrency management --------------------
978 pub struct OtterPauseable(nix::unistd::Pid);
979 pub struct OtterPaused(nix::unistd::Pid);
982 pub fn otter_pauseable(&self) -> OtterPauseable {
983 OtterPauseable(nix::unistd::Pid::from_raw(
984 self.server_child.id() as nix::libc::pid_t
989 pub fn pause_otter(&self) -> OtterPaused {
990 self.otter_pauseable().pause()?
993 pub fn mgmt_conn<'m>(&'m self) -> RefMut<'m, MgmtChannelForGame> {
994 self.mgmt_conn.borrow_mut()
998 impl OtterPauseable {
1000 pub fn pause(self) -> OtterPaused {
1001 nix::sys::signal::kill(self.0, nix::sys::signal::SIGSTOP)?;
1008 pub fn resume(self) -> OtterPauseable {
1009 nix::sys::signal::kill(self.0, nix::sys::signal::SIGCONT)?;
1010 OtterPauseable(self.0)
1014 impl Drop for OtterPaused {
1015 fn drop(&mut self) {
1016 debug!("note, otter server pid={} was still paused", self.0);
1020 // -------------------- utilities --------------------
1025 fn game_synch(&mut self, game: InstanceName) -> Generation {
1026 let cmd = MgmtCommand::AlterGame {
1027 how: MgmtGameUpdateMode::Online,
1028 insns: vec![ MgmtGameInstruction::SynchLog ],
1031 let gen = if_chain!{
1032 let resp = self.cmd(&cmd)?;
1033 if let MgmtResponse::AlterGame {
1037 if let [MgmtGameResponse::Synch(gen)] = responses[..];
1039 else { throw!(anyhow!("unexpected resp to synch {:?}", resp)) }
1041 trace!("gen={} ...", gen);
1045 fn fakerng_load(&mut self, values: &[&dyn ToString]) -> Result<(),AE> {
1046 let values = values.iter().map(|v| v.to_string()).collect();
1047 self.cmd(&MC::LoadFakeRng(values))?;
1050 fn fakerng_unfake(&mut self) -> Result<(),AE> {
1051 self.cmd(&MC::LoadFakeRng(vec![]))?;
1056 // ==================== core entrypoint, for wdriver too ====================
1059 pub fn setup_core<O>(module_paths: &[&str]) ->
1060 (O, Instance, SetupCore)
1061 where O: StructOpt + AsRef<Opts>
1063 let mut builder = env_logger::Builder::new();
1065 .format_timestamp_micros()
1066 .format_level(true);
1067 for too_verbose in &[
1068 "html5ever::tokenizer",
1069 "html5ever::tree_builder",
1070 "selectors::matching",
1072 "hyper::client::pool",
1074 builder.filter_module(too_verbose, log::LevelFilter::Info);
1077 for module in module_paths {
1079 .filter_module(module, log::LevelFilter::Debug);
1083 .filter_level(log::LevelFilter::Debug)
1084 .parse_env("OTTER_TEST_LOG")
1088 let caller_opts = O::from_args();
1089 let opts = caller_opts.as_ref();
1091 let current_exe: String = env::current_exe()
1092 .context("find current executable")?
1094 .ok_or_else(|| anyhow!("current executable path is not UTF-8 !"))?
1100 &mut |s: &OsStr| s.to_str().unwrap().starts_with("--test=")
1102 .context("reinvoke via bwrap")?;
1105 info!("pid = {}", nix::unistd::getpid());
1106 sleep(opts.pause.into());
1108 let cln = cleanup_notify::Handle::new()?;
1109 let ds = prepare_tmpdir(opts, ¤t_exe)?;
1111 let (mgmt_conn, server_child) =
1112 prepare_gameserver(&cln, &ds).did("setup game server")?;
1115 prepare_game(&ds, &default(), TABLE).context("setup game")?;
1117 let wanted_tests = opts.tests.track();
1125 mgmt_conn: mgmt_conn.into(),
1131 pub struct PortmanteauMember {
1132 pub path: &'static str,
1133 pub f: fn() -> Result<(), Explode>,
1135 inventory::collect!(PortmanteauMember);
1138 macro_rules! portmanteau_has {
1139 ($path:literal, $mod:ident) => {
1140 #[path = $path] mod $mod;
1141 inventory::submit!(PortmanteauMember { path: $path, f: $mod::main });
1146 pub fn portmanteau_main(prefix: &str){
1147 let arg = 'arg: loop {
1148 for (ai, s) in env::args().enumerate() {
1149 let plausible = |s: &str| s.starts_with(&format!("{}-",prefix));
1151 break 'arg if ai == 0 {
1152 let s = s.rsplitn(2,'/').next().unwrap();
1153 if ! plausible(s) { continue }
1156 let s = s.strip_prefix("--test=")
1158 "found non-long-option looking for --test={}-*: {:?}",
1161 panic!("found non --no-bwrap --{}-* option looking for --{}-*",
1167 panic!("ran out of options looking for --test={}-*", prefix);
1170 let f = inventory::iter::<PortmanteauMember>.into_iter()
1172 let n = pm.path.strip_suffix(".rs").unwrap();
1173 if n == arg { Some(pm.f) } else { None }
1175 .expect("unrecognosed {wdt,at}-* portanteau member");