chiark / gitweb /
wip config specs in otter
authorIan Jackson <ijackson@chiark.greenend.org.uk>
Sat, 21 Nov 2020 20:08:07 +0000 (20:08 +0000)
committerIan Jackson <ijackson@chiark.greenend.org.uk>
Sat, 21 Nov 2020 20:08:07 +0000 (20:08 +0000)
Signed-off-by: Ian Jackson <ijackson@chiark.greenend.org.uk>
Cargo.lock.example
Cargo.toml
src/bin/daemon-otter.rs
src/bin/otter.rs
src/config.rs

index 9727eb58c504926952ad1d7a81d3d5997a755ca6..c27d1cfacb2800edd10be06804f5b8fdbc7325f3 100644 (file)
@@ -1115,6 +1115,7 @@ dependencies = [
  "chrono",
  "chrono-tz",
  "delegate",
+ "derive_more",
  "either",
  "failure",
  "fehler",
index edb4edc53ba00b17dc435ed111391df1c77477c1..cc055521561b920465b878c8c9b8d210b225f626 100644 (file)
@@ -24,6 +24,7 @@ boolinator = "2"
 chrono = "0.4"
 chrono-tz = "0.5"
 delegate = "0.4"
+derive_more = "0.99"
 either = "1"
 failure = "0.1.8" # for pwd
 fehler = "1"
index 95e2fa8ce5bb6825fd049619596585093cc8c944..76104cc8b4a71223b05ea10b0f9dbe2914739f9c 100644 (file)
@@ -106,7 +106,7 @@ fn main() {
   // todo test suite for web api
 
   let config_filename = env::args().nth(1);
-  ServerConfig::read(config_filename.as_ref().map(String::as_str), true)?;
+  ServerConfig::read(config_filename.as_ref().map(String::as_str))?;
 
   std::env::set_var("ROCKET_CLI_COLORS","off");
 
index 672639c2b665756d4e700a5ca545b55fb65ef063..3975118e7d71137c9e1fcc88002be88ba359d01f 100644 (file)
@@ -7,6 +7,7 @@
 use otter::imports::*;
 use argparse::{self,ArgumentParser,action::{TypedAction,ParseResult}};
 use argparse::action::{Action,IFlagAction,IArgAction};
+use derive_more::Display;
 use std::rc::Rc;
 use std::cell::RefCell;
 use std::cell::Cell;
@@ -67,6 +68,7 @@ struct MainOpts {
   socket_path: String,
   verbose: i32,
   superuser: bool,
+  specs_dir: Option<String>,
 }
 
 impl MainOpts {
@@ -175,6 +177,56 @@ impl From<AccessOpt> for Box<dyn PlayerAccessSpec> {
   fn from(a: AccessOpt) -> Self { a.0 }
 }
 
+#[derive(Error,Debug,Display)]
+struct ExecutableRelatedError(String);
+
+#[throws(ExecutableRelatedError)]
+pub fn find_executable() -> String {
+  let e = env::current_exe()
+    .map_err(|e| ExecutableRelatedError(
+      format!("could not find current executable ({})", &e)
+    ))?;
+  let s = e.to_str()
+    .ok_or_else(|| ExecutableRelatedError(
+      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,
+                  now_what: &str,
+                  then_in: &str,
+                  leaf: &str,
+                  local_subdir: &str)
+                  -> String
+{
+  match (||{
+    let mut comps = from?.rsplit('/').skip(1);
+    if_chain! {
+      if Some(from_exp_in) == comps.next();
+      if let Some(path) = comps.next();
+      then { Ok(path) }
+      else { Err(ExecutableRelatedError(
+        format!("{} is not in a directory called {}", from_what, from_exp_in)
+      )) }
+    }
+  })() {
+    Err(whynot) => {
+      let r = format!("{}/{}", local_subdir, leaf);
+      if verbose {
+        eprintln!("{}: looking for {} in {}", &whynot.0, now_what, &r);
+      }
+      r
+    }
+    Ok(basedir) => {
+      format!("{}/{}/{}", basedir, then_in, leaf)
+    }
+  }
+}
+
 fn main() {
   #[derive(Default,Debug)]
   struct RawMainArgs {
@@ -188,6 +240,7 @@ fn main() {
     superuser: bool,
     subcommand: String,
     subargs: Vec<String>,
+    spec_dir: Option<String>,
   };
   let (subcommand, subargs, mo) = parse_args::<RawMainArgs,_>(
     env::args().collect(),
@@ -248,12 +301,37 @@ fn main() {
       .add_option(&["--super"], StoreTrue,
                   "enable game server superuser access");
 
+    ap.refer(&mut rma.spec_dir)
+      .add_option(&["--spec-dir"], StoreOption,
+                  "directory for table and game specs");
+
     ap
   }, &|RawMainArgs {
     account, nick, timezone,
     access, socket_path, verbose, config_filename, superuser,
-    subcommand, subargs,
+    subcommand, subargs, spec_dir,
   }|{
+    let config_info = Thunk::new(move ||{
+      let config_filename = config_filename
+        .unwrap_or_else(||{
+          let exe = find_executable();
+          in_basedir(verbose > 1, exe, "current executable", "bin",
+                     "config file", "etc", DEFAULT_CONFIG_LEAFNAME,
+                     ".")
+        });
+      ServerConfig::read(Some(&config_filename))
+        .context("read config file")?;
+      Ok::<_,AE>((otter::config::config(), config_filename))
+    });
+
+    let spec_dir = spec_dir.unwrap_or_else(||{
+      let cfg = &config_info.as_ref()?.1;
+      let spec_dir = in_basedir(verbose > 1, cfg, "config filename", "etc",
+                 "game and table specs", "specs", "", ".")
+        .strip_suffix("/").to_string();
+      Ok(spec_dir)
+    })?;
+
     let account : AccountName = account.map(Ok::<_,APE>).unwrap_or_else(||{
       let user = env::var("USER").map_err(|e| ArgumentParseError(
         format!("default account needs USER env var: {}", &e)
@@ -263,14 +341,7 @@ fn main() {
         subaccount: "".into(),
       })
     })?;
-    let config = Thunk::new(||{
-      ServerConfig::read(
-        config_filename.as_ref().map(String::as_str),
-        verbose > 1
-      )
-        .context("read config file")?;
-      Ok::<_,AE>(otter::config::config())
-    });
+
     let socket_path = socket_path.map(Ok::<_,APE>).unwrap_or_else(||{
       Ok(config.as_ref()?.command_socket.clone())
     })?;
@@ -282,6 +353,7 @@ fn main() {
       socket_path,
       verbose,
       superuser,
+      spec_dir,
     }))
   }, Some(&|w|{
     writeln!(w, "\nSubcommands:")?;
@@ -618,7 +690,14 @@ fn setup_table(_ma: &MainOpts, spec: &TableSpec) -> Vec<MGI> {
 */
 
 #[throws(AE)]
-fn read_spec<T: DeserializeOwned>(filename: &str, what: &str) -> T {
+fn read_spec<T: DeserializeOwned>(ma: &MainOpts, specname: &str,
+                                  kind: &str, what: &str) -> T {
+  let filename = if specname.contains('/') {
+    specname.to_string()
+  } else {
+    in_basedir(ma.basedir, "specs","specs",
+               format!("{}.{}.toml", specname, kind))
+  };
   (||{
     let mut f = File::open(filename).context("open")?;
     let mut buf = String::new();
@@ -659,13 +738,16 @@ mod reset_game {
   fn subargs(sa: &mut Args) -> ArgumentParser {
     use argparse::*;
     let mut ap = ArgumentParser::new();
-    ap.refer(&mut sa.table_file).metavar("TABLE-SPEC-TOML")
+    ap.refer(&mut sa.table_file)
+      .metavar("TABLE-SPEC[-TOML]")
       .add_option(&["--reset-table"],StoreOption,
                   "reset the players and access too");
     ap.refer(&mut sa.table_name).required()
       .add_argument("TABLE-NAME",Store,"table name");
     ap.refer(&mut sa.game_file).required()
-      .add_argument("GAME-SPEC-TOML",Store,"game spec");
+      .add_argument("GAME-SPEC[-TOML]",Store,
+ "game spec (path to .toml file, or found in specs directory if no '/')"
+      );
     ap
   }
 
index 581cb16a15996c0d4c1f42324baa0260e7a6547a..241ca097741e9d3507a2bc919a05686a57a2d845 100644 (file)
@@ -9,7 +9,8 @@ pub const EXIT_SITUATION : i32 =  8;
 pub const EXIT_USAGE     : i32 = 12;
 pub const EXIT_DISASTER  : i32 = 16;
 
-pub const DEFAULT_CONFIG_FILENAME : &str = "server.toml";
+pub const DEFAULT_CONFIG_DIR      : &str = "/etc/otter";
+pub const DEFAULT_CONFIG_LEAFNAME : &str = "server.toml";
 
 const DEFAULT_SAVE_DIRECTORY : &str = "save";
 const DEFAULT_COMMAND_SOCKET : &str = "command.socket"; // in save dir
@@ -114,65 +115,12 @@ fn set_config(config: ServerConfig) {
   *GLOBAL.config.write().unwrap() = Arc::new(config)
 }
 
-fn default_basedir_from_executable(verbose: bool) -> Option<String> {
-  #[throws(StartupError)]
-  fn inner(verbose: bool) -> Option<String> {
-    match match env::current_exe()
-      .map(|p| p.to_str().map(|s| s.to_string()))
-    {
-      Err(e) => Err(format!(
-        "could not find current executable ({})", &e
-      )),
-      Ok(None) => Err(format!(
-        "current executable has non-UTF8 filename!"
-      )),
-      Ok(Some(basedir)) if basedir.rsplit('/').nth(1) == Some("bin") => Ok(
-        format!("{}/", basedir)
-      ),
-      Ok(_) => Err(format!(
-        "current executable is not in a directory called bin"
-      )),
-    } {
-      Err(whynot) => {
-        if verbose {
-          eprintln!("{}: looking for ancillary files in current directory",
-                    &whynot);
-        }
-        None
-      },
-      Ok(f) => Some(f),
-    }
-  }
-
-  inner(verbose).unwrap_or_else(|e|{
-    eprintln!("startup error finding installation pieces: {}", &e);
-    exit(EXIT_DISASTER)
-  })
-}
-
-fn in_basedir
-  (basedir: Option<&String>,
-   subdir: &str,
-   localdir: &str,
-   leaf: &str) -> String
-{
-  match basedir {
-    Some(basedir) => format!("{}/{}/{}", basedir, subdir, leaf),
-    None          => format!(   "{}/{}",        localdir, leaf),
-  }
-}
-
 impl ServerConfig {
   #[throws(StartupError)]
-  pub fn read(config_filename: Option<&str>, verbose: bool) {
-    let basedir = Thunk::new(
-      move || default_basedir_from_executable(verbose)
-    );
-    let config_filename = config_filename
-      .map(
-        |s| s.to_string()
-      ).unwrap_or_else(
-        || in_basedir(basedir.as_ref(), "etc", "", DEFAULT_CONFIG_FILENAME)
+  pub fn read(config_filename: Option<&str>) {
+    let config_filename = config_filename.map(|s| s.to_string())
+      .unwrap_or_else(
+        || format!("{}/{}", DEFAULT_CONFIG_DIR, DEFAULT_CONFIG_LEAFNAME)
       );
     let mut buf = String::new();
     File::open(&config_filename).with_context(||config_filename.to_string())?