1 // Copyright 2020-2021 Ian Jackson and contributors to Otter
2 // SPDX-License-Identifier: AGPL-3.0-or-later
3 // There is NO WARRANTY.
8 pub struct MapStore<T, F: FnMut(&str) -> Result<T, String>>(pub F);
10 pub struct BoundMapStore<'r, T, F: FnMut(&str) -> Result<T,String>> {
12 r: Rc<RefCell<&'r mut T>>,
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
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())),
27 impl<'x, T, F: FnMut(&str) -> Result<T,String>>
28 IArgAction for BoundMapStore<'x, T, F>
30 fn parse_arg(&self, arg: &str) -> ParseResult {
31 let v: T = match self.f.borrow_mut()(arg) {
33 Err(e) => return ParseResult::Error(e),
35 **self.r.borrow_mut() = v;
40 #[derive(Error,Debug,Clone,Display)]
41 pub struct ArgumentParseError(pub String);
43 impl From<&anyhow::Error> for ArgumentParseError {
44 fn from(ae: &anyhow::Error) -> ArgumentParseError {
45 eprintln!("otter: error during argument parsing/startup: {}", ae.d());
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);
60 pub fn default_ssh_proxy_command() -> String {
61 format!("{} {}", DEFAULT_SSH_PROXY_CMD, SSH_PROXY_SUBCMD)
65 pub fn game(&self) -> &str {
66 self.game.as_deref().unwrap_or_else(||{
68 "game (table) name not specified; pass --game option");
73 pub fn instance(&self) -> InstanceName {
74 match self.game().strip_prefix(':') {
77 account: self.account.clone(),
82 self.game().parse().unwrap_or_else(|e|{
84 "game (table) name must start with : or be valid full name: {}",
93 pub fn access_account(&self) -> Conn {
94 let mut conn = connect(self)?;
95 conn.prep_access_account(self, true)?;
100 pub fn access_game(&self) -> MgmtChannelForGame {
101 self.access_account()?.chan.for_game(
103 MgmtGameUpdateMode::Online,
108 pub fn progressbar(&self) -> Box<dyn termprogress::Reporter> {
109 if self.verbose >= 0 {
110 termprogress::reporter()
112 termprogress::Null::reporter()
117 #[derive(Default,Debug)]
118 pub struct NoArgs { }
119 pub fn noargs(_sa: &mut NoArgs) -> ArgumentParser { ArgumentParser::new() }
121 pub type ApMaker<'apm, T> =
122 &'apm dyn for <'a> Fn(&'a mut T) -> ArgumentParser<'a>;
124 pub type ExtraMessage<'exh> =
125 &'exh dyn Fn(&mut dyn Write) -> Result<(), io::Error>;
127 pub type ApCompleter<'apc,T,U> =
128 &'apc dyn Fn(T) -> Result<U, ArgumentParseError>;
130 pub struct RawArgParserContext {
132 pub stdout: CookedStdout,
133 pub stderr: io::Stderr,
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(),
145 pub fn run(&mut self,
146 ap: &mut ArgumentParser<'_>,
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() };
154 let r = ap.parse(args, &mut self.stdout, &mut self.stderr);
158 em_call(extra_help, &mut self.stdout);
162 em_call(extra_error, &mut self.stderr);
165 _ => panic!("unexpected error rc {} from ArgumentParser::parse", rc),
170 pub fn done(self) -> String /* us */ {
175 pub fn argparse_more<T,U,F>(us: String, apmaker: ApMaker<T>, f: F) -> U
177 F: FnOnce() -> Result<U, ArgumentParseError>
179 f().unwrap_or_else(|e| e.report(&us,apmaker))
182 pub fn parse_args<T:Default,U>(
185 completer: ApCompleter<T,U>,
186 extra_help: Option<ExtraMessage>,
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();
194 let completed = argparse_more(us, apmaker, || completer(parsed));
198 pub fn ok_id<T,E>(t: T) -> Result<T,E> { Ok(t) }
200 pub fn clone_via_serde<T: Debug + Serialize + DeserializeOwned>(t: &T) -> T {
202 let s = serde_json::to_string(t).context("ser")?;
203 let c = serde_json::from_str(&s).context("de")?;
206 .with_context(|| format!("clone {:?} via serde failed", t))
211 pub struct AccessOpt(Box<dyn PlayerAccessSpec>);
212 impl Clone for AccessOpt {
213 fn clone(&self) -> Self { Self(clone_via_serde(&self.0)) }
215 impl<T: PlayerAccessSpec + 'static> From<T> for AccessOpt {
216 fn from(t: T) -> Self { AccessOpt(Box::new(t)) }
218 impl From<AccessOpt> for Box<dyn PlayerAccessSpec> {
219 fn from(a: AccessOpt) -> Self { a.0 }
222 pub type ExecutableRelatedError = AE;
223 fn ere(s: String) -> ExecutableRelatedError { anyhow!(s) }
225 #[throws(ExecutableRelatedError)]
226 pub fn find_executable() -> String {
227 let e = env::current_exe()
229 format!("could not find current executable ({})", &e)
233 format!("current executable has non-UTF8 filename!")
238 pub fn in_basedir(verbose: bool,
239 from: Result<String,ExecutableRelatedError>,
241 from_exp_in: &str, from_must_be_in_exp: bool,
250 if from_must_be_in_exp {
251 let mut comps = from.rsplitn(3,'/').skip(1);
253 if Some(from_exp_in) == comps.next();
254 if let Some(path) = comps.next();
255 then { Ok(path.to_string()) }
257 format!("{} is not in a directory called {}", from_what, from_exp_in)
261 let mut comps = from.rsplitn(2,'/');
263 if let Some(dirname) = comps.nth(1);
264 let mut dir_comps = dirname.rsplitn(2,'/');
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()) }
280 let r = format!("{}/{}", local_subdir, leaf);
282 eprintln!("{}: looking for {} in {}", &whynot, now_what, &r);
287 format!("{}/{}/{}", basedir, then_in, leaf)
292 // argparse is pretty insistent about references and they are awkward
295 fn leak(self) -> &'static str { Box::<str>::leak(self.into()) }
298 #[derive(Deref,DerefMut)]
300 pub chan: ClientMgmtChannel,
305 pub fn prep_access_account(&mut self, ma: &MainOpts,
306 maybe_update_account: bool) {
310 fn u<T:Clone>(&mut self, rhs: &Option<T>) -> Option<T> {
311 if rhs.is_some() { self.0 = true }
315 let mut wantup = Wantup(false);
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),
324 AccountDetails::default(ma.account.clone())
327 fn is_no_account<T>(r: &Result<T, anyhow::Error>) -> bool {
330 if let Some(&ME::AccountNotFound(_)) = e.downcast_ref();
332 else { return false }
340 desc = "UpdateAccount";
341 resp = self.cmd(&MC::UpdateAccount(clone_via_serde(&ad)));
343 desc = "CheckAccount";
344 resp = self.cmd(&MC::CheckAccount);
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)));
351 resp.with_context(||format!("response to {}", &desc))?;
357 pub fn connect_chan(ma: &MainOpts) -> MgmtChannel {
360 SL::Socket(socket) => {
361 MgmtChannel::connect(socket)?
364 SL::Ssh(user_host) => {
368 user_host.split_once('@')
369 .unwrap_or_else(|| ("Otter", user_host));
370 format!("{}@{}", user, host)
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));
379 &ma.ssh_proxy_command,
383 let desc = format!("ssh: {:?} {:?}", &ma.ssh_command, &args);
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)
395 pub fn connect(ma: &MainOpts) -> Conn {
396 let chan = connect_chan(ma)?;
397 let mut chan = Conn { chan };
399 chan.cmd(&MC::SetSuperuser(true))?;
401 if ! ma.sc.props.suppress_selectaccount {
402 chan.cmd(&MC::SelectAccount(ma.account.clone()))?;
407 pub const PLAYER_ALWAYS_PERMS: &[TablePermission] = &[
414 pub const PLAYER_DEFAULT_PERMS: &[TablePermission] = &[
420 pub fn setup_table(_ma: &MainOpts, instance_name: &InstanceName,
421 spec: &TableSpec, do_links: bool)
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());
429 players.iter().map(|tps| AclEntry {
430 account_glob: tps.account_glob(instance_name),
431 allow: player_perms.clone(),
435 acl.ents.iter().cloned()
439 let acl = acl.try_into()?;
441 let mut insns = vec![];
442 insns.push(MGI::SetACL { acl });
443 if do_links { insns.push(MGI::SetLinks(links.clone())); }
448 const WHAT : &'static str;
449 const FNCOMP : &'static str;
452 impl SomeSpec for GameSpec {
453 const WHAT : &'static str = "game spec";
454 const FNCOMP : &'static str = "game";
457 impl SomeSpec for TableSpec {
458 const WHAT : &'static str = "table spec";
459 const FNCOMP : &'static str = "table";
462 pub trait SpecParse {
465 fn parse(s: String) -> Result<Self::T,AE>;
467 #[derive(Debug,Copy,Clone,Educe)]
469 pub struct SpecParseToml<T>(pub PhantomData<T>);
470 impl<T:DeserializeOwned+SomeSpec> SpecParse for SpecParseToml<T> {
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")?;
480 impl<T> SpecParseToml<T> { pub fn new() -> Self { default() } }
483 pub struct SpecRaw<T>(pub PhantomData<T>);
484 impl<T:SomeSpec> SpecParse for SpecRaw<T> {
488 fn parse(buf: String) -> String { buf }
490 impl<T> SpecRaw<T> { pub fn new() -> Self { default() } }
492 pub fn spec_arg_is_path(specname: &str) -> Option<String> {
493 if specname.contains('/') {
494 Some(specname.to_string())
501 pub fn read_spec<P:SpecParse>(ma: &MainOpts, specname: &str, p: P) -> P::T
503 let filename = spec_arg_is_path(specname).unwrap_or_else(
504 || format!("{}/{}.{}.toml", &ma.spec_dir, specname, P::S::FNCOMP)
506 read_spec_from_path(filename, p)?
510 pub fn read_spec_from_path<P:SpecParse>(filename: String, _: P) -> P::T
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)?;
518 })().with_context(|| format!("read {} {:?}", P::S::WHAT, &filename))?
522 macro_rules! inventory_subcmd {
523 {$verb:expr, $help:expr $(,)?} => {
524 inventory::submit!{Subcommand {
528 props: $crate::SubcommandProperties::DEFAULT,
531 {$verb:expr, $help:expr, $($prop:tt)+} => {
532 inventory::submit!{Subcommand {
536 props: SubcommandProperties {
538 ..$crate::SubcommandProperties::DEFAULT