chiark / gitweb /
cli: Add do_links parameter to setup_table
[otter.git] / cli / clisupport.rs
1 // Copyright 2020-2021 Ian Jackson and contributors to Otter
2 // SPDX-License-Identifier: AGPL-3.0-or-later
3 // There is NO WARRANTY.
4
5 use super::*;
6
7 #[derive(Clone)]
8 pub struct MapStore<T, F: FnMut(&str) -> Result<T, String>>(pub F);
9
10 pub struct BoundMapStore<'r, T, F: FnMut(&str) -> Result<T,String>> {
11   f: Rc<RefCell<F>>,
12   r: Rc<RefCell<&'r mut T>>,
13 }
14
15 impl<'f,T,F> TypedAction<T> for MapStore<T,F>
16 where F: 'f + Clone + FnMut(&str) -> Result<T,String>,
17      'f: 'static // ideally TypedAction wuld have a lifetime parameter
18 {
19   fn bind<'x>(&self, r: Rc<RefCell<&'x mut T>>) -> Action<'x> {
20     Action::Single(Box::new(BoundMapStore {
21       f: Rc::new(RefCell::new(self.0.clone())),
22       r,
23     }))
24   }
25 }
26
27 impl<'x, T, F: FnMut(&str) -> Result<T,String>>
28   IArgAction for BoundMapStore<'x, T, F>
29 {
30   fn parse_arg(&self, arg: &str) -> ParseResult {
31     let v: T = match self.f.borrow_mut()(arg) {
32       Ok(r) => r,
33       Err(e) => return ParseResult::Error(e),
34     };
35     **self.r.borrow_mut() = v;
36     ParseResult::Parsed
37   }
38 }
39
40 #[derive(Error,Debug,Clone,Display)]
41 pub struct ArgumentParseError(pub String);
42
43 impl From<&anyhow::Error> for ArgumentParseError {
44   fn from(ae: &anyhow::Error) -> ArgumentParseError {
45     eprintln!("otter: error during argument parsing/startup: {}", ae.d());
46     exit(EXIT_USAGE);
47   }
48 }
49
50 impl ArgumentParseError {
51   fn report<T:Default>(self, us: &str, apmaker: ApMaker<T>) -> ! {
52     let mut stderr = io::stderr();
53     let mut def = default();
54     let ap = apmaker(&mut def);
55     ap.error(us, &self.0, &mut stderr);
56     exit(EXIT_USAGE);
57   }
58 }
59
60 pub fn default_ssh_proxy_command() -> String {
61   format!("{} {}", DEFAULT_SSH_PROXY_CMD, SSH_PROXY_SUBCMD)
62 }
63
64 impl MainOpts {
65   pub fn game(&self) -> &str {
66     self.game.as_deref().unwrap_or_else(||{
67       eprintln!(
68         "game (table) name not specified; pass --game option");
69       exit(EXIT_USAGE);
70     })
71   }
72
73   pub fn instance(&self) -> InstanceName {
74     match self.game().strip_prefix(':') {
75       Some(rest) => {
76         InstanceName {
77           account: self.account.clone(),
78           game: rest.into(),
79         }
80       }
81       None => {
82         self.game().parse().unwrap_or_else(|e|{
83           eprintln!(
84             "game (table) name must start with : or be valid full name: {}",
85             &e);
86           exit(EXIT_USAGE);
87         })
88       }
89     }
90   }
91
92   #[throws(AE)]
93   pub fn access_account(&self) -> Conn {
94     let mut conn = connect(self)?;
95     conn.prep_access_account(self, true)?;
96     conn
97   }
98
99   #[throws(AE)]
100   pub fn access_game(&self) -> MgmtChannelForGame {
101     self.access_account()?.chan.for_game(
102       self.instance(),
103       MgmtGameUpdateMode::Online,
104     )
105   }
106
107   #[throws(AE)]
108   pub fn progressbar(&self) -> Box<dyn termprogress::Reporter> {
109     if self.verbose >= 0 {
110       termprogress::reporter()
111     } else {
112       termprogress::Null::reporter()
113     }
114   }
115 }
116
117 #[derive(Default,Debug)]
118 pub struct NoArgs { }
119 pub fn noargs(_sa: &mut NoArgs) -> ArgumentParser { ArgumentParser::new() }
120
121 pub type ApMaker<'apm, T> =
122   &'apm dyn for <'a> Fn(&'a mut T) -> ArgumentParser<'a>;
123
124 pub type ExtraMessage<'exh> =
125   &'exh dyn Fn(&mut dyn Write) -> Result<(), io::Error>;
126
127 pub type ApCompleter<'apc,T,U> =
128   &'apc dyn Fn(T) -> Result<U, ArgumentParseError>;
129
130 pub struct RawArgParserContext {
131   pub us: String,
132   pub stdout: CookedStdout,
133   pub stderr: io::Stderr,
134 }
135
136 impl RawArgParserContext {
137   pub fn new(args0: &[String]) -> Self {
138     RawArgParserContext {
139       us: args0.get(0).expect("argv[0] must be provided!").clone(),
140       stdout: CookedStdout::new(),
141       stderr: io::stderr(),
142     }
143   }
144
145   pub fn run(&mut self,
146              ap: &mut ArgumentParser<'_>,
147              args: Vec<String>,
148              extra_help: Option<ExtraMessage>,
149              extra_error: Option<ExtraMessage>) {
150     let em_call = |em: Option<ExtraMessage>, f| {
151       if let Some(em) = em { em(f).unwrap() };
152     };
153
154     let r = ap.parse(args, &mut self.stdout, &mut self.stderr);
155     if let Err(rc) = r {
156       exit(match rc {
157         0 => {
158           em_call(extra_help, &mut self.stdout);
159           0
160         },
161         2 => {
162           em_call(extra_error, &mut self.stderr);
163           EXIT_USAGE
164         },
165         _ => panic!("unexpected error rc {} from ArgumentParser::parse", rc),
166       });
167     }
168   }
169
170   pub fn done(self) -> String /* us */ {
171     self.us
172   }
173 }
174
175 pub fn argparse_more<T,U,F>(us: String, apmaker: ApMaker<T>, f: F) -> U
176   where T: Default,
177         F: FnOnce() -> Result<U, ArgumentParseError>
178 {
179   f().unwrap_or_else(|e| e.report(&us,apmaker))
180 }
181
182 pub fn parse_args<T:Default,U>(
183   args: Vec<String>,
184   apmaker: ApMaker<T>,
185   completer: ApCompleter<T,U>,
186   extra_help: Option<ExtraMessage>,
187 ) -> U {
188   let mut parsed = default();
189   let mut rapc = RawArgParserContext::new(&args);
190   let mut ap = apmaker(&mut parsed);
191   rapc.run(&mut ap, args, extra_help, None);
192   let us = rapc.done();
193   drop(ap);
194   let completed = argparse_more(us, apmaker, || completer(parsed));
195   completed
196 }
197
198 pub fn ok_id<T,E>(t: T) -> Result<T,E> { Ok(t) }
199
200 pub fn clone_via_serde<T: Debug + Serialize + DeserializeOwned>(t: &T) -> T {
201   (|| {
202     let s = serde_json::to_string(t).context("ser")?;
203     let c = serde_json::from_str(&s).context("de")?;
204     Ok::<_,AE>(c)
205   })()
206     .with_context(|| format!("clone {:?} via serde failed", t))
207     .unwrap()
208 }
209
210 #[derive(Debug)]
211 pub struct AccessOpt(Box<dyn PlayerAccessSpec>);
212 impl Clone for AccessOpt {
213   fn clone(&self) -> Self { Self(clone_via_serde(&self.0)) }
214 }
215 impl<T: PlayerAccessSpec + 'static> From<T> for AccessOpt {
216   fn from(t: T) -> Self { AccessOpt(Box::new(t)) }
217 }
218 impl From<AccessOpt> for Box<dyn PlayerAccessSpec> {
219   fn from(a: AccessOpt) -> Self { a.0 }
220 }
221
222 pub type ExecutableRelatedError = AE;
223 fn ere(s: String) -> ExecutableRelatedError { anyhow!(s) }
224
225 #[throws(ExecutableRelatedError)]
226 pub fn find_executable() -> String {
227   let e = env::current_exe()
228     .map_err(|e| ere(
229       format!("could not find current executable ({})", &e)
230     ))?;
231   let s = e.to_str()
232     .ok_or_else(|| ere(
233       format!("current executable has non-UTF8 filename!")
234     ))?;
235   s.into()
236 }
237
238 pub fn in_basedir(verbose: bool,
239                   from: Result<String,ExecutableRelatedError>,
240                   from_what: &str,
241                   from_exp_in: &str, from_must_be_in_exp: bool,
242                   now_what: &str,
243                   then_in: &str,
244                   leaf: &str,
245                   local_subdir: &str)
246                   -> String
247 {
248   match (||{
249     let from = from?;
250     if from_must_be_in_exp {
251       let mut comps = from.rsplitn(3,'/').skip(1);
252       if_chain! {
253         if Some(from_exp_in) == comps.next();
254         if let Some(path) = comps.next();
255         then { Ok(path.to_string()) }
256         else { Err(ere(
257           format!("{} is not in a directory called {}", from_what, from_exp_in)
258         )) }
259       }
260     } else {
261       let mut comps = from.rsplitn(2,'/');
262       if_chain! {
263         if let Some(dirname) = comps.nth(1);
264         let mut dir_comps = dirname.rsplitn(2,'/');
265         then {
266           if_chain! {
267             if Some(from_exp_in) == dir_comps.next();
268             if let Some(above) = dir_comps.next();
269             then { Ok(above.to_string()) }
270             else { Ok(dirname.to_string()) }
271           }
272         }
273         else {
274           Ok(from.to_string())
275         }
276       }
277     }
278   })() {
279     Err(whynot) => {
280       let r = format!("{}/{}", local_subdir, leaf);
281       if verbose {
282         eprintln!("{}: looking for {} in {}", &whynot, now_what, &r);
283       }
284       r
285     }
286     Ok(basedir) => {
287       format!("{}/{}/{}", basedir, then_in, leaf)
288     }
289   }
290 }
291
292 // argparse is pretty insistent about references and they are awkward
293 #[ext(pub)]
294 impl String {
295   fn leak(self) -> &'static str { Box::<str>::leak(self.into()) }
296 }
297
298 #[derive(Deref,DerefMut)]
299 pub struct Conn {
300   pub chan: ClientMgmtChannel,
301 }
302
303 impl Conn {
304   #[throws(AE)]
305   pub fn prep_access_account(&mut self, ma: &MainOpts,
306                          maybe_update_account: bool) {
307     #[derive(Debug)]
308     struct Wantup(bool);
309     impl Wantup {
310       fn u<T:Clone>(&mut self, rhs: &Option<T>) -> Option<T> {
311         if rhs.is_some() { self.0 = true }
312         rhs.clone()
313       }
314     }
315     let mut wantup = Wantup(false);
316
317     let mut ad = if maybe_update_account { AccountDetails {
318       account:  ma.account.clone(),
319       nick:     wantup.u(&ma.nick),
320       timezone: wantup.u(&ma.timezone),
321       layout:   wantup.u(&ma.layout),
322       access:   wantup.u(&ma.access).map(Into::into),
323     } } else {
324       AccountDetails::default(ma.account.clone())
325     };
326
327     fn is_no_account<T>(r: &Result<T, anyhow::Error>) -> bool {
328       if_chain! {
329         if let Err(e) = r;
330           if let Some(&ME::AccountNotFound(_)) = e.downcast_ref();
331         then { return true }
332         else { return false }
333       }
334     }
335
336     {
337       let mut desc;
338       let mut resp;
339       if wantup.0 {
340         desc = "UpdateAccount";
341         resp = self.cmd(&MC::UpdateAccount(clone_via_serde(&ad)));
342       } else {
343         desc = "CheckAccount";
344         resp = self.cmd(&MC::CheckAccount);
345       };
346       if is_no_account(&resp) {
347         ad.access.get_or_insert(Box::new(UrlOnStdout));
348         desc = "CreateAccount";
349         resp = self.cmd(&MC::CreateAccount(clone_via_serde(&ad)));
350       }
351       resp.with_context(||format!("response to {}", &desc))?;
352     }
353   }
354 }
355
356 #[throws(E)]
357 pub fn connect_chan(ma: &MainOpts) -> MgmtChannel {
358   match &ma.server {
359
360     SL::Socket(socket) => {
361       MgmtChannel::connect(socket)?
362     },
363
364     SL::Ssh(user_host) => {
365       
366       let user_host = {
367         let (user,host) =
368           user_host.split_once('@')
369           .unwrap_or_else(|| ("Otter", user_host));
370         format!("{}@{}", user, host)
371       };
372       
373       let mut cmd = Command::new("sh");
374       cmd.arg(if ma.verbose > 2 { "-xec" } else { "-ec" });
375       cmd.arg(format!(r#"exec {} "$@""#, &ma.ssh_command));
376       cmd.arg("x");
377       let args = [
378         &user_host,
379         &ma.ssh_proxy_command,
380       ];
381       cmd.args(args);
382
383       let desc = format!("ssh: {:?} {:?}", &ma.ssh_command, &args);
384
385       let (w,r) = childio::run_pair(cmd, desc.clone())
386         .with_context(|| desc.clone())
387         .context("run remote command")?;
388       MgmtChannel::new_boxed(r,w)
389     },
390
391   }
392 }
393
394 #[throws(E)]
395 pub fn connect(ma: &MainOpts) -> Conn {
396   let chan = connect_chan(ma)?;
397   let mut chan = Conn { chan };
398   if ma.superuser {
399     chan.cmd(&MC::SetSuperuser(true))?;
400   }
401   if ! ma.sc.props.suppress_selectaccount {
402     chan.cmd(&MC::SelectAccount(ma.account.clone()))?;
403   }
404   chan
405 }
406
407 pub const PLAYER_ALWAYS_PERMS: &[TablePermission] = &[
408   TP::TestExistence,
409   TP::ShowInList,
410   TP::ViewNotSecret,
411   TP::Play,
412 ];
413
414 pub const PLAYER_DEFAULT_PERMS: &[TablePermission] = &[
415   TP::ChangePieces,
416   TP::UploadBundles,
417 ];
418
419 #[throws(AE)]
420 pub fn setup_table(_ma: &MainOpts, instance_name: &InstanceName,
421                    spec: &TableSpec, do_links: bool)
422                -> Vec<MGI> {
423   let TableSpec { players, player_perms, acl, links } = spec;
424   let mut player_perms = player_perms.clone()
425     .unwrap_or_else(|| PLAYER_DEFAULT_PERMS.iter().cloned().collect());
426   player_perms.extend(PLAYER_ALWAYS_PERMS.iter());
427
428   let acl: RawAcl<_> =
429     players.iter().map(|tps| AclEntry {
430       account_glob: tps.account_glob(instance_name),
431       allow: player_perms.clone(),
432       deny: default(),
433     })
434     .chain(
435       acl.ents.iter().cloned()
436     )
437     .collect();
438
439   let acl = acl.try_into()?;
440
441   let mut insns = vec![];
442   insns.push(MGI::SetACL { acl });
443   if do_links { insns.push(MGI::SetLinks(links.clone())); }
444   insns
445 }
446
447 pub trait SomeSpec {
448   const WHAT   : &'static str;
449   const FNCOMP : &'static str;
450 }
451
452 impl SomeSpec for GameSpec {
453   const WHAT   : &'static str = "game spec";
454   const FNCOMP : &'static str = "game";
455 }
456
457 impl SomeSpec for TableSpec {
458   const WHAT   : &'static str = "table spec";
459   const FNCOMP : &'static str = "table";
460 }
461
462 pub trait SpecParse {
463   type T;
464   type S: SomeSpec;
465   fn parse(s: String) -> Result<Self::T,AE>;
466 }
467 #[derive(Debug,Copy,Clone,Educe)]
468 #[educe(Default)]
469 pub struct SpecParseToml<T>(pub PhantomData<T>);
470 impl<T:DeserializeOwned+SomeSpec> SpecParse for SpecParseToml<T> {
471   type T = T;
472   type S = T;
473   #[throws(AE)]
474   fn parse(buf: String) -> T {
475     let tv: toml::Value = buf.parse().context("parse TOML")?;
476     let spec: T = toml_de::from_value(&tv).context("parse value")?;
477     spec
478   }
479 }
480 impl<T> SpecParseToml<T> { pub fn new() -> Self { default() } }
481 #[derive(Educe)]
482 #[educe(Default)]
483 pub struct SpecRaw<T>(pub PhantomData<T>);
484 impl<T:SomeSpec> SpecParse for SpecRaw<T> {
485   type T = String;
486   type S = T;
487   #[throws(AE)]
488   fn parse(buf: String) -> String { buf }
489 }
490 impl<T> SpecRaw<T> { pub fn new() -> Self { default() } }
491
492 pub fn spec_arg_is_path(specname: &str) -> Option<String> {
493   if specname.contains('/') {
494     Some(specname.to_string())
495   } else {
496     None
497   }
498 }
499
500 #[throws(AE)]
501 pub fn read_spec<P:SpecParse>(ma: &MainOpts, specname: &str, p: P) -> P::T
502 {
503   let filename = spec_arg_is_path(specname).unwrap_or_else(
504     || format!("{}/{}.{}.toml", &ma.spec_dir, specname, P::S::FNCOMP)
505   );
506   read_spec_from_path(filename, p)?
507 }
508
509 #[throws(AE)]
510 pub fn read_spec_from_path<P:SpecParse>(filename: String, _: P) -> P::T
511 {
512   (||{
513     let mut f = File::open(&filename).context("open")?;
514     let mut buf = String::new();
515     f.read_to_string(&mut buf).context("read")?;
516     let spec = P::parse(buf)?;
517     Ok::<_,AE>(spec)
518   })().with_context(|| format!("read {} {:?}", P::S::WHAT, &filename))?
519 }
520
521 #[macro_export]
522 macro_rules! inventory_subcmd {
523   {$verb:expr, $help:expr $(,)?} => {
524     inventory::submit!{Subcommand {
525       verb: $verb,
526       help: $help,
527       call,
528       props: $crate::SubcommandProperties::DEFAULT,
529     }}
530   };
531   {$verb:expr, $help:expr, $($prop:tt)+} => {
532     inventory::submit!{Subcommand {
533       verb: $verb,
534       help: $help,
535       call,
536       props: SubcommandProperties {
537         $($prop)*
538         ..$crate::SubcommandProperties::DEFAULT
539       },
540     }}
541   };
542 }