chiark / gitweb /
wip await gen
[otter.git] / wdriver.rs
1 // Copyright 2020 Ian Jackson
2 // SPDX-License-Identifier: AGPL-3.0-or-later
3 // There is NO WARRANTY.
4
5 #![feature(unboxed_closures)]
6 #![feature(fn_traits)]
7
8 pub use anyhow::{anyhow, 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_derive::FromPrimitive;
16 pub use parking_lot::{Mutex, MutexGuard};
17 pub use regex::{Captures, Regex};
18 pub use serde::{Serialize, Deserialize};
19 pub use structopt::StructOpt;
20 pub use strum::{EnumIter, EnumProperty, IntoEnumIterator, IntoStaticStr};
21 pub use thirtyfour_sync as t4;
22 pub use void::Void;
23
24 pub use t4::WebDriverCommands;
25
26 pub use std::env;
27 pub use std::fmt::{self, Debug};
28 pub use std::fs;
29 pub use std::collections::hash_map::HashMap;
30 pub use std::convert::TryInto;
31 pub use std::io::{self, BufRead, BufReader, ErrorKind, Write};
32 pub use std::mem;
33 pub use std::net::TcpStream;
34 pub use std::ops::Deref;
35 pub use std::os::unix::process::CommandExt;
36 pub use std::os::unix::fs::DirBuilderExt;
37 pub use std::os::linux::fs::MetadataExt; // todo why linux for st_mode??
38 pub use std::path;
39 pub use std::process::{Command, Stdio};
40 pub use std::thread::{self, sleep};
41 pub use std::time;
42
43 pub use otter::commands::{MgmtCommand, MgmtResponse};
44 pub use otter::commands::{MgmtGameInstruction, MgmtGameResponse};
45 pub use otter::commands::{MgmtGameUpdateMode};
46 pub use otter::gamestate::Generation;
47 pub use otter::global::InstanceName;
48 pub use otter::mgmtchannel::MgmtChannel;
49
50 pub type T4d = t4::WebDriver;
51 pub type WDE = t4::error::WebDriverError;
52
53 pub const MS : time::Duration = time::Duration::from_millis(1);
54 pub type AE = anyhow::Error;
55
56 pub const URL : &str = "http://localhost:8000";
57
58 use t4::Capabilities;
59 use otter::config::DAEMON_STARTUP_REPORT;
60
61 const TABLE : &str = "server::dummy";
62 const CONFIG : &str = "server-config.toml";
63
64 #[derive(Copy,Clone,Debug,Eq,PartialEq,Ord,PartialOrd)]
65 #[derive(FromPrimitive,EnumIter,IntoStaticStr,EnumProperty)]
66 #[strum(serialize_all = "snake_case")]
67 pub enum StaticUser {
68   #[strum(props(Token="kmqAKPwK4TfReFjMor8MJhdRPBcwIBpe"))] Alice,
69   #[strum(props(Token="ccg9kzoTh758QrVE1xMY7BQWB36dNJTx"))] Bob,
70 }
71
72 pub trait AlwaysContext<T,E> {
73   fn always_context(self, msg: &'static str) -> anyhow::Result<T>;
74 }
75
76 impl<T,E> AlwaysContext<T,E> for Result<T,E>
77 where Self: anyhow::Context<T,E>,
78 {
79   fn always_context(self, msg: &'static str) -> anyhow::Result<T> {
80     let x = self.context(msg);
81     if x.is_ok() { info!("completed {}.", msg) };
82     x
83   }
84 }
85
86 pub trait JustWarn<T> {
87   fn just_warn(self) -> Option<T>;
88 }
89
90 impl<T> JustWarn<T> for Result<T,AE> {
91   fn just_warn(self) -> Option<T> {
92     match self {
93       Ok(x) => Some(x),
94       Err(e) => {
95         warn!("{:#}", e);
96         None
97       },
98     }
99   }
100 }
101
102 #[derive(Debug,Clone)]
103 #[derive(StructOpt)]
104 struct Opts {
105   #[structopt(long="--no-bwrap")]
106   no_bwrap: bool,
107
108   #[structopt(long="--tmp-dir", default_value="tmp")]
109   tmp_dir: String,
110
111   #[structopt(long="--pause", default_value="0ms")]
112   pause: humantime::Duration,
113
114   #[structopt(long="--geckodriver-args", default_value="")]
115   geckodriver_args: String,
116 }
117
118 #[derive(Debug)]
119 pub struct FinalInfoCollection;
120
121 type ScreenShotCount = u32;
122 type WindowState = Option<String>;
123
124 #[derive(Debug)]
125 pub struct Setup {
126   pub ds: DirSubst,
127   pub mgmt_conn: MgmtChannel,
128   driver: T4d,
129   current_window: WindowState,
130   screenshot_count: ScreenShotCount,
131   final_hook: FinalInfoCollection,
132   windows_squirreled: Vec<String>, // see Drop impl
133 }
134
135 #[derive(Clone,Debug)]
136 pub struct DirSubst {
137   pub tmp: String,
138   pub abstmp: String,
139   pub start_dir: String,
140   pub src: String,
141 }
142
143 pub struct Instance(InstanceName);
144
145 #[derive(Clone,Debug)]
146 pub struct Subst(HashMap<String,String>);
147
148 #[derive(Clone,Debug)]
149 pub struct ExtendedSubst<B: Substitutor, X: Substitutor>(B, X);
150
151 impl<'i,
152      T: AsRef<str> + 'i,
153      U: AsRef<str> + 'i,
154      L: IntoIterator<Item=&'i (T, U)>>
155   From<L> for Subst
156 {
157   fn from(l: L) -> Subst {
158     let map = l.into_iter()
159       .map(|(k,v)| (k.as_ref().to_owned(), v.as_ref().to_owned())).collect();
160     Subst(map)
161   }
162 }
163
164 pub trait Substitutor {
165   fn get(&self, kw: &str) -> Option<String>;
166
167   fn also<L: Into<Subst>>(&self, xl: L) -> ExtendedSubst<Self, Subst>
168   where Self: Clone + Sized {
169     ExtendedSubst(self.clone(), xl.into())
170   }
171
172   #[throws(AE)]
173   fn subst<S: AsRef<str>>(&self, s: S) -> String 
174   where Self: Sized {
175     #[throws(AE)]
176     fn inner(self_: &dyn Substitutor, s: &dyn AsRef<str>) -> String {
177       let s = s.as_ref();
178       let re = Regex::new(r"@(\w+)@").expect("bad re!");
179       let mut errs = vec![];
180       let out = re.replace_all(s, |caps: &regex::Captures| {
181         let kw = caps.get(1).expect("$1 missing!").as_str();
182         if kw == "" { return "".to_owned() }
183         let v = self_.get(kw);
184         v.unwrap_or_else(||{
185           errs.push(kw.to_owned());
186           "".to_owned()
187         })
188       });
189       if ! errs.is_empty() {
190         throw!(anyhow!("bad substitution(s) {:?} in {:?}",
191                        &errs, s));
192       }
193       out.into()
194     }
195     inner(self, &s)?
196   }
197
198   #[throws(AE)]
199   fn ss(&self, s: &str) -> Vec<String> 
200   where Self: Sized {
201     self.subst(s)?
202       .trim()
203       .split(' ')
204       .filter(|s| !s.is_empty())
205       .map(str::to_string)
206       .collect()
207   }
208 }
209
210 impl Substitutor for Subst {
211   fn get(&self, kw: &str) -> Option<String> {
212     self.0.get(kw).map(String::clone)
213   }
214 }
215
216 impl<B:Substitutor, X:Substitutor> Substitutor for ExtendedSubst<B, X> {
217   fn get(&self, kw: &str) -> Option<String> {
218     self.1.get(kw).or_else(|| self.0.get(kw))
219   }
220 }
221
222 impl Substitutor for DirSubst {
223   fn get(&self, kw: &str) -> Option<String> {
224     Some(match kw {
225       "url"    => URL.to_owned(),
226       "src"    => self.src.clone(),
227       "build"  => self.start_dir.clone(),
228       "target" => format!("{}/target", &self.start_dir),
229       "specs"  => format!("{}/specs" , &self.src      ),
230       _ => return None,
231     })
232   }
233 }
234
235 mod cleanup_notify {
236   use anyhow::Context;
237   use fehler::{throw, throws};
238   use libc::_exit;
239   use nix::errno::Errno::*;
240   use nix::{unistd::*, fcntl::OFlag};
241   use nix::sys::signal::*;
242   use nix::Error::Sys;
243   use void::Void;
244   use std::io;
245   use std::os::unix::io::RawFd;
246   use std::panic::catch_unwind;
247   use std::process::Command;
248   type AE = anyhow::Error;
249
250   pub struct Handle(RawFd);
251
252   #[throws(io::Error)]
253   fn mkpipe() -> (RawFd,RawFd) {
254     pipe2(OFlag::O_CLOEXEC).map_err(nix2io)?
255   }
256
257   #[throws(io::Error)]
258   fn read_await(fd: RawFd) {
259     loop {
260       let mut buf = [0u8; 1];
261       match nix::unistd::read(fd, &mut buf) {
262         Ok(0) => break,
263         Ok(_) => throw!(io::Error::from_raw_os_error(libc::EINVAL)),
264         Err(Sys(EINTR)) => continue,
265         _ => throw!(io::Error::last_os_error()),
266       }
267     }
268   }
269
270   fn nix2io(_n: nix::Error) -> io::Error {
271     io::Error::last_os_error()
272   }
273
274   impl Handle {
275     #[throws(AE)]
276     pub fn new() -> Self {
277       let (reading_end, _writing_end) = mkpipe()
278         .context("create cleanup notify pipe")?;
279       // we leak the writing end, keeping it open only in this process
280       Handle(reading_end)
281     }
282
283     #[throws(AE)]
284     pub fn arm_hook(&self, cmd: &mut Command) { unsafe {
285       use std::os::unix::process::CommandExt;
286
287       let notify_writing_end = self.0;
288       let all_signals = nix::sys::signal::SigSet::all();
289
290       cmd.pre_exec(move || -> Result<(), io::Error> {
291         let semidaemon = nix::unistd::getpid();
292         let (reading_end, writing_end) = mkpipe()?;
293
294         match fork().map_err(nix2io)? {
295           ForkResult::Child => {
296             let _ = catch_unwind(move || -> Void {
297               let _ = sigprocmask(
298                 SigmaskHow::SIG_BLOCK,
299                 Some(&all_signals),
300                 None
301               );
302
303               let _ = close(writing_end);
304               for fd in 2.. {
305                 if fd == notify_writing_end { continue }
306                 let r = close(fd);
307                 if fd >= writing_end && matches!(r, Err(Sys(EBADF))) {
308                   break;
309                 }                  
310               }
311               let _ = read_await(notify_writing_end);
312               let _ = kill(semidaemon, SIGTERM);
313               _exit(0);
314             });
315             let _ = raise(SIGABRT);
316             _exit(127);
317           },
318           ForkResult::Parent{..} => {
319             // parent
320             close(writing_end).map_err(nix2io)?;
321             read_await(reading_end)?;
322           },
323         };
324
325         Ok(())
326       });
327     } }
328   }
329 }
330
331 #[throws(AE)]
332 fn reinvoke_via_bwrap(_opts: &Opts, current_exe: &str) -> Void {
333   debug!("running bwrap");
334   
335   let mut bcmd = Command::new("bwrap");
336   bcmd
337     .args("--unshare-net \
338            --dev-bind / / \
339            --tmpfs /tmp \
340            --die-with-parent".split(" "))
341     .arg(current_exe)
342     .arg("--no-bwrap")
343     .args(env::args_os().skip(1));
344
345   std::io::stdout().flush().context("flush stdout")?;
346   let e : AE = bcmd.exec().into();
347   throw!(e.context("exec bwrap"));
348 }
349
350 #[throws(AE)]
351 fn prepare_tmpdir(opts: &Opts, current_exe: &str) -> DirSubst {
352   #[throws(AE)]
353   fn getcwd() -> String {
354     env::current_dir()
355       .context("getcwd")?
356       .to_str()
357       .ok_or_else(|| anyhow!("path is not UTF-8"))?
358       .to_owned()
359   }
360
361   let start_dir = getcwd()
362     .context("canonicalise our invocation directory (getcwd)")?;
363
364   (||{
365     match fs::metadata(&opts.tmp_dir) {
366       Ok(m) => {
367         if !m.is_dir() {
368           throw!(anyhow!("existing object is not a directory"));
369         }
370         if (m.st_mode() & 0o01002) != 0 {
371           throw!(anyhow!(
372             "existing directory mode {:#o} is sticky or world-writeable. \
373              We use predictable pathnames so that would be a tmp race",
374             m.st_mode()
375           ));
376         }
377       }
378       Err(e) if e.kind() == ErrorKind::NotFound => {
379         fs::create_dir(&opts.tmp_dir)
380           .context("create")?;
381       }
382       Err(e) => {
383         let e : AE = e.into();
384         throw!(e.context("stat existing directory"))
385       }
386     }
387
388     env::set_current_dir(&opts.tmp_dir)
389       .context("chdir into it")?;
390
391     Ok::<_,AE>(())
392   })()
393     .with_context(|| opts.tmp_dir.to_owned())
394     .context("prepare/create tmp-dir")?;
395
396   let leaf = current_exe.rsplitn(2, '/').next().unwrap();
397   let our_tmpdir = format!("{}/{}", &opts.tmp_dir, &leaf);
398   (||{
399     match fs::remove_dir_all(&leaf) {
400       Ok(()) => {},
401       Err(e) if e.kind() == ErrorKind::NotFound => {},
402       Err(e) => throw!(AE::from(e).context("remove previous directory")),
403     };
404
405     fs::DirBuilder::new().create(&leaf)
406       .context("create fresh subdirectory")?;
407
408     env::set_current_dir(&leaf)
409       .context("chdir into it")?;
410
411     Ok::<_,AE>(())
412   })()
413     .with_context(|| our_tmpdir.to_owned())
414     .context("prepare/create our tmp subdir")?;
415
416   let abstmp =
417     getcwd().context("canonicalise our tmp subdir (getcwd)")?;
418
419   env::set_var("HOME", &abstmp);
420   env::set_var("TMPDIR", &abstmp);
421   for v in "http_proxy https_proxy XAUTHORITY CDPATH \
422             SSH_AGENT_PID SSH_AUTH_SOCK WINDOWID WWW_HOME".split(' ')
423   {
424     env::remove_var(v);
425   }
426
427   let manifest_var = "CARGO_MANIFEST_DIR";
428   let src : String = (|| Ok::<_,AE>(match env::var(manifest_var) {
429     Ok(dir) => dir.into(),
430     Err(env::VarError::NotPresent) => start_dir.clone(),
431     e@ Err(_) => throw!(e.context(manifest_var).err().unwrap()),
432   }))()
433     .context("find source code")?;
434
435   DirSubst {
436     tmp: our_tmpdir,
437     abstmp,
438     src,
439     start_dir,
440   }
441 }
442
443 #[throws(AE)]
444 fn fork_something_which_prints(mut cmd: Command,
445                                cln: &cleanup_notify::Handle,
446                                what: &str)
447                                -> String
448 {
449   (||{
450     cmd.stdout(Stdio::piped());
451     cln.arm_hook(&mut cmd)?;
452     let mut child = cmd.spawn().context("spawn")?;
453     let mut report = BufReader::new(child.stdout.take().unwrap())
454       .lines().fuse();
455
456     let l = report.next();
457
458     let s = child.try_wait().context("check on spawned child")?;
459     if let Some(e) = s {
460       throw!(anyhow!("failed to start: wait status = {}", &e));
461     }
462
463     let l = match l {
464       Some(Ok(l)) => l,
465       None => throw!(anyhow!("EOF (but it's still running?")),
466       Some(Err(e)) => throw!(AE::from(e).context("failed to read")),
467     };
468
469     let what = what.to_owned();
470     thread::spawn(move|| (||{
471       for l in report {
472         let l : Result<String, io::Error> = l;
473         let l = l.context("reading further output")?;
474         const MAXLEN : usize = 300;
475         if l.len() <= MAXLEN {
476           println!("{} {}", what, l);
477         } else {
478           println!("{} {}...", what, &l[..MAXLEN-3]);
479         }
480       }
481       Ok::<_,AE>(())
482     })().context(what).just_warn()
483     );
484
485     Ok::<_,AE>(l)
486   })().with_context(|| what.to_owned())?
487 }
488
489 #[throws(AE)]
490 fn prepare_xserver(cln: &cleanup_notify::Handle, ds: &DirSubst) {
491   const DISPLAY : u16 = 12;
492
493   let mut xcmd = Command::new("Xvfb");
494   xcmd
495     .args("-nolisten unix \
496            -nolisten local \
497            -listen inet \
498            -listen inet6 \
499            -terminate \
500            -retro \
501            -displayfd 1".split(' '))
502     .args(&["-fbdir", &ds.abstmp])
503     .arg(format!(":{}", DISPLAY));
504
505   let l = fork_something_which_prints(xcmd, cln, "Xvfb")?;
506
507   if l != DISPLAY.to_string() {
508     throw!(anyhow!(
509       "Xfvb said {:?}, expected {:?}",
510       l, DISPLAY
511     ));
512   }
513
514   let display = format!("[::1]:{}", DISPLAY);
515   env::set_var("DISPLAY", &display);
516
517   // Doesn't do IPv6 ??
518   let v4display = format!("127.0.0.1:{}", DISPLAY);
519   let (xconn, _) = x11rb::connect(Some(&v4display))
520     .context("make keepalive connection to X server")?;
521
522   // Sadly, if we die between spawning Xfvb, and here, we will
523   // permanently leak the whole Xfvb process (and the network
524   // namespace).  There doesn't seem to a way to avoid this without
525   // editing Xvfb,
526
527   Box::leak(Box::new(xconn));
528 }
529
530 #[throws(AE)]
531 fn prepare_gameserver(cln: &cleanup_notify::Handle, ds: &DirSubst)
532                       -> MgmtChannel {
533   let subst = ds.also(&[
534     ("command_socket", "command.socket"),
535   ]);
536   let config = subst.subst(r##"
537 base_dir = "@build@"
538 public_url = "@url@"
539
540 save_dir = "."
541 command_socket = "@command_socket@"
542 template_dir = "@src@/templates"
543 nwtemplate_dir = "@src@/nwtemplates"
544 bundled_sources = "@target@/bundled-sources"
545 wasm_dir = "@target@/packed-wasm"
546 shapelibs = [ "@src@/library/*.toml" ]
547
548 debug_js_inject_file = "@src@/templates/log-save.js"
549
550 [log]
551 global_level = 'debug'
552
553 [log.modules]
554 rocket = 'error'
555 _ = "error" # rocket
556 # ^ comment these two out to see Tera errors, *sigh*
557
558 'hyper::server' = 'info'
559 "game::debugreader" = 'info'
560 "game::updates" = 'trace'
561 "##)?;
562
563   fs::write(CONFIG, &config)
564     .context(CONFIG).context("create server config")?;
565
566   let server_exe = ds.subst("@target@/debug/daemon-otter")?;
567   let mut cmd = Command::new(&server_exe);
568   cmd
569     .arg("--report-startup")
570     .arg(CONFIG);
571
572   (||{
573     let l = fork_something_which_prints(cmd, cln, &server_exe)?;
574     if l != DAEMON_STARTUP_REPORT {
575       throw!(anyhow!("otter-daemon startup report {:?}, expected {:?}",
576                      &l, DAEMON_STARTUP_REPORT));
577     }
578     Ok::<_,AE>(())
579   })()
580     .context("game server")?;
581
582   let mut mgmt_conn = MgmtChannel::connect(
583     &subst.subst("@command_socket@")?
584   )?;
585
586   mgmt_conn.cmd(&MgmtCommand::SetSuperuser(true))?;
587   mgmt_conn.cmd(&MgmtCommand::SelectAccount("server:".parse()?))?;  
588
589   mgmt_conn
590 }
591
592 impl DirSubst {
593   #[throws(AE)]
594   pub fn otter<S:AsRef<str>>(&self, xargs: &[S]) {
595     let ds = self;
596     let exe = ds.subst("@target@/debug/otter")?;
597     let mut args : Vec<&str> = vec![];
598     args.extend(&["--config", CONFIG]);
599     args.extend(xargs.iter().map(AsRef::as_ref));
600     let dbg = format!("running {} {:?}", &exe, &args);
601     debug!("{}", &dbg);
602     (||{
603       let mut cmd = Command::new(&exe);
604       cmd.args(&args);
605       let st = cmd
606         .spawn().context("spawn")?
607         .wait().context("wait")?;
608       if !st.success() {
609         throw!(anyhow!("wait status {}", &st));
610       }
611       Ok::<_,AE>(())
612     })()
613       .context(dbg)
614       .context("run otter client")?;
615   }
616 }
617
618 #[throws(AE)]
619 pub fn prepare_game(ds: &DirSubst, table: &str) -> InstanceName {
620   let subst = ds.also(&[("table", &table)]);
621   ds.otter(&subst.ss(
622     "--account server:                                  \
623      reset                                              \
624      --reset-table @specs@/test.table.toml              \
625                    @table@ @specs@/demo.game.toml \
626     ")?).context("reset table")?;
627
628   let instance : InstanceName = table.parse()
629     .with_context(|| table.to_owned())
630     .context("parse table name")?;
631
632   instance
633 }
634
635 #[throws(AE)]
636 fn prepare_geckodriver(opts: &Opts, cln: &cleanup_notify::Handle) {
637   const EXPECTED : &str = "Listening on 127.0.0.1:4444";
638   let mut cmd = Command::new("geckodriver");
639   if opts.geckodriver_args != "" {
640     cmd.args(opts.geckodriver_args.split(' '));
641   }
642   let l = fork_something_which_prints(cmd, cln, "geckodriver")?;
643   let fields : Vec<_> = l.split('\t').skip(2).take(2).collect();
644   let expected = ["INFO", EXPECTED];
645   if fields != expected {
646     throw!(anyhow!("geckodriver did not report as expected \
647                     - got {:?}, expected {:?}",
648                    fields, &expected));
649   }
650 }
651
652 #[throws(AE)]
653 fn prepare_thirtyfour() -> (T4d, ScreenShotCount, Vec<String>) {
654   let mut count = 0;
655   let mut caps = t4::DesiredCapabilities::firefox();
656   let prefs : HashMap<_,_> = [
657     ("devtools.console.stdout.content", true),
658   ].iter().cloned().collect();
659   caps.add("prefs", prefs)?;
660   caps.add("stdio", "inherit")?;
661   let mut driver = t4::WebDriver::new("http://localhost:4444", &caps)
662     .context("create 34 WebDriver")?;
663
664   const FRONT : &str = "front";
665   let window_names = vec![FRONT.into()];
666   driver.set_window_name(FRONT).context("set initial window name")?;
667   screenshot(&mut driver, &mut count, "startup")?;
668   driver.get(URL).context("navigate to front page")?;
669   screenshot(&mut driver, &mut count, "front")?;
670
671   fetch_log(&driver, "front")?;
672   
673   let t = Some(5_000 * MS);
674   driver.set_timeouts(t4::TimeoutConfiguration::new(t,t,t))
675     .context("set webdriver timeouts")?;
676
677   (driver, count, window_names)
678 }
679
680 /// current window must be `name`
681 #[throws(AE)]
682 fn fetch_log(driver: &T4d, name: &str) {
683   (||{
684     let got = driver.execute_script(r#"
685       var returning = window.console.saved;
686       window.console.saved = [];
687       return returning;
688     "#).context("get log")?;
689
690     for ent in got.value().as_array()
691       .ok_or(anyhow!("saved isn't an array?"))?
692     {
693       #[derive(Deserialize)]
694       struct LogEnt(String, Vec<serde_json::Value>);
695       impl fmt::Display for LogEnt {
696         #[throws(fmt::Error)]
697         fn fmt(&self, f: &mut fmt::Formatter) {
698           write!(f, "{}:", self.0)?;
699           for a in &self.1 { write!(f, " {}", a)?; }
700         }
701       }
702
703       let ent: LogEnt = serde_json::from_value(ent.clone())
704         .context("parse log entry")?;
705
706       debug!("JS {} {}", name, &ent);
707     }
708     Ok::<_,AE>(())
709   })()
710     .with_context(|| name.to_owned())
711     .context("fetch JS log messages")?;
712 }
713
714 #[derive(Debug)]
715 pub struct Window {
716   name: String,
717   instance: InstanceName,
718 }
719
720 pub struct WindowGuard<'g> {
721   su: &'g mut Setup,
722   w: &'g Window,
723 }
724
725 impl Debug for WindowGuard<'_> {
726   #[throws(fmt::Error)]
727   fn fmt(&self, f: &mut fmt::Formatter) {
728     f.debug_struct("WindowGuard")
729       .field("w.name", &self.w.name)
730       .field("w.instance", &self.w.instance.to_string())
731       .finish()?
732   }
733 }
734
735 #[throws(AE)]
736 fn check_window_name_sanity(name: &str) -> &str {
737   let e = || anyhow!("bad window name {:?}", &name);
738
739   name.chars().nth(0).ok_or(e())?
740     .is_ascii_alphanumeric().ok_or(e())?;
741
742   name.chars().all(
743     |c| c.is_ascii_alphanumeric() || c == '-' || c == '_'
744   ).ok_or(e())?;
745
746   name
747 }
748
749 impl Setup {
750   #[throws(AE)]
751   pub fn new_window<'s>(&'s mut self, instance: &Instance, name: &str)
752                         -> Window {
753     let name = check_window_name_sanity(name)?;
754     let window = (||{
755
756       self.current_window = None; // we might change the current window
757
758       match self.driver.switch_to().window_name(name) {
759         Ok(()) => throw!(anyhow!("window already exists")),
760         Err(WDE::NoSuchWindow(_)) |
761         Err(WDE::NotFound(..)) => (),
762         e@ Err(_) => {
763           eprintln!("wot {:?}", &e);
764           throw!(e
765                             .context("check for pre-existing window")
766                             .err().unwrap())
767         },
768       };
769
770       self.driver.execute_script(&format!(
771         r#"window.open('', target='{}');"#,
772         name,
773       ))
774         .context("execute script to create window")?;
775
776       Ok::<_,AE>(Window {
777         name: name.to_owned(),
778         instance: instance.0.clone(),
779       })
780     })()
781       .with_context(|| name.to_owned())
782       .context("create window")?;
783
784     self.windows_squirreled.push(name.to_owned());
785     window
786   }
787 }
788
789 impl Setup {
790   #[throws(AE)]
791   pub fn w<'s>(&'s mut self, w: &'s Window) -> WindowGuard<'s> {
792     if self.current_window.as_ref() != Some(&w.name) {
793       self.driver.switch_to().window_name(&w.name)
794         .with_context(|| w.name.to_owned())
795         .context("switch to window")?;
796       self.current_window = Some(w.name.clone());
797     }
798     WindowGuard { su: self, w }
799   }
800 }
801
802 impl<'g> Deref for WindowGuard<'g> {
803   type Target = T4d;
804   fn deref(&self) -> &T4d { &self.su.driver }
805 }
806
807 impl<'g> Drop for WindowGuard<'g> {
808   fn drop(&mut self) {
809     fetch_log(&self.su.driver, &self.w.name)
810       .just_warn();
811   }
812 }
813
814 pub trait Screenshottable {
815   fn screenshot(&mut self, slug: &str) -> Result<(),AE>;
816 }
817
818 impl<'g> Screenshottable for WindowGuard<'g> {
819   #[throws(AE)]
820   fn screenshot(&mut self, slug: &str) {
821     screenshot(&self.su.driver, &mut self.su.screenshot_count,
822                &format!("{}-{}", &self.w.name, slug))?
823   }
824 }
825
826 #[throws(AE)]
827 fn screenshot(driver: &T4d, count: &mut ScreenShotCount, slug: &str) {
828   let path = format!("{:03}{}.png", count, slug);
829   *count += 1;
830   driver.screenshot(&path::PathBuf::from(&path))
831     .context(path)
832     .context("take screenshot")?;
833 }
834
835 impl<'g> WindowGuard<'g> {
836   #[throws(AE)]
837   pub fn synch(&mut self) {
838     let cmd = MgmtCommand::AlterGame {
839       game: self.w.instance.clone(),
840       how: MgmtGameUpdateMode::Online,
841       insns: vec![ MgmtGameInstruction::Synch ],
842     };
843     let gen = if_chain!{
844       let resp = self.su.mgmt_conn.cmd(&cmd)?;
845       if let MgmtResponse::AlterGame {
846         error: None,
847         ref responses
848       } = resp;
849       if let [MgmtGameResponse::Synch(gen)] = responses[..];
850       then { gen }
851       else { throw!(anyhow!("unexpected resp to synch {:?}", resp)) }
852     };
853     trace!("{:?} gen={} ...", self, gen);
854     (|| {
855       loop {
856         let tgen = self.su.driver.execute_async_script(
857           &Subst::from(&[
858             ("wanted", &gen.to_string())
859           ]).subst(r#"
860             var done = arguments[0];
861             if (gen >= @wanted@) { done(gen); return; }
862             gen_update_hook = function() {
863               gen_update_hook = function() { };
864               done(gen);
865             };
866           "#)?
867         )
868           .context("run async script")?
869           .value().as_u64().ok_or(anyhow!("script return is not u64"))?;
870         let tgen = Generation(tgen);
871         trace!("{:?} gen={} tgen={}", self, gen, tgen);
872         if tgen >= gen { break; }
873       }
874       Ok::<(),AE>(())
875     })()
876       .context("await gen update via async js script")?;
877   }
878 }
879
880 impl Drop for FinalInfoCollection {
881   fn drop(&mut self) {
882     nix::unistd::linkat(None, "Xvfb_screen0",
883                         None, "Xvfb_keep.xwd",
884                         LinkatFlags::NoSymlinkFollow)
885       .context("preserve Xvfb screen")
886       .just_warn();
887   }
888 }
889
890 impl Drop for Setup {
891   fn drop(&mut self) {
892     (||{
893       for name in mem::take(&mut self.windows_squirreled) {
894         // This constructor is concurrency-hazardous.  It's only OK
895         // here because we have &mut self.  If there is only one
896         // Setup, there can be noone else with a Window with an
897         // identical name to be interfered with by us.
898         let w = Window {
899           name: name.clone(),
900           instance: TABLE.parse().context(TABLE)?,
901         };
902         self.w(&w)?.screenshot("final")
903           .context(name)
904           .context("final screenshot")
905           .just_warn();
906       }
907       Ok::<_,AE>(())
908     })()
909       .context("screenshots, in Setup::drop")
910       .just_warn();
911   }
912 }
913
914 #[throws(AE)]
915 pub fn setup(exe_module_path: &str) -> (Setup, Instance) {
916   env_logger::Builder::new()
917     .format_timestamp_micros()
918     .format_level(true)
919     .filter_module("otter_webdriver_tests", log::LevelFilter::Debug)
920     .filter_module(exe_module_path, log::LevelFilter::Debug)
921     .filter_level(log::LevelFilter::Info)
922     .parse_env("OTTER_WDT_LOG")
923     .init();
924   debug!("starting");
925
926   let current_exe : String = env::current_exe()
927     .context("find current executable")?
928     .to_str()
929     .ok_or_else(|| anyhow!("current executable path is not UTF-8 !"))?
930     .to_owned();
931
932   let opts = Opts::from_args();
933   if !opts.no_bwrap {
934     reinvoke_via_bwrap(&opts, &current_exe)
935       .context("reinvoke via bwrap")?;
936   }
937
938   info!("pid = {}", nix::unistd::getpid());
939   sleep(opts.pause.into());
940
941   let cln = cleanup_notify::Handle::new()?;
942   let ds = prepare_tmpdir(&opts, &current_exe)?;
943
944   prepare_xserver(&cln, &ds).always_context("setup X server")?;
945
946   let mgmt_conn =
947     prepare_gameserver(&cln, &ds).always_context("setup game server")?;
948
949   let instance_name =
950     prepare_game(&ds, TABLE).context("setup game")?;
951
952   let final_hook = FinalInfoCollection;
953
954   prepare_geckodriver(&opts, &cln).always_context("setup webdriver server")?;
955   let (driver, screenshot_count, windows_squirreled) =
956     prepare_thirtyfour().always_context("prepare web session")?;
957
958   (Setup {
959     ds,
960     mgmt_conn,
961     driver,
962     screenshot_count,
963     current_window: None,
964     windows_squirreled,
965     final_hook,
966   },
967    Instance(
968      instance_name
969    ))
970 }
971
972 impl Setup {
973   #[throws(AE)]
974   pub fn setup_static_users(&mut self, instance: &Instance) -> Vec<Window> {
975     #[throws(AE)]
976     fn mk(su: &mut Setup, instance: &Instance, u: StaticUser) -> Window {
977       let nick: &str = u.into();
978       let token = u.get_str("Token").expect("StaticUser missing Token");
979       let subst = su.ds.also([("nick",  nick),
980                               ("token", token)].iter());
981       su.ds.otter(&subst
982                   .ss("--super                          \
983                        --account server:@nick@       \
984                        --fixed-token @token@         \
985                        join-game server::dummy")?)?;
986       let w = su.new_window(instance, nick)?;
987       let url = subst.subst("@url@/?@token@")?;
988       su.w(&w)?.get(url)?;
989       su.w(&w)?.screenshot("initial")?;
990       w
991     }
992     StaticUser::iter().map(
993       |u| mk(self, instance, u)
994         .with_context(|| format!("{:?}", u))
995         .context("make static user")
996     )
997       .collect::<Result<Vec<Window>,AE>>()?
998   }
999 }