chiark / gitweb /
Break out clisupport.rs
authorIan Jackson <ijackson@chiark.greenend.org.uk>
Wed, 2 Jun 2021 22:56:05 +0000 (23:56 +0100)
committerIan Jackson <ijackson@chiark.greenend.org.uk>
Wed, 2 Jun 2021 22:56:05 +0000 (23:56 +0100)
Signed-off-by: Ian Jackson <ijackson@chiark.greenend.org.uk>
cli/clisupport.rs [new file with mode: 0644]
cli/otter.rs

diff --git a/cli/clisupport.rs b/cli/clisupport.rs
new file mode 100644 (file)
index 0000000..ffb7151
--- /dev/null
@@ -0,0 +1,429 @@
+// Copyright 2020-2021 Ian Jackson and contributors to Otter
+// SPDX-License-Identifier: AGPL-3.0-or-later
+// There is NO WARRANTY.
+
+use super::*;
+
+#[derive(Clone)]
+pub struct MapStore<T, F: FnMut(&str) -> Result<T, String>>(pub F);
+
+pub struct BoundMapStore<'r, T, F: FnMut(&str) -> Result<T,String>> {
+  f: Rc<RefCell<F>>,
+  r: Rc<RefCell<&'r mut T>>,
+}
+
+impl<'f,T,F> TypedAction<T> for MapStore<T,F>
+where F: 'f + Clone + FnMut(&str) -> Result<T,String>,
+     'f: 'static // ideally TypedAction wuld have a lifetime parameter
+{
+  fn bind<'x>(&self, r: Rc<RefCell<&'x mut T>>) -> Action<'x> {
+    Action::Single(Box::new(BoundMapStore {
+      f: Rc::new(RefCell::new(self.0.clone())),
+      r,
+    }))
+  }
+}
+
+impl<'x, T, F: FnMut(&str) -> Result<T,String>>
+  IArgAction for BoundMapStore<'x, T, F>
+{
+  fn parse_arg(&self, arg: &str) -> ParseResult {
+    let v: T = match self.f.borrow_mut()(arg) {
+      Ok(r) => r,
+      Err(e) => return ParseResult::Error(e),
+    };
+    **self.r.borrow_mut() = v;
+    ParseResult::Parsed
+  }
+}
+
+#[derive(Error,Debug,Clone,Display)]
+pub struct ArgumentParseError(pub String);
+
+impl From<&anyhow::Error> for ArgumentParseError {
+  fn from(ae: &anyhow::Error) -> ArgumentParseError {
+    eprintln!("error during argument parsing/startup: {}\n", ae);
+    exit(EXIT_USAGE);
+  }
+}
+
+pub type ApMaker<'apm, T> =
+  &'apm dyn for <'a> Fn(&'a mut T) -> ArgumentParser<'a>;
+
+pub fn parse_args<T:Default,U>(
+  args: Vec<String>,
+  apmaker: ApMaker<T>,
+  completer: &dyn Fn(T) -> Result<U, ArgumentParseError>,
+  extra_help: Option<&dyn Fn(&mut dyn Write) -> Result<(), io::Error>>,
+) -> U {
+  let mut parsed = default();
+  let ap = apmaker(&mut parsed);
+  let us = args.get(0).expect("argv[0] must be provided!").clone();
+
+  let mut stdout = CookedStdout::new();
+  let mut stderr = io::stderr();
+
+  let r = ap.parse(args, &mut stdout, &mut stderr);
+  if let Err(rc) = r {
+    exit(match rc {
+      0 => {
+        if let Some(eh) = extra_help {
+          eh(&mut stdout).unwrap();
+        }
+        0
+      },
+      2 => EXIT_USAGE,
+      _ => panic!("unexpected error rc {} from ArgumentParser::parse", rc),
+    });
+  }
+  mem::drop(stdout);
+  mem::drop(ap);
+  let completed  = completer(parsed)
+    .unwrap_or_else(|e:ArgumentParseError| {
+      let mut def = default();
+      let ap = apmaker(&mut def);
+      ap.error(&us, &e.0, &mut stderr);
+      exit(EXIT_USAGE);
+    });
+  completed
+}
+
+pub fn ok_id<T,E>(t: T) -> Result<T,E> { Ok(t) }
+
+pub fn clone_via_serde<T: Debug + Serialize + DeserializeOwned>(t: &T) -> T {
+  (|| {
+    let s = serde_json::to_string(t).context("ser")?;
+    let c = serde_json::from_str(&s).context("de")?;
+    Ok::<_,AE>(c)
+  })()
+    .with_context(|| format!("clone {:?} via serde failed", t))
+    .unwrap()
+}
+
+#[derive(Debug)]
+pub struct AccessOpt(Box<dyn PlayerAccessSpec>);
+impl Clone for AccessOpt {
+  fn clone(&self) -> Self { Self(clone_via_serde(&self.0)) }
+}
+impl<T: PlayerAccessSpec + 'static> From<T> for AccessOpt {
+  fn from(t: T) -> Self { AccessOpt(Box::new(t)) }
+}
+impl From<AccessOpt> for Box<dyn PlayerAccessSpec> {
+  fn from(a: AccessOpt) -> Self { a.0 }
+}
+
+pub type ExecutableRelatedError = AE;
+fn ere(s: String) -> ExecutableRelatedError { anyhow!(s) }
+
+#[throws(ExecutableRelatedError)]
+pub fn find_executable() -> String {
+  let e = env::current_exe()
+    .map_err(|e| ere(
+      format!("could not find current executable ({})", &e)
+    ))?;
+  let s = e.to_str()
+    .ok_or_else(|| ere(
+      format!("current executable has non-UTF8 filename!")
+    ))?;
+  s.into()
+}
+
+pub fn in_basedir(verbose: bool,
+                  from: Result<String,ExecutableRelatedError>,
+                  from_what: &str,
+                  from_exp_in: &str, from_must_be_in_exp: bool,
+                  now_what: &str,
+                  then_in: &str,
+                  leaf: &str,
+                  local_subdir: &str)
+                  -> String
+{
+  match (||{
+    let from = from?;
+    if from_must_be_in_exp {
+      let mut comps = from.rsplitn(3,'/').skip(1);
+      if_chain! {
+        if Some(from_exp_in) == comps.next();
+        if let Some(path) = comps.next();
+        then { Ok(path.to_string()) }
+        else { Err(ere(
+          format!("{} is not in a directory called {}", from_what, from_exp_in)
+        )) }
+      }
+    } else {
+      let mut comps = from.rsplitn(2,'/');
+      if_chain! {
+        if let Some(dirname) = comps.nth(1);
+        let mut dir_comps = dirname.rsplitn(2,'/');
+        then {
+          if_chain! {
+            if Some(from_exp_in) == dir_comps.next();
+            if let Some(above) = dir_comps.next();
+            then { Ok(above.to_string()) }
+            else { Ok(dirname.to_string()) }
+          }
+        }
+        else {
+          Ok(from.to_string())
+        }
+      }
+    }
+  })() {
+    Err(whynot) => {
+      let r = format!("{}/{}", local_subdir, leaf);
+      if verbose {
+        eprintln!("{}: looking for {} in {}", &whynot, now_what, &r);
+      }
+      r
+    }
+    Ok(basedir) => {
+      format!("{}/{}/{}", basedir, then_in, leaf)
+    }
+  }
+}
+
+// argparse is pretty insistent about references and they are awkward
+#[ext(pub)]
+impl String {
+  fn leak(self) -> &'static str { Box::<str>::leak(self.into()) }
+}
+
+pub struct Conn {
+  pub chan: ClientMgmtChannel,
+}
+
+deref_to_field_mut!{Conn, MgmtChannel, chan}
+
+impl Conn {
+  #[throws(AE)]
+  pub fn prep_access_account(&mut self, ma: &MainOpts,
+                         maybe_update_account: bool) {
+    #[derive(Debug)]
+    struct Wantup(bool);
+    impl Wantup {
+      fn u<T:Clone>(&mut self, rhs: &Option<T>) -> Option<T> {
+        if rhs.is_some() { self.0 = true }
+        rhs.clone()
+      }
+    }
+    let mut wantup = Wantup(false);
+
+    let mut ad = if maybe_update_account { AccountDetails {
+      account:  ma.account.clone(),
+      nick:     wantup.u(&ma.nick),
+      timezone: wantup.u(&ma.timezone),
+      layout:   wantup.u(&ma.layout),
+      access:   wantup.u(&ma.access).map(Into::into),
+    } } else {
+      AccountDetails::default(ma.account.clone())
+    };
+
+    fn is_no_account<T>(r: &Result<T, anyhow::Error>) -> bool {
+      if_chain! {
+        if let Err(e) = r;
+          if let Some(&ME::AccountNotFound(_)) = e.downcast_ref();
+        then { return true }
+        else { return false }
+      }
+    }
+
+    {
+      let mut desc;
+      let mut resp;
+      if wantup.0 {
+        desc = "UpdateAccount";
+        resp = self.cmd(&MC::UpdateAccount(clone_via_serde(&ad)));
+      } else {
+        desc = "CheckAccount";
+        resp = self.cmd(&MC::CheckAccount);
+      };
+      if is_no_account(&resp) {
+        ad.access.get_or_insert(Box::new(UrlOnStdout));
+        desc = "CreateAccount";
+        resp = self.cmd(&MC::CreateAccount(clone_via_serde(&ad)));
+      }
+      resp.with_context(||format!("response to {}", &desc))?;
+    }
+  }
+}
+
+#[throws(E)]
+pub fn connect_chan(ma: &MainOpts) -> MgmtChannel {
+  match &ma.server {
+
+    SL::Socket(socket) => {
+      MgmtChannel::connect(socket)?
+    },
+
+    SL::Ssh(user_host) => {
+      
+      let user_host = {
+        let (user,host) =
+          user_host.split_once('@')
+          .unwrap_or_else(|| ("Otter", user_host));
+        format!("{}@{}", user, host)
+      };
+      
+      let mut cmd = Command::new("sh");
+      cmd.arg(if ma.verbose > 2 { "-xec" } else { "-ec" });
+      cmd.arg(format!(r#"exec {} "$@""#, &ma.ssh_command));
+      cmd.arg("x");
+      let args = [
+        &user_host,
+        &ma.ssh_proxy_command,
+      ];
+      cmd.args(args);
+
+      let desc = format!("ssh: {:?} {:?}", &ma.ssh_command, &args);
+
+      let (w,r) = childio::run_pair(cmd, desc.clone())
+        .with_context(|| desc.clone())
+        .context("run remote command")?;
+      MgmtChannel::new_boxed(r,w)
+    },
+
+  }
+}
+
+#[throws(E)]
+pub fn connect(ma: &MainOpts) -> Conn {
+  let chan = connect_chan(ma)?;
+  let mut chan = Conn { chan };
+  if ma.superuser {
+    chan.cmd(&MC::SetSuperuser(true))?;
+  }
+  if ! ma.sc.props.suppress_selectaccount {
+    chan.cmd(&MC::SelectAccount(ma.account.clone()))?;
+  }
+  chan
+}
+
+pub const PLAYER_ALWAYS_PERMS: &[TablePermission] = &[
+  TP::TestExistence,
+  TP::ShowInList,
+  TP::ViewNotSecret,
+  TP::Play,
+];
+
+pub const PLAYER_DEFAULT_PERMS: &[TablePermission] = &[
+  TP::ChangePieces,
+  TP::UploadBundles,
+];
+
+#[throws(AE)]
+pub fn setup_table(_ma: &MainOpts, instance_name: &InstanceName, spec: &TableSpec)
+               -> Vec<MGI> {
+  let TableSpec { players, player_perms, acl, links } = spec;
+  let mut player_perms = player_perms.clone()
+    .unwrap_or(PLAYER_DEFAULT_PERMS.iter().cloned().collect());
+  player_perms.extend(PLAYER_ALWAYS_PERMS.iter());
+
+  let acl: RawAcl<_> =
+    players.iter().map(|tps| AclEntry {
+      account_glob: tps.account_glob(instance_name),
+      allow: player_perms.clone(),
+      deny: default(),
+    })
+    .chain(
+      acl.ents.iter().cloned()
+    )
+    .collect();
+
+  let acl = acl.try_into()?;
+
+  let mut insns = vec![];
+  insns.push(MGI::SetACL { acl });
+  insns.push(MGI::SetLinks(links.clone()));
+  insns
+}
+
+pub trait SomeSpec {
+  const WHAT   : &'static str;
+  const FNCOMP : &'static str;
+}
+
+impl SomeSpec for GameSpec {
+  const WHAT   : &'static str = "game spec";
+  const FNCOMP : &'static str = "game";
+}
+
+impl SomeSpec for TableSpec {
+  const WHAT   : &'static str = "table spec";
+  const FNCOMP : &'static str = "table";
+}
+
+pub trait SpecParse {
+  type T;
+  type S: SomeSpec;
+  fn parse(s: String) -> Result<Self::T,AE>;
+}
+#[derive(Debug,Copy,Clone)]
+pub struct SpecParseToml<T>(pub PhantomData<T>);
+impl<T:DeserializeOwned+SomeSpec> SpecParse for SpecParseToml<T> {
+  type T = T;
+  type S = T;
+  #[throws(AE)]
+  fn parse(buf: String) -> T {
+    let tv: toml::Value = buf.parse().context("parse TOML")?;
+    let spec: T = toml_de::from_value(&tv).context("parse value")?;
+    spec
+  }
+}
+impl<T> SpecParseToml<T> { pub fn new() -> Self { Self(default()) } }
+pub struct SpecRaw<T>(pub PhantomData<T>);
+impl<T:SomeSpec> SpecParse for SpecRaw<T> {
+  type T = String;
+  type S = T;
+  #[throws(AE)]
+  fn parse(buf: String) -> String { buf }
+}
+impl<T> SpecRaw<T> { pub fn new() -> Self { Self(default()) } }
+
+pub fn spec_arg_is_path(specname: &str) -> Option<String> {
+  if specname.contains('/') {
+    Some(specname.to_string())
+  } else {
+    None
+  }
+}
+
+#[throws(AE)]
+pub fn read_spec<P:SpecParse>(ma: &MainOpts, specname: &str, p: P) -> P::T
+{
+  let filename = spec_arg_is_path(specname).unwrap_or_else(
+    || format!("{}/{}.{}.toml", &ma.spec_dir, specname, P::S::FNCOMP)
+  );
+  read_spec_from_path(filename, p)?
+}
+
+#[throws(AE)]
+pub fn read_spec_from_path<P:SpecParse>(filename: String, _: P) -> P::T
+{
+  (||{
+    let mut f = File::open(&filename).context("open")?;
+    let mut buf = String::new();
+    f.read_to_string(&mut buf).context("read")?;
+    let spec = P::parse(buf)?;
+    Ok::<_,AE>(spec)
+  })().with_context(|| format!("read {} {:?}", P::S::WHAT, &filename))?
+}
+
+#[macro_export]
+macro_rules! inventory_subcmd {
+  {$verb:expr, $help:expr $(,)?} => {
+    inventory::submit!{Subcommand {
+      verb: $verb,
+      help: $help,
+      call,
+      props: default(),
+    }}
+  };
+  {$verb:expr, $help:expr, $($prop:tt)+} => {
+    inventory::submit!{Subcommand {
+      verb: $verb,
+      help: $help,
+      call,
+      props: SubcommandProperties { $($prop)* ..default() },
+    }}
+  };
+}
index b0e2acc58057abfe9f585eaae03b8c93e246fc66..9c1b4193749a1915f50f6929f44da935c8264ce3 100644 (file)
@@ -6,58 +6,28 @@
 
 pub type MgmtChannel = ClientMgmtChannel;
 
-use otter::imports::*;
+pub use otter::imports::*;
 
-use std::cell::Cell;
-use std::cell::RefCell;
-use std::rc::Rc;
+pub use std::cell::Cell;
+pub use std::cell::RefCell;
+pub use std::rc::Rc;
 
-use argparse::{self,ArgumentParser,action::{TypedAction,ParseResult}};
-use argparse::action::{Action,IFlagAction,IArgAction};
-use derive_more::Display;
+pub use argparse::{self,ArgumentParser,action::{TypedAction,ParseResult}};
+pub use argparse::action::{Action,IFlagAction,IArgAction};
+pub use derive_more::Display;
 
-use otter::prelude::*;
-use otter::commands::*;
+pub use otter::prelude::*;
+pub use otter::commands::*;
 
-type APE = ArgumentParseError;
-type E = anyhow::Error;
-type PL = PresentationLayout;
-type TP = TablePermission;
+pub type APE = ArgumentParseError;
+pub type E = anyhow::Error;
+pub type PL = PresentationLayout;
+pub type TP = TablePermission;
 
-use argparse::action::ParseResult::Parsed;
+pub use argparse::action::ParseResult::Parsed;
 
-#[derive(Clone)]
-struct MapStore<T, F: FnMut(&str) -> Result<T, String>>(F);
-
-struct BoundMapStore<'r, T, F: FnMut(&str) -> Result<T,String>> {
-  f: Rc<RefCell<F>>,
-  r: Rc<RefCell<&'r mut T>>,
-}
-
-impl<'f,T,F> TypedAction<T> for MapStore<T,F>
-where F: 'f + Clone + FnMut(&str) -> Result<T,String>,
-     'f: 'static // ideally TypedAction wuld have a lifetime parameter
-{
-  fn bind<'x>(&self, r: Rc<RefCell<&'x mut T>>) -> Action<'x> {
-    Action::Single(Box::new(BoundMapStore {
-      f: Rc::new(RefCell::new(self.0.clone())),
-      r,
-    }))
-  }
-}
-
-impl<'x, T, F: FnMut(&str) -> Result<T,String>>
-  IArgAction for BoundMapStore<'x, T, F>
-{
-  fn parse_arg(&self, arg: &str) -> ParseResult {
-    let v: T = match self.f.borrow_mut()(arg) {
-      Ok(r) => r,
-      Err(e) => return ParseResult::Error(e),
-    };
-    **self.r.borrow_mut() = v;
-    ParseResult::Parsed
-  }
-}
+pub mod clisupport;
+use clisupport::*;
 
 #[derive(Debug)]
 enum ServerLocation {
@@ -67,7 +37,7 @@ enum ServerLocation {
 use ServerLocation as SL;
 
 #[derive(Debug)]
-struct MainOpts {
+pub struct MainOpts {
   account: AccountName,
   nick: Option<String>,
   timezone: Option<String>,
@@ -165,157 +135,6 @@ pub struct SubCommandCallArgs {
 }
 pub type SCCA = SubCommandCallArgs;
 
-#[derive(Error,Debug,Clone,Display)]
-struct ArgumentParseError(String);
-
-impl From<&anyhow::Error> for ArgumentParseError {
-  fn from(ae: &anyhow::Error) -> ArgumentParseError {
-    eprintln!("error during argument parsing/startup: {}\n", ae);
-    exit(EXIT_USAGE);
-  }
-}
-
-pub type ApMaker<'apm, T> =
-  &'apm dyn for <'a> Fn(&'a mut T) -> ArgumentParser<'a>;
-
-fn parse_args<T:Default,U>(
-  args: Vec<String>,
-  apmaker: ApMaker<T>,
-  completer: &dyn Fn(T) -> Result<U, ArgumentParseError>,
-  extra_help: Option<&dyn Fn(&mut dyn Write) -> Result<(), io::Error>>,
-) -> U {
-  let mut parsed = default();
-  let ap = apmaker(&mut parsed);
-  let us = args.get(0).expect("argv[0] must be provided!").clone();
-
-  let mut stdout = CookedStdout::new();
-  let mut stderr = io::stderr();
-
-  let r = ap.parse(args, &mut stdout, &mut stderr);
-  if let Err(rc) = r {
-    exit(match rc {
-      0 => {
-        if let Some(eh) = extra_help {
-          eh(&mut stdout).unwrap();
-        }
-        0
-      },
-      2 => EXIT_USAGE,
-      _ => panic!("unexpected error rc {} from ArgumentParser::parse", rc),
-    });
-  }
-  mem::drop(stdout);
-  mem::drop(ap);
-  let completed  = completer(parsed)
-    .unwrap_or_else(|e:ArgumentParseError| {
-      let mut def = default();
-      let ap = apmaker(&mut def);
-      ap.error(&us, &e.0, &mut stderr);
-      exit(EXIT_USAGE);
-    });
-  completed
-}
-
-pub fn ok_id<T,E>(t: T) -> Result<T,E> { Ok(t) }
-
-pub fn clone_via_serde<T: Debug + Serialize + DeserializeOwned>(t: &T) -> T {
-  (|| {
-    let s = serde_json::to_string(t).context("ser")?;
-    let c = serde_json::from_str(&s).context("de")?;
-    Ok::<_,AE>(c)
-  })()
-    .with_context(|| format!("clone {:?} via serde failed", t))
-    .unwrap()
-}
-
-#[derive(Debug)]
-struct AccessOpt(Box<dyn PlayerAccessSpec>);
-impl Clone for AccessOpt {
-  fn clone(&self) -> Self { Self(clone_via_serde(&self.0)) }
-}
-impl<T: PlayerAccessSpec + 'static> From<T> for AccessOpt {
-  fn from(t: T) -> Self { AccessOpt(Box::new(t)) }
-}
-impl From<AccessOpt> for Box<dyn PlayerAccessSpec> {
-  fn from(a: AccessOpt) -> Self { a.0 }
-}
-
-type ExecutableRelatedError = AE;
-fn ere(s: String) -> ExecutableRelatedError { anyhow!(s) }
-
-#[throws(ExecutableRelatedError)]
-pub fn find_executable() -> String {
-  let e = env::current_exe()
-    .map_err(|e| ere(
-      format!("could not find current executable ({})", &e)
-    ))?;
-  let s = e.to_str()
-    .ok_or_else(|| ere(
-      format!("current executable has non-UTF8 filename!")
-    ))?;
-  s.into()
-}
-
-pub fn in_basedir(verbose: bool,
-                  from: Result<String,ExecutableRelatedError>,
-                  from_what: &str,
-                  from_exp_in: &str, from_must_be_in_exp: bool,
-                  now_what: &str,
-                  then_in: &str,
-                  leaf: &str,
-                  local_subdir: &str)
-                  -> String
-{
-  match (||{
-    let from = from?;
-    if from_must_be_in_exp {
-      let mut comps = from.rsplitn(3,'/').skip(1);
-      if_chain! {
-        if Some(from_exp_in) == comps.next();
-        if let Some(path) = comps.next();
-        then { Ok(path.to_string()) }
-        else { Err(ere(
-          format!("{} is not in a directory called {}", from_what, from_exp_in)
-        )) }
-      }
-    } else {
-      let mut comps = from.rsplitn(2,'/');
-      if_chain! {
-        if let Some(dirname) = comps.nth(1);
-        let mut dir_comps = dirname.rsplitn(2,'/');
-        then {
-          if_chain! {
-            if Some(from_exp_in) == dir_comps.next();
-            if let Some(above) = dir_comps.next();
-            then { Ok(above.to_string()) }
-            else { Ok(dirname.to_string()) }
-          }
-        }
-        else {
-          Ok(from.to_string())
-        }
-      }
-    }
-  })() {
-    Err(whynot) => {
-      let r = format!("{}/{}", local_subdir, leaf);
-      if verbose {
-        eprintln!("{}: looking for {} in {}", &whynot, now_what, &r);
-      }
-      r
-    }
-    Ok(basedir) => {
-      format!("{}/{}/{}", basedir, then_in, leaf)
-    }
-  }
-}
-
-// argparse is pretty insistent about references and they are awkward
-#[ext]
-impl String {
-  fn leak(self) -> &'static str { Box::<str>::leak(self.into()) }
-}
-
 fn main() {
   #[derive(Default,Debug)]
   struct RawMainArgs {
@@ -560,245 +379,6 @@ fn main() {
     .unwrap_or_else(|e| e.end_process(12));
 }
 
-struct Conn {
-  chan: ClientMgmtChannel,
-}
-
-deref_to_field_mut!{Conn, MgmtChannel, chan}
-
-impl Conn {
-  #[throws(AE)]
-  fn prep_access_account(&mut self, ma: &MainOpts,
-                         maybe_update_account: bool) {
-    #[derive(Debug)]
-    struct Wantup(bool);
-    impl Wantup {
-      fn u<T:Clone>(&mut self, rhs: &Option<T>) -> Option<T> {
-        if rhs.is_some() { self.0 = true }
-        rhs.clone()
-      }
-    }
-    let mut wantup = Wantup(false);
-
-    let mut ad = if maybe_update_account { AccountDetails {
-      account:  ma.account.clone(),
-      nick:     wantup.u(&ma.nick),
-      timezone: wantup.u(&ma.timezone),
-      layout:   wantup.u(&ma.layout),
-      access:   wantup.u(&ma.access).map(Into::into),
-    } } else {
-      AccountDetails::default(ma.account.clone())
-    };
-
-    fn is_no_account<T>(r: &Result<T, anyhow::Error>) -> bool {
-      if_chain! {
-        if let Err(e) = r;
-          if let Some(&ME::AccountNotFound(_)) = e.downcast_ref();
-        then { return true }
-        else { return false }
-      }
-    }
-
-    {
-      let mut desc;
-      let mut resp;
-      if wantup.0 {
-        desc = "UpdateAccount";
-        resp = self.cmd(&MC::UpdateAccount(clone_via_serde(&ad)));
-      } else {
-        desc = "CheckAccount";
-        resp = self.cmd(&MC::CheckAccount);
-      };
-      if is_no_account(&resp) {
-        ad.access.get_or_insert(Box::new(UrlOnStdout));
-        desc = "CreateAccount";
-        resp = self.cmd(&MC::CreateAccount(clone_via_serde(&ad)));
-      }
-      resp.with_context(||format!("response to {}", &desc))?;
-    }
-  }
-}
-
-#[throws(E)]
-fn connect_chan(ma: &MainOpts) -> MgmtChannel {
-  match &ma.server {
-
-    SL::Socket(socket) => {
-      MgmtChannel::connect(socket)?
-    },
-
-    SL::Ssh(user_host) => {
-      
-      let user_host = {
-        let (user,host) =
-          user_host.split_once('@')
-          .unwrap_or_else(|| ("Otter", user_host));
-        format!("{}@{}", user, host)
-      };
-      
-      let mut cmd = Command::new("sh");
-      cmd.arg(if ma.verbose > 2 { "-xec" } else { "-ec" });
-      cmd.arg(format!(r#"exec {} "$@""#, &ma.ssh_command));
-      cmd.arg("x");
-      let args = [
-        &user_host,
-        &ma.ssh_proxy_command,
-      ];
-      cmd.args(args);
-
-      let desc = format!("ssh: {:?} {:?}", &ma.ssh_command, &args);
-
-      let (w,r) = childio::run_pair(cmd, desc.clone())
-        .with_context(|| desc.clone())
-        .context("run remote command")?;
-      MgmtChannel::new_boxed(r,w)
-    },
-
-  }
-}
-
-#[throws(E)]
-fn connect(ma: &MainOpts) -> Conn {
-  let chan = connect_chan(ma)?;
-  let mut chan = Conn { chan };
-  if ma.superuser {
-    chan.cmd(&MC::SetSuperuser(true))?;
-  }
-  if ! ma.sc.props.suppress_selectaccount {
-    chan.cmd(&MC::SelectAccount(ma.account.clone()))?;
-  }
-  chan
-}
-
-const PLAYER_ALWAYS_PERMS: &[TablePermission] = &[
-  TP::TestExistence,
-  TP::ShowInList,
-  TP::ViewNotSecret,
-  TP::Play,
-];
-
-const PLAYER_DEFAULT_PERMS: &[TablePermission] = &[
-  TP::ChangePieces,
-  TP::UploadBundles,
-];
-
-#[throws(AE)]
-fn setup_table(_ma: &MainOpts, instance_name: &InstanceName, spec: &TableSpec)
-               -> Vec<MGI> {
-  let TableSpec { players, player_perms, acl, links } = spec;
-  let mut player_perms = player_perms.clone()
-    .unwrap_or(PLAYER_DEFAULT_PERMS.iter().cloned().collect());
-  player_perms.extend(PLAYER_ALWAYS_PERMS.iter());
-
-  let acl: RawAcl<_> =
-    players.iter().map(|tps| AclEntry {
-      account_glob: tps.account_glob(instance_name),
-      allow: player_perms.clone(),
-      deny: default(),
-    })
-    .chain(
-      acl.ents.iter().cloned()
-    )
-    .collect();
-
-  let acl = acl.try_into()?;
-
-  let mut insns = vec![];
-  insns.push(MGI::SetACL { acl });
-  insns.push(MGI::SetLinks(links.clone()));
-  insns
-}
-
-trait SomeSpec {
-  const WHAT   : &'static str;
-  const FNCOMP : &'static str;
-}
-
-impl SomeSpec for GameSpec {
-  const WHAT   : &'static str = "game spec";
-  const FNCOMP : &'static str = "game";
-}
-
-impl SomeSpec for TableSpec {
-  const WHAT   : &'static str = "table spec";
-  const FNCOMP : &'static str = "table";
-}
-
-trait SpecParse {
-  type T;
-  type S: SomeSpec;
-  fn parse(s: String) -> Result<Self::T,AE>;
-}
-#[derive(Debug,Copy,Clone)]
-struct SpecParseToml<T>(pub PhantomData<T>);
-impl<T:DeserializeOwned+SomeSpec> SpecParse for SpecParseToml<T> {
-  type T = T;
-  type S = T;
-  #[throws(AE)]
-  fn parse(buf: String) -> T {
-    let tv: toml::Value = buf.parse().context("parse TOML")?;
-    let spec: T = toml_de::from_value(&tv).context("parse value")?;
-    spec
-  }
-}
-impl<T> SpecParseToml<T> { pub fn new() -> Self { Self(default()) } }
-struct SpecRaw<T>(pub PhantomData<T>);
-impl<T:SomeSpec> SpecParse for SpecRaw<T> {
-  type T = String;
-  type S = T;
-  #[throws(AE)]
-  fn parse(buf: String) -> String { buf }
-}
-impl<T> SpecRaw<T> { pub fn new() -> Self { Self(default()) } }
-
-fn spec_arg_is_path(specname: &str) -> Option<String> {
-  if specname.contains('/') {
-    Some(specname.to_string())
-  } else {
-    None
-  }
-}
-
-#[throws(AE)]
-fn read_spec<P:SpecParse>(ma: &MainOpts, specname: &str, p: P) -> P::T
-{
-  let filename = spec_arg_is_path(specname).unwrap_or_else(
-    || format!("{}/{}.{}.toml", &ma.spec_dir, specname, P::S::FNCOMP)
-  );
-  read_spec_from_path(filename, p)?
-}
-
-#[throws(AE)]
-fn read_spec_from_path<P:SpecParse>(filename: String, _: P) -> P::T
-{
-  (||{
-    let mut f = File::open(&filename).context("open")?;
-    let mut buf = String::new();
-    f.read_to_string(&mut buf).context("read")?;
-    let spec = P::parse(buf)?;
-    Ok::<_,AE>(spec)
-  })().with_context(|| format!("read {} {:?}", P::S::WHAT, &filename))?
-}
-
-macro_rules! inventory_subcmd {
-  {$verb:expr, $help:expr $(,)?} => {
-    inventory::submit!{Subcommand {
-      verb: $verb,
-      help: $help,
-      call,
-      props: default(),
-    }}
-  };
-  {$verb:expr, $help:expr, $($prop:tt)+} => {
-    inventory::submit!{Subcommand {
-      verb: $verb,
-      help: $help,
-      call,
-      props: SubcommandProperties { $($prop)* ..default() },
-    }}
-  };
-}
-
 //---------- list-games ----------
 
 mod list_games {