qsu 0.10.1

Service subsystem utilities and runtime wrapper.
Documentation
use std::{fs, io::ErrorKind, path::Path, sync::Arc};

use sidoc::{Builder, RenderContext};

use super::Error;

/// Generate a plist file for running service under launchd.
///
/// # Errors
/// [`Error::IO`] will be returned if file I/O failed when trying to generate
/// plist file.
pub fn install(ctx: super::RegSvc) -> Result<(), Error> {
  let mut bldr = Builder::new();
  bldr.line(r#"<?xml version="1.0" encoding="UTF-8"?>"#);
  bldr.line(r#"<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">"#);
  bldr.scope(r#"<plist version="1.0">"#, Some("</plist>"));
  bldr.scope("<dict>", Some("</dict"));

  // Use name "local.<svcname>" for now
  bldr.line("<key>Label</key>");
  bldr.line(format!("<string>{}</string>", ctx.svcname()));

  let service_binary_path = ::std::env::current_exe()?
    .to_str()
    .ok_or_else(|| Error::bad_format("Executable pathname is not utf-8"))?
    .to_string();


  if let Some(ref username) = ctx.runas.user {
    bldr.line("<key>UserName</key>");
    bldr.line(format!("<string>{username}</string>"));
  }
  if let Some(ref groupname) = ctx.runas.group {
    bldr.line("<key>GroupName</key>");
    bldr.line(format!("<string>{groupname}</string>"));
  }
  if ctx.runas.initgroups {
    bldr.line("<key>InitGroups</key>");
    bldr.line("<true/>");
  }
  if let Some(ref umask) = ctx.runas.umask {
    bldr.line("<key>Umask</key>");
    bldr.line(format!("<integer>{umask}</integer>"));
  }

  if ctx.have_args() {
    bldr.line("<key>ProgramArguments</key>");
    bldr.scope("<array>", Some("</array"));
    bldr.line(format!("<string>{service_binary_path}</string>"));

    for arg in &ctx.args {
      bldr.line(format!("<string>{arg}</string>"));
    }

    bldr.exit(); // <array>
  } else {
    bldr.line("<key>Program</key>");
    bldr.line(format!("<string>{service_binary_path}</string>"));
  }


  let mut envs = Vec::new();
  if let Some(ll) = ctx.log_level {
    envs.push((String::from("LOG_LEVEL"), ll.to_string()));
  }
  if let Some(ref lf) = ctx.log_filter {
    envs.push((String::from("LOG_FILTER"), lf.clone()));
  }
  if let Some(ref tf) = ctx.trace_filter {
    envs.push((String::from("TRACE_FILTER"), tf.clone()));
  }
  if let Some(ref fname) = ctx.trace_file {
    envs.push((String::from("TRACE_FILE"), fname.clone()));
  }
  if ctx.have_envs() {
    for (key, value) in &ctx.envs {
      envs.push((key.clone(), value.clone()));
    }
  }

  if !envs.is_empty() {
    bldr.line("<key>EnvironmentVariables</key>");
    bldr.scope("<dict>", Some("</dict"));

    for (key, value) in envs {
      bldr.line(format!("<key>{key}</key>"));
      bldr.line(format!("<string>{value}</string>"));
    }

    bldr.exit(); // <dict>
  }


  if let Some(wd) = ctx.workdir {
    bldr.line("<key>WorkingDirectory</key>");
    bldr.line(format!("<string>{wd}</string>"));
  }

  if ctx.autostart {
    bldr.line("<key>RunAtLoad</key>");
    bldr.line("<true/>");
  }


  /*
  if ctx.restart_on_failure {
    bldr.line("<key>KeepAlive</key>");
    bldr.line("<true/>");
  }
  */


  bldr.exit(); // <dict>
  bldr.exit(); // <plist>

  let doc = bldr.build()?;


  // Create a render context, add document to it
  let mut r = RenderContext::new();
  r.doc("root", Arc::new(doc));

  // Render the output
  let buf = r.render("root")?;

  // ToDo: Set proper path
  let fname = format!("{}.plist", ctx.svcname);
  let fname = Path::new(&fname);

  if fname.exists() && !ctx.force {
    Err(Error::IO(
      std::io::Error::new(ErrorKind::AlreadyExists, "File already exists")
        .to_string()
    ))?;
  }

  fs::write(fname, buf)?;

  Ok(())
}

#[expect(clippy::missing_errors_doc)]
#[expect(clippy::missing_const_for_fn)]
pub fn uninstall(_svcname: &str) -> Result<(), Error> {
  Ok(())
}

// vim: set ft=rust et sw=2 ts=2 sts=2 cinoptions=2 tw=79 :