1 // Copyright 2020 Ian Jackson
2 // SPDX-License-Identifier: AGPL-3.0-or-later
3 // There is NO WARRANTY.
5 #![feature(unboxed_closures)]
8 pub use anyhow::{anyhow, ensure, Context};
9 pub use boolinator::Boolinator;
10 pub use fehler::{throw, throws};
11 pub use if_chain::if_chain;
12 pub use log::{debug, error, info, trace, warn};
13 pub use log::{log, log_enabled};
14 pub use nix::unistd::LinkatFlags;
15 pub use num_traits::NumCast;
16 pub use num_derive::FromPrimitive;
17 pub use parking_lot::{Mutex, MutexGuard};
18 pub use regex::{Captures, Regex};
19 pub use serde::{Serialize, Deserialize};
20 pub use structopt::StructOpt;
21 pub use strum::{EnumIter, EnumProperty, IntoEnumIterator, IntoStaticStr};
22 pub use thirtyfour_sync as t4;
25 pub use t4::WebDriverCommands;
29 pub use std::fmt::{self, Debug};
30 pub use std::collections::hash_map::HashMap;
31 pub use std::convert::TryInto;
33 pub use std::io::{self, BufRead, BufReader, ErrorKind, Write};
36 pub use std::net::TcpStream;
37 pub use std::ops::Deref;
38 pub use std::os::unix::process::CommandExt;
39 pub use std::os::unix::fs::DirBuilderExt;
40 pub use std::os::linux::fs::MetadataExt; // todo why linux for st_mode??
42 pub use std::process::{Command, Stdio};
43 pub use std::thread::{self, sleep};
46 pub use otter::ensure_eq;
47 pub use otter::commands::{MgmtCommand, MgmtResponse};
48 pub use otter::commands::{MgmtGameInstruction, MgmtGameResponse};
49 pub use otter::commands::{MgmtGameUpdateMode};
50 pub use otter::gamestate::{self, Generation};
51 pub use otter::global::InstanceName;
52 pub use otter::mgmtchannel::MgmtChannel;
53 pub use otter::spec::{Coord, Pos, PosC};
55 pub type T4d = t4::WebDriver;
56 pub type WDE = t4::error::WebDriverError;
58 pub const MS : time::Duration = time::Duration::from_millis(1);
59 pub type AE = anyhow::Error;
61 pub const URL : &str = "http://localhost:8000";
63 pub fn default<T:Default>() -> T { Default::default() }
66 use otter::config::DAEMON_STARTUP_REPORT;
68 const TABLE : &str = "server::dummy";
69 const CONFIG : &str = "server-config.toml";
71 #[derive(Copy,Clone,Debug,Eq,PartialEq,Ord,PartialOrd)]
72 #[derive(FromPrimitive,EnumIter,IntoStaticStr,EnumProperty)]
73 #[strum(serialize_all = "snake_case")]
75 #[strum(props(Token="kmqAKPwK4TfReFjMor8MJhdRPBcwIBpe"))] Alice,
76 #[strum(props(Token="ccg9kzoTh758QrVE1xMY7BQWB36dNJTx"))] Bob,
79 pub trait AlwaysContext<T,E> {
80 fn always_context(self, msg: &'static str) -> anyhow::Result<T>;
83 impl<T,E> AlwaysContext<T,E> for Result<T,E>
84 where Self: anyhow::Context<T,E>,
86 fn always_context(self, msg: &'static str) -> anyhow::Result<T> {
87 let x = self.context(msg);
88 if x.is_ok() { info!("completed {}.", msg) };
93 pub trait JustWarn<T> {
94 fn just_warn(self) -> Option<T>;
97 impl<T> JustWarn<T> for Result<T,AE> {
98 fn just_warn(self) -> Option<T> {
109 #[derive(Debug,Clone)]
112 #[structopt(long="--no-bwrap")]
115 #[structopt(long="--tmp-dir", default_value="tmp")]
118 #[structopt(long="--pause", default_value="0ms")]
119 pause: humantime::Duration,
121 #[structopt(long="--geckodriver-args", default_value="")]
122 geckodriver_args: String,
126 pub struct FinalInfoCollection;
128 type ScreenShotCount = u32;
129 type WindowState = Option<String>;
134 pub mgmt_conn: MgmtChannel,
136 current_window: WindowState,
137 screenshot_count: ScreenShotCount,
138 final_hook: FinalInfoCollection,
139 windows_squirreled: Vec<String>, // see Drop impl
142 #[derive(Clone,Debug)]
143 pub struct DirSubst {
146 pub start_dir: String,
150 pub struct Instance(InstanceName);
152 #[derive(Clone,Debug)]
153 pub struct Subst(HashMap<String,String>);
155 #[derive(Clone,Debug)]
156 pub struct ExtendedSubst<B: Substitutor, X: Substitutor>(B, X);
161 L: IntoIterator<Item=&'i (T, U)>>
164 fn from(l: L) -> Subst {
165 let map = l.into_iter()
166 .map(|(k,v)| (k.as_ref().to_owned(), v.as_ref().to_owned())).collect();
171 pub trait Substitutor {
172 fn get(&self, kw: &str) -> Option<String>;
174 fn also<L: Into<Subst>>(&self, xl: L) -> ExtendedSubst<Self, Subst>
175 where Self: Clone + Sized {
176 ExtendedSubst(self.clone(), xl.into())
180 fn subst<S: AsRef<str>>(&self, s: S) -> String
183 fn inner(self_: &dyn Substitutor, s: &dyn AsRef<str>) -> String {
185 let re = Regex::new(r"@(\w+)@").expect("bad re!");
186 let mut errs = vec![];
187 let out = re.replace_all(s, |caps: ®ex::Captures| {
188 let kw = caps.get(1).expect("$1 missing!").as_str();
189 if kw == "" { return "".to_owned() }
190 let v = self_.get(kw);
192 errs.push(kw.to_owned());
196 if ! errs.is_empty() {
197 throw!(anyhow!("bad substitution(s) {:?} in {:?}",
206 fn ss(&self, s: &str) -> Vec<String>
211 .filter(|s| !s.is_empty())
217 impl Substitutor for Subst {
218 fn get(&self, kw: &str) -> Option<String> {
219 self.0.get(kw).map(String::clone)
223 impl<B:Substitutor, X:Substitutor> Substitutor for ExtendedSubst<B, X> {
224 fn get(&self, kw: &str) -> Option<String> {
225 self.1.get(kw).or_else(|| self.0.get(kw))
229 impl Substitutor for DirSubst {
230 fn get(&self, kw: &str) -> Option<String> {
232 "url" => URL.to_owned(),
233 "src" => self.src.clone(),
234 "build" => self.start_dir.clone(),
235 "abstmp" => self.abstmp.clone(),
236 "target" => format!("{}/target", &self.start_dir),
237 "specs" => format!("{}/specs" , &self.src ),
245 use fehler::{throw, throws};
247 use nix::errno::Errno::*;
248 use nix::{unistd::*, fcntl::OFlag};
249 use nix::sys::signal::*;
253 use std::os::unix::io::RawFd;
254 use std::panic::catch_unwind;
255 use std::process::Command;
256 type AE = anyhow::Error;
258 pub struct Handle(RawFd);
261 fn mkpipe() -> (RawFd,RawFd) {
262 pipe2(OFlag::O_CLOEXEC).map_err(nix2io)?
266 fn read_await(fd: RawFd) {
268 let mut buf = [0u8; 1];
269 match nix::unistd::read(fd, &mut buf) {
271 Ok(_) => throw!(io::Error::from_raw_os_error(libc::EINVAL)),
272 Err(Sys(EINTR)) => continue,
273 _ => throw!(io::Error::last_os_error()),
278 fn nix2io(_n: nix::Error) -> io::Error {
279 io::Error::last_os_error()
284 pub fn new() -> Self {
285 let (reading_end, _writing_end) = mkpipe()
286 .context("create cleanup notify pipe")?;
287 // we leak the writing end, keeping it open only in this process
292 pub fn arm_hook(&self, cmd: &mut Command) { unsafe {
293 use std::os::unix::process::CommandExt;
295 let notify_writing_end = self.0;
296 let all_signals = nix::sys::signal::SigSet::all();
298 cmd.pre_exec(move || -> Result<(), io::Error> {
299 let semidaemon = nix::unistd::getpid();
300 let (reading_end, writing_end) = mkpipe()?;
302 match fork().map_err(nix2io)? {
303 ForkResult::Child => {
304 let _ = catch_unwind(move || -> Void {
306 SigmaskHow::SIG_BLOCK,
311 let _ = close(writing_end);
312 let _ = nix::unistd::dup2(2, 1);
315 if fd == notify_writing_end { continue }
317 if fd >= writing_end && matches!(r, Err(Sys(EBADF))) {
321 let _ = read_await(notify_writing_end);
322 let _ = kill(semidaemon, SIGTERM);
325 let _ = raise(SIGABRT);
328 ForkResult::Parent{..} => {
330 close(writing_end).map_err(nix2io)?;
331 read_await(reading_end)?;
342 fn reinvoke_via_bwrap(_opts: &Opts, current_exe: &str) -> Void {
343 debug!("running bwrap");
345 let mut bcmd = Command::new("bwrap");
347 .args("--unshare-net \
350 --die-with-parent".split(" "))
353 .args(env::args_os().skip(1));
355 std::io::stdout().flush().context("flush stdout")?;
356 let e : AE = bcmd.exec().into();
357 throw!(e.context("exec bwrap"));
361 fn prepare_tmpdir(opts: &Opts, current_exe: &str) -> DirSubst {
363 fn getcwd() -> String {
367 .ok_or_else(|| anyhow!("path is not UTF-8"))?
371 let start_dir = getcwd()
372 .context("canonicalise our invocation directory (getcwd)")?;
375 match fs::metadata(&opts.tmp_dir) {
378 throw!(anyhow!("existing object is not a directory"));
380 if (m.st_mode() & 0o01002) != 0 {
382 "existing directory mode {:#o} is sticky or world-writeable. \
383 We use predictable pathnames so that would be a tmp race",
388 Err(e) if e.kind() == ErrorKind::NotFound => {
389 fs::create_dir(&opts.tmp_dir)
393 let e : AE = e.into();
394 throw!(e.context("stat existing directory"))
398 env::set_current_dir(&opts.tmp_dir)
399 .context("chdir into it")?;
403 .with_context(|| opts.tmp_dir.to_owned())
404 .context("prepare/create tmp-dir")?;
406 let leaf = current_exe.rsplitn(2, '/').next().unwrap();
407 let our_tmpdir = format!("{}/{}", &opts.tmp_dir, &leaf);
409 match fs::remove_dir_all(&leaf) {
411 Err(e) if e.kind() == ErrorKind::NotFound => {},
412 Err(e) => throw!(AE::from(e).context("remove previous directory")),
415 fs::DirBuilder::new().create(&leaf)
416 .context("create fresh subdirectory")?;
418 env::set_current_dir(&leaf)
419 .context("chdir into it")?;
423 .with_context(|| our_tmpdir.to_owned())
424 .context("prepare/create our tmp subdir")?;
427 getcwd().context("canonicalise our tmp subdir (getcwd)")?;
429 env::set_var("HOME", &abstmp);
430 env::set_var("TMPDIR", &abstmp);
431 for v in "http_proxy https_proxy XAUTHORITY CDPATH \
432 SSH_AGENT_PID SSH_AUTH_SOCK WINDOWID WWW_HOME".split(' ')
437 let manifest_var = "CARGO_MANIFEST_DIR";
438 let src : String = (|| Ok::<_,AE>(match env::var(manifest_var) {
439 Ok(dir) => dir.into(),
440 Err(env::VarError::NotPresent) => start_dir.clone(),
441 e@ Err(_) => throw!(e.context(manifest_var).err().unwrap()),
443 .context("find source code")?;
454 fn fork_something_which_prints(mut cmd: Command,
455 cln: &cleanup_notify::Handle,
460 cmd.stdout(Stdio::piped());
461 cln.arm_hook(&mut cmd)?;
462 let mut child = cmd.spawn().context("spawn")?;
463 let mut report = BufReader::new(child.stdout.take().unwrap())
466 let l = report.next();
468 let s = child.try_wait().context("check on spawned child")?;
470 throw!(anyhow!("failed to start: wait status = {}", &e));
475 None => throw!(anyhow!("EOF (but it's still running?")),
476 Some(Err(e)) => throw!(AE::from(e).context("failed to read")),
479 let what = what.to_owned();
480 thread::spawn(move|| (||{
482 let l : Result<String, io::Error> = l;
483 let l = l.context("reading further output")?;
484 const MAXLEN : usize = 300;
485 if l.len() <= MAXLEN {
486 println!("{} {}", what, l);
488 println!("{} {}...", what, &l[..MAXLEN-3]);
492 })().context(what).just_warn()
496 })().with_context(|| what.to_owned())?
500 fn prepare_xserver(cln: &cleanup_notify::Handle, ds: &DirSubst) {
501 const DISPLAY : u16 = 12;
503 let mut xcmd = Command::new("Xvfb");
505 .args("-nolisten unix \
511 -displayfd 1".split(' '))
512 .args(&["-fbdir", &ds.abstmp])
513 .arg(format!(":{}", DISPLAY));
515 let l = fork_something_which_prints(xcmd, cln, "Xvfb")?;
517 if l != DISPLAY.to_string() {
519 "Xfvb said {:?}, expected {:?}",
524 let display = format!("[::1]:{}", DISPLAY);
525 env::set_var("DISPLAY", &display);
527 // Doesn't do IPv6 ??
528 let v4display = format!("127.0.0.1:{}", DISPLAY);
529 let (xconn, _) = x11rb::connect(Some(&v4display))
530 .context("make keepalive connection to X server")?;
532 // Sadly, if we die between spawning Xfvb, and here, we will
533 // permanently leak the whole Xfvb process (and the network
534 // namespace). There doesn't seem to a way to avoid this without
537 Box::leak(Box::new(xconn));
541 fn prepare_gameserver(cln: &cleanup_notify::Handle, ds: &DirSubst)
543 let subst = ds.also(&[
544 ("command_socket", "command.socket"),
546 let config = subst.subst(r##"
547 change_directory = "@abstmp@"
552 command_socket = "@command_socket@"
553 template_dir = "@src@/templates"
554 nwtemplate_dir = "@src@/nwtemplates"
555 bundled_sources = "@target@/bundled-sources"
556 wasm_dir = "@target@/packed-wasm"
557 shapelibs = [ "@src@/library/*.toml" ]
559 debug_js_inject_file = "@src@/templates/log-save.js"
560 check_bundled_sources = false # For testing only! see LICENCE!
563 global_level = 'debug'
568 # ^ comment these two out to see Tera errors, *sigh*
570 'hyper::server' = 'info'
571 "game::debugreader" = 'info'
572 "game::updates" = 'trace'
575 fs::write(CONFIG, &config)
576 .context(CONFIG).context("create server config")?;
578 let server_exe = ds.subst("@target@/debug/daemon-otter")?;
579 let mut cmd = Command::new(&server_exe);
581 .arg("--report-startup")
585 let l = fork_something_which_prints(cmd, cln, &server_exe)?;
586 if l != DAEMON_STARTUP_REPORT {
587 throw!(anyhow!("otter-daemon startup report {:?}, expected {:?}",
588 &l, DAEMON_STARTUP_REPORT));
592 .context("game server")?;
594 let mut mgmt_conn = MgmtChannel::connect(
595 &subst.subst("@command_socket@")?
598 mgmt_conn.cmd(&MgmtCommand::SetSuperuser(true))?;
599 mgmt_conn.cmd(&MgmtCommand::SelectAccount("server:".parse()?))?;
606 pub fn otter<S:AsRef<str>>(&self, xargs: &[S]) {
608 let exe = ds.subst("@target@/debug/otter")?;
609 let mut args : Vec<&str> = vec![];
610 args.extend(&["--config", CONFIG]);
611 args.extend(xargs.iter().map(AsRef::as_ref));
612 let dbg = format!("running {} {:?}", &exe, &args);
615 let mut cmd = Command::new(&exe);
618 .spawn().context("spawn")?
619 .wait().context("wait")?;
621 throw!(anyhow!("wait status {}", &st));
626 .context("run otter client")?;
631 pub fn prepare_game(ds: &DirSubst, table: &str) -> InstanceName {
632 let subst = ds.also(&[("table", &table)]);
636 --reset-table @specs@/test.table.toml \
637 @table@ @specs@/demo.game.toml \
638 ")?).context("reset table")?;
640 let instance : InstanceName = table.parse()
641 .with_context(|| table.to_owned())
642 .context("parse table name")?;
648 fn prepare_geckodriver(opts: &Opts, cln: &cleanup_notify::Handle) {
649 const EXPECTED : &str = "Listening on 127.0.0.1:4444";
650 let mut cmd = Command::new("geckodriver");
651 if opts.geckodriver_args != "" {
652 cmd.args(opts.geckodriver_args.split(' '));
654 let l = fork_something_which_prints(cmd, cln, "geckodriver")?;
655 let fields : Vec<_> = l.split('\t').skip(2).take(2).collect();
656 let expected = ["INFO", EXPECTED];
657 if fields != expected {
658 throw!(anyhow!("geckodriver did not report as expected \
659 - got {:?}, expected {:?}",
665 fn prepare_thirtyfour() -> (T4d, ScreenShotCount, Vec<String>) {
667 let mut caps = t4::DesiredCapabilities::firefox();
668 let prefs : HashMap<_,_> = [
669 ("devtools.console.stdout.content", true),
670 ].iter().cloned().collect();
671 caps.add("prefs", prefs)?;
672 caps.add("stdio", "inherit")?;
673 let mut driver = t4::WebDriver::new("http://localhost:4444", &caps)
674 .context("create 34 WebDriver")?;
676 const FRONT : &str = "front";
677 let window_names = vec![FRONT.into()];
678 driver.set_window_name(FRONT).context("set initial window name")?;
679 screenshot(&mut driver, &mut count, "startup")?;
680 driver.get(URL).context("navigate to front page")?;
681 screenshot(&mut driver, &mut count, "front")?;
683 fetch_log(&driver, "front")?;
685 let t = Some(5_000 * MS);
686 driver.set_timeouts(t4::TimeoutConfiguration::new(t,t,t))
687 .context("set webdriver timeouts")?;
689 (driver, count, window_names)
692 /// current window must be `name`
694 fn fetch_log(driver: &T4d, name: &str) {
696 let got = driver.execute_script(r#"
697 var returning = window.console.saved;
698 window.console.saved = [];
700 "#).context("get log")?;
702 for ent in got.value().as_array()
703 .ok_or(anyhow!("saved isn't an array?"))?
705 #[derive(Deserialize)]
706 struct LogEnt(String, Vec<serde_json::Value>);
707 impl fmt::Display for LogEnt {
708 #[throws(fmt::Error)]
709 fn fmt(&self, f: &mut fmt::Formatter) {
710 write!(f, "{}:", self.0)?;
711 for a in &self.1 { write!(f, " {}", a)?; }
715 let ent: LogEnt = serde_json::from_value(ent.clone())
716 .context("parse log entry")?;
718 debug!("JS {} {}", name, &ent);
722 .with_context(|| name.to_owned())
723 .context("fetch JS log messages")?;
729 instance: InstanceName,
733 pub fn table(&self) -> String { self.instance.to_string() }
736 type ScreenCTM = ndarray::Array2::<f64>;
738 pub struct WindowGuard<'g> {
741 matrix: once_cell::sync::OnceCell<ScreenCTM>,
744 impl Debug for WindowGuard<'_> {
745 #[throws(fmt::Error)]
746 fn fmt(&self, f: &mut fmt::Formatter) {
747 f.debug_struct("WindowGuard")
748 .field("w.name", &self.w.name)
749 .field("w.instance", &self.w.instance.to_string())
754 impl<'g> WindowGuard<'g> {
756 pub fn find_piece(&'g self, pieceid: &'g str) -> PieceElement<'g> {
757 let id = format!("use{}", pieceid);
758 let elem = self.su.driver.find_element(By::Id(&id))?;
766 pub type WebCoord = i32;
767 pub type WebPos = (WebCoord, WebCoord);
769 pub struct PieceElement<'g> {
771 w: &'g WindowGuard<'g>,
772 elem: t4::WebElement<'g>,
775 impl<'g> Deref for PieceElement<'g> {
776 type Target = t4::WebElement<'g>;
777 fn deref<'i>(&'i self) -> &'i t4::WebElement<'g> { &self.elem }
780 impl<'g> PieceElement<'g> {
782 pub fn posg(&self) -> Pos {
784 let a = |a| Ok::<_,AE>(
785 self.get_attribute(a)?.ok_or(anyhow!("{}", a))?.parse()?
789 Ok::<_,AE>(PosC([x,y]))
791 .with_context(|| self.pieceid.to_owned())
792 .context("read position of piece out of x,y attributes")?
796 pub fn posw(&self) -> WebPos {
797 let posg = self.posg()?;
799 let mat = self.w.matrix.get_or_try_init(||{
800 let ary = self.w.su.driver.execute_script(r#"
801 let m = space.getScreenCTM();
802 return [m.a, m.b, m.c, m.d, m.e, m.f];
804 let ary = ary.value();
808 let ary = ary.as_array().ok_or_else(|| anyhow!("not array"))?;
809 let mut mat : ScreenCTM = ndarray::Array2::zeros((3,3));
810 for got in itertools::Itertools::zip_longest(
811 [11, 12, 21, 22, 41, 42].iter(),
812 // ^ from https://developer.mozilla.org/en-US/docs/Web/API/DOMMatrix
815 let (mij, v) = got.both().ok_or_else(|| anyhow!("wrong length"))?;
816 let adj = |v| (if v == 4 { 3 } else { v }) - 1;
817 let i = adj(mij / 10);
818 let j = adj(mij % 10);
819 mat[(j,i)] = v.as_f64().ok_or_else(|| anyhow!("entry not f64"))?;
823 .with_context(|| format!("getScreenCGM script gave {:?}", &ary))?;
829 let vec : ndarray::Array1<f64> =
833 .chain(iter::once(1.))
837 let vec = mat.dot(&vec);
838 let mut coords = vec.iter().map(
839 |v| NumCast::from(v.round()).ok_or_else(
840 || anyhow!("coordinate {} out of range in {:?}", v, &vec))
842 let mut coord = || coords.next().unwrap();
848 .with_context(|| self.pieceid.to_owned())
849 .context("find piece position")?
854 fn check_window_name_sanity(name: &str) -> &str {
855 let e = || anyhow!("bad window name {:?}", &name);
857 name.chars().nth(0).ok_or(e())?
858 .is_ascii_alphanumeric().ok_or(e())?;
861 |c| c.is_ascii_alphanumeric() || c == '-' || c == '_'
869 pub fn new_window<'s>(&'s mut self, instance: &Instance, name: &str)
871 let name = check_window_name_sanity(name)?;
874 self.current_window = None; // we might change the current window
876 match self.driver.switch_to().window_name(name) {
877 Ok(()) => throw!(anyhow!("window already exists")),
878 Err(WDE::NoSuchWindow(_)) |
879 Err(WDE::NotFound(..)) => (),
881 eprintln!("wot {:?}", &e);
883 .context("check for pre-existing window")
888 self.driver.execute_script(&format!(
889 r#"window.open('', target='{}');"#,
892 .context("execute script to create window")?;
895 name: name.to_owned(),
896 instance: instance.0.clone(),
899 .with_context(|| name.to_owned())
900 .context("create window")?;
902 self.windows_squirreled.push(name.to_owned());
909 pub fn w<'s>(&'s mut self, w: &'s Window) -> WindowGuard<'s> {
910 if self.current_window.as_ref() != Some(&w.name) {
911 self.driver.switch_to().window_name(&w.name)
912 .with_context(|| w.name.to_owned())
913 .context("switch to window")?;
914 self.current_window = Some(w.name.clone());
924 impl<'g> Deref for WindowGuard<'g> {
926 fn deref(&self) -> &T4d { &self.su.driver }
929 impl<'g> Drop for WindowGuard<'g> {
931 fetch_log(&self.su.driver, &self.w.name)
936 pub trait Screenshottable {
937 fn screenshot(&mut self, slug: &str) -> Result<(),AE>;
940 impl<'g> Screenshottable for WindowGuard<'g> {
942 fn screenshot(&mut self, slug: &str) {
943 screenshot(&self.su.driver, &mut self.su.screenshot_count,
944 &format!("{}-{}", &self.w.name, slug))?
949 fn screenshot(driver: &T4d, count: &mut ScreenShotCount, slug: &str) {
950 let path = format!("{:03}{}.png", count, slug);
952 driver.screenshot(&path::PathBuf::from(&path))
953 .with_context(|| path.clone())
954 .context("take screenshot")?;
955 debug!("screenshot {}", &path);
958 impl<'g> WindowGuard<'g> {
960 pub fn synch(&mut self) {
961 let cmd = MgmtCommand::AlterGame {
962 game: self.w.instance.clone(),
963 how: MgmtGameUpdateMode::Online,
964 insns: vec![ MgmtGameInstruction::Synch ],
967 let resp = self.su.mgmt_conn.cmd(&cmd)?;
968 if let MgmtResponse::AlterGame {
972 if let [MgmtGameResponse::Synch(gen)] = responses[..];
974 else { throw!(anyhow!("unexpected resp to synch {:?}", resp)) }
976 trace!("{:?} gen={} ...", self, gen);
979 let tgen = self.su.driver.execute_async_script(
981 ("wanted", &gen.to_string())
983 var done = arguments[0];
984 if (gen >= @wanted@) { done(gen); return; }
985 window.gen_update_hook = function() {
986 window.gen_update_hook = function() { };
991 .context("run async script")?
992 .value().as_u64().ok_or(anyhow!("script return is not u64"))?;
993 let tgen = Generation(tgen);
994 trace!("{:?} gen={} tgen={}", self, gen, tgen);
995 if tgen >= gen { break; }
999 .context("await gen update via async js script")?;
1003 impl Drop for FinalInfoCollection {
1004 fn drop(&mut self) {
1005 nix::unistd::linkat(None, "Xvfb_screen0",
1006 None, "Xvfb_keep.xwd",
1007 LinkatFlags::NoSymlinkFollow)
1008 .context("preserve Xvfb screen")
1013 impl Drop for Setup {
1014 fn drop(&mut self) {
1016 for name in mem::take(&mut self.windows_squirreled) {
1017 // This constructor is concurrency-hazardous. It's only OK
1018 // here because we have &mut self. If there is only one
1019 // Setup, there can be noone else with a Window with an
1020 // identical name to be interfered with by us.
1023 instance: TABLE.parse().context(TABLE)?,
1025 self.w(&w)?.screenshot("final")
1027 .context("final screenshot")
1032 .context("screenshots, in Setup::drop")
1038 pub fn setup(exe_module_path: &str) -> (Setup, Instance) {
1039 env_logger::Builder::new()
1040 .format_timestamp_micros()
1042 .filter_module("otter_webdriver_tests", log::LevelFilter::Debug)
1043 .filter_module(exe_module_path, log::LevelFilter::Debug)
1044 .filter_level(log::LevelFilter::Info)
1045 .parse_env("OTTER_WDT_LOG")
1049 let current_exe : String = env::current_exe()
1050 .context("find current executable")?
1052 .ok_or_else(|| anyhow!("current executable path is not UTF-8 !"))?
1055 let opts = Opts::from_args();
1057 reinvoke_via_bwrap(&opts, ¤t_exe)
1058 .context("reinvoke via bwrap")?;
1061 info!("pid = {}", nix::unistd::getpid());
1062 sleep(opts.pause.into());
1064 let cln = cleanup_notify::Handle::new()?;
1065 let ds = prepare_tmpdir(&opts, ¤t_exe)?;
1067 prepare_xserver(&cln, &ds).always_context("setup X server")?;
1070 prepare_gameserver(&cln, &ds).always_context("setup game server")?;
1073 prepare_game(&ds, TABLE).context("setup game")?;
1075 let final_hook = FinalInfoCollection;
1077 prepare_geckodriver(&opts, &cln).always_context("setup webdriver server")?;
1078 let (driver, screenshot_count, windows_squirreled) =
1079 prepare_thirtyfour().always_context("prepare web session")?;
1086 current_window: None,
1097 pub fn setup_static_users(&mut self, instance: &Instance) -> Vec<Window> {
1099 fn mk(su: &mut Setup, instance: &Instance, u: StaticUser) -> Window {
1100 let nick: &str = u.into();
1101 let token = u.get_str("Token").expect("StaticUser missing Token");
1102 let subst = su.ds.also([("nick", nick),
1103 ("token", token)].iter());
1106 --account server:@nick@ \
1107 --fixed-token @token@ \
1108 join-game server::dummy")?)?;
1109 let w = su.new_window(instance, nick)?;
1110 let url = subst.subst("@url@/?@token@")?;
1111 su.w(&w)?.get(url)?;
1112 su.w(&w)?.screenshot("initial")?;
1115 StaticUser::iter().map(
1116 |u| mk(self, instance, u)
1117 .with_context(|| format!("{:?}", u))
1118 .context("make static user")
1120 .collect::<Result<Vec<Window>,AE>>()?