Repository: Nukesor/pueue (Rust)

Issue:
[BUG][CHANGE] Configurable PID and move default location to runtime dir
There are several issues with the way the pid file is currently managed.

### The name of the `pid` file is non-configurable.

As a result, one cannot launch more than one daemon per user, which is something that has been encouraged with the new configuration `profile` feature.

The plan is to add a new configuration flag, which allows to explicitly specify the name of the `pid` file for a given daemon instance.

### The pid lives in the `pueue_directory` instead of the `runtime_directory`.

This leads to a rare bug I ran into last week:
- Pueued runs as usual.
- system crashes
   -> The pid file still exists and isn't removed
- The system restarts
- Another long running process snatches that PID
- Pueue refuses to start up, since there's a process with the ID that's in the current pueue.pid.

To fix this, we should move the PID file to the runtime directory, which ensures that the pid file doesn't survive a restart.


Discussion:

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 3bfbe0789..c66fe2535 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -10,6 +10,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
 
 - Better debug output for migration instructions from v1 to v2 [#298](https://github.com/Nukesor/pueue/issues/298).
 - Better error output and error context for filesystem related errors (Continuation).
+- Add a new option to specify the location of the `PID` file: `shared.pid_path` [#302](https://github.com/Nukesor/pueue/issues/298).
 
 ### Fixed
 
@@ -17,6 +18,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
     * the `-vvv` flags
     * the `--profile` option.
 - Autocompletion shell scripts. They generation is now also tested to prevent regressions.
+- Move the `PID` file into the runtime directory to prevent rare startup issues after crashes + reboot. [#302](https://github.com/Nukesor/pueue/issues/298).
+    This won't cause any problems for running clients/daemons, making this a backward compatible change.
 
 ## [2.0.1] - 2022-03-12
 
diff --git a/daemon/lib.rs b/daemon/lib.rs
index b80f8e43b..a7366a773 100644
--- a/daemon/lib.rs
+++ b/daemon/lib.rs
@@ -69,8 +69,7 @@ pub async fn run(config_path: Option<PathBuf>, profile: Option<String>, test: bo
     }
     init_shared_secret(&settings.shared.shared_secret_path())
         .context("Failed to initialize shared secret.")?;
-    pid::create_pid_file(&settings.shared.pueue_directory())
-        .context("Failed to create pid file.")?;
+    pid::create_pid_file(&settings.shared.pid_path()).context("Failed to create pid file.")?;
 
     // Restore the previous state and save any changes that might have happened during this
     // process. If no previous state exists, just create a new one.
@@ -167,7 +166,7 @@ fn setup_signal_panic_handling(settings: &Settings, sender: &Sender<Message>) ->
         orig_hook(panic_info);
 
         // Cleanup the pid file
-        if let Err(error) = pid::cleanup_pid_file(&settings_clone.shared.pueue_directory()) {
+        if let Err(error) = pid::cleanup_pid_file(&settings_clone.shared.pid_path()) {
             println!("Failed to cleanup pid after panic.");
             println!("{error}");
         }
diff --git a/daemon/pid.rs b/daemon/pid.rs
index fc893fde9..684c317a6 100644
--- a/daemon/pid.rs
+++ b/daemon/pid.rs
@@ -3,15 +3,19 @@ use std::io::{Read, Write};
 use std::path::Path;
 
 use anyhow::{bail, Context, Result};
+use log::info;
+use pueue_lib::error::Error;
 
 use crate::platform::process_helper::process_exists;
 
 /// Read a PID file and throw an error, if another daemon instance is still running.
 fn check_for_running_daemon(pid_path: &Path) -> Result<()> {
-    let mut file = File::open(&pid_path).context("Failed to open PID file")?;
+    info!("Placing pid file at {pid_path:?}");
+    let mut file =
+        File::open(&pid_path).map_err(|err| Error::IoError("opening pid file".to_string(), err))?;
     let mut pid = String::new();
     file.read_to_string(&mut pid)
-        .context("Failed to read PID file")?;
+        .map_err(|err| Error::IoError("reading pid file".to_string(), err))?;
 
     let pid: u32 = pid
         .parse()
@@ -29,30 +33,25 @@ fn check_for_running_daemon(pid_path: &Path) -> Result<()> {
 
 /// Create a file containing the current pid of the daemon's main process.
 /// Fails if it already exists or cannot be created.
-pub fn create_pid_file(pueue_dir: &Path) -> Result<()> {
-    let pid_path = pueue_dir.join("pueue.pid");
+pub fn create_pid_file(pid_path: &Path) -> Result<()> {
     // If an old PID file exists, check if the referenced process is still running.
     // The pid might not have been properly cleaned up, if the machine or Pueue crashed hard.
     if pid_path.exists() {
-        check_for_running_daemon(&pid_path)?;
+        check_for_running_daemon(pid_path)?;
     }
-    let mut file = File::create(pid_path)?;
+    let mut file = File::create(pid_path)
+        .map_err(|err| Error::IoError("creating pid file".to_string(), err))?;
 
-    file.write_all(std::process::id().to_string().as_bytes())?;
+    file.write_all(std::process::id().to_string().as_bytes())
+        .map_err(|err| Error::IoError("writing pid file".to_string(), err))?;
 
     Ok(())
 }
 
 /// Remove the daemon's pid file.
 /// Errors if it doesn't exist or cannot be deleted.
-pub fn cleanup_pid_file(pueue_dir: &Path) -> Result<()> {
-    let pid_file = pueue_dir.join("pueue.pid");
-    if !pid_file.exists() {
-        bail!(
-            "Couldn't remove pid file, since it doesn't exists. This shouldn't happen: {pid_file:?}"
-        );
-    }
-
-    std::fs::remove_file(pid_file)?;
+pub fn cleanup_pid_file(pid_path: &Path) -> Result<()> {
+    std::fs::remove_file(pid_path)
+        .map_err(|err| Error::IoError("removing pid file".to_string(), err))?;
     Ok(())
 }
diff --git a/daemon/state_helper.rs b/daemon/state_helper.rs
index 58eba68d5..8e77c6534 100644
--- a/daemon/state_helper.rs
+++ b/daemon/state_helper.rs
@@ -137,7 +137,7 @@ pub fn restore_state(pueue_directory: &Path) -> Result<Option<State>> {
         info!("Couldn't find state from previous session at location: {path:?}");
         return Ok(None);
     }
-    info!("Start restoring state");
+    info!("Restoring state");
 
     // Try to load the file.
     let data = fs::read_to_string(&path).context("State restore: Failed to read file:\n\n{}")?;
diff --git a/lib/src/network/platform/unix/socket.rs b/lib/src/network/platform/unix/socket.rs
index 785b168bb..b42cc8dcf 100644
--- a/lib/src/network/platform/unix/socket.rs
+++ b/lib/src/network/platform/unix/socket.rs
@@ -2,6 +2,7 @@ use std::convert::TryFrom;
 use std::path::PathBuf;
 
 use async_trait::async_trait;
+use log::info;
 use rustls::ServerName;
 use tokio::io::{AsyncRead, AsyncWrite};
 use tokio::net::{TcpListener, TcpStream, UnixListener, UnixStream};
@@ -126,6 +127,7 @@ pub async fn get_client_stream(settings: &Shared) -> Result<GenericStream, Error
 pub async fn get_listener(settings: &Shared) -> Result<GenericListener, Error> {
     if settings.use_unix_socket {
         let socket_path = settings.unix_socket_path();
+        info!("Using unix socket at: {socket_path:?}");
 
         // Check, if the socket already exists
         // In case it does, we have to check, if it's an active socket.
@@ -148,6 +150,7 @@ pub async fn get_listener(settings: &Shared) -> Result<GenericListener, Error> {
 
     // This is the listener, which accepts low-level TCP connections
     let address = format!("{}:{}", &settings.host, &settings.port);
+    info!("Binding to address: {address}");
     let tcp_listener = TcpListener::bind(&address)
         .await
         .map_err(|err| Error::IoError("binding tcp listener to address".to_string(), err))?;
diff --git a/lib/src/settings.rs b/lib/src/settings.rs
index 66d223f03..259d8bdfc 100644
--- a/lib/src/settings.rs
+++ b/lib/src/settings.rs
@@ -28,6 +28,9 @@ pub struct Shared {
     #[cfg(not(target_os = "windows"))]
     #[serde(default = "default_true")]
     pub use_unix_socket: bool,
+    /// The path where the daemon's PID is located.
+    /// This is by default in `runtime_directory/pueue.pid`.
+    pub pid_path: Option<PathBuf>,
     /// Don't access this property directly, but rather use the getter with the same name.
     /// It's only public to allow proper integration testing.
     ///
@@ -205,6 +208,16 @@ impl Shared {
         }
     }
 
+    /// The daemon's pid path can either be explicitly specified or it's simply placed in the
+    /// current runtime directory.
+    pub fn pid_path(&self) -> PathBuf {
+        if let Some(path) = &self.pid_path {
+            expand_home(path)
+        } else {
+            self.runtime_directory().join("pueue.pid")
+        }
+    }
+
     pub fn daemon_cert(&self) -> PathBuf {
         if let Some(path) = &self.daemon_cert {
             expand_home(path)


Test patch:
diff --git a/lib/tests/helper.rs b/lib/tests/helper.rs
index 9937a2398..37539a454 100644
--- a/lib/tests/helper.rs
+++ b/lib/tests/helper.rs
@@ -16,7 +16,8 @@ pub fn get_shared_settings() -> (Shared, TempDir) {
         #[cfg(not(target_os = "windows"))]
         use_unix_socket: true,
         #[cfg(not(target_os = "windows"))]
-        unix_socket_path: Some(tempdir_path.join("test.socket")),
+        unix_socket_path: None,
+        pid_path: None,
         host: "localhost".to_string(),
         port: pick_unused_port()
             .expect("There should be a free port")
diff --git a/tests/fixtures/daemon.rs b/tests/fixtures/daemon.rs
index 802905945..a4af63bd8 100644
--- a/tests/fixtures/daemon.rs
+++ b/tests/fixtures/daemon.rs
@@ -1,5 +1,5 @@
 use std::collections::HashMap;
-use std::path::{Path, PathBuf};
+use std::path::PathBuf;
 use std::process::{Child, Command, Stdio};
 
 use anyhow::{bail, Context, Result};
@@ -30,13 +30,13 @@ pub async fn daemon() -> Result<PueueDaemon> {
     let path = pueue_dir.to_path_buf();
     // Start/spin off the daemon and get its PID
     tokio::spawn(run_and_handle_error(path, true));
-    let pid = get_pid(pueue_dir)?;
+    let pid = get_pid(&settings.shared.pid_path())?;
 
     let tries = 20;
     let mut current_try = 0;
 
     // Wait up to 1s for the unix socket to pop up.
-    let socket_path = pueue_dir.join("test.socket");
+    let socket_path = settings.shared.unix_socket_path();
     while current_try < tries {
         sleep_ms(50);
         if socket_path.exists() {
@@ -72,7 +72,7 @@ async fn run_and_handle_error(pueue_dir: PathBuf, test: bool) -> Result<()> {
 
 /// Spawn the daemon by calling the actual pueued binary.
 /// This function also checks for the pid file and the unix socket to appear.
-pub async fn standalone_daemon(pueue_dir: &Path) -> Result<Child> {
+pub async fn standalone_daemon(shared: &Shared) -> Result<Child> {
     // Inject an environment variable into the daemon.
     // This is used to ensure that the spawned subprocesses won't inherit the daemon's environment.
     let mut envs = HashMap::new();
@@ -80,7 +80,7 @@ pub async fn standalone_daemon(pueue_dir: &Path) -> Result<Child> {
 
     let child = Command::cargo_bin("pueued")?
         .arg("--config")
-        .arg(pueue_dir.join("pueue.yml").to_str().unwrap())
+        .arg(shared.pueue_directory().join("pueue.yml").to_str().unwrap())
         .arg("-vvv")
         .envs(envs)
         .stdout(Stdio::piped())
@@ -91,7 +91,7 @@ pub async fn standalone_daemon(pueue_dir: &Path) -> Result<Child> {
     let mut current_try = 0;
 
     // Wait up to 1s for the unix socket to pop up.
-    let socket_path = pueue_dir.join("test.socket");
+    let socket_path = shared.unix_socket_path();
     while current_try < tries {
         sleep_ms(50);
         if socket_path.exists() {
@@ -118,7 +118,8 @@ pub fn daemon_base_setup() -> Result<(Settings, TempDir)> {
         #[cfg(not(target_os = "windows"))]
         use_unix_socket: true,
         #[cfg(not(target_os = "windows"))]
-        unix_socket_path: Some(tempdir_path.join("test.socket")),
+        unix_socket_path: None,
+        pid_path: None,
         host: "localhost".to_string(),
         port: "51230".to_string(),
         daemon_cert: Some(tempdir_path.join("certs").join("daemon.cert")),
diff --git a/tests/helper/daemon.rs b/tests/helper/daemon.rs
index 780d1a530..a4de78040 100644
--- a/tests/helper/daemon.rs
+++ b/tests/helper/daemon.rs
@@ -21,22 +21,20 @@ pub async fn shutdown_daemon(shared: &Shared) -> Result<Message> {
 /// Get a daemon pid from a specific pueue directory.
 /// This function gives the daemon a little time to boot up, but ultimately crashes if it takes too
 /// long.
-pub fn get_pid(pueue_dir: &Path) -> Result<i32> {
-    let pid_file = pueue_dir.join("pueue.pid");
-
+pub fn get_pid(pid_path: &Path) -> Result<i32> {
     // Give the daemon about 1 sec to boot and create the pid file.
     let tries = 20;
     let mut current_try = 0;
 
     while current_try < tries {
         // The daemon didn't create the pid file yet. Wait for 100ms and try again.
-        if !pid_file.exists() {
+        if !pid_path.exists() {
             sleep_ms(50);
             current_try += 1;
             continue;
         }
 
-        let mut file = File::open(&pid_file).context("Couldn't open pid file")?;
+        let mut file = File::open(&pid_path).context("Couldn't open pid file")?;
         let mut content = String::new();
         file.read_to_string(&mut content)
             .context("Couldn't write to file")?;
diff --git a/tests/unix/environment_variables.rs b/tests/unix/environment_variables.rs
index dff911d32..bffa1c611 100644
--- a/tests/unix/environment_variables.rs
+++ b/tests/unix/environment_variables.rs
@@ -5,8 +5,8 @@ use crate::helper::*;
 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
 /// Make sure that the daemon's environment variables don't bleed into the spawned subprocesses.
 async fn test_isolated_task_environment() -> Result<()> {
-    let (settings, tempdir) = daemon_base_setup()?;
-    let mut child = standalone_daemon(tempdir.path()).await?;
+    let (settings, _tempdir) = daemon_base_setup()?;
+    let mut child = standalone_daemon(&settings.shared).await?;
 
     let shared = &settings.shared;
 
diff --git a/tests/unix/restore.rs b/tests/unix/restore.rs
index 5dfba05e4..3484ddd19 100644
--- a/tests/unix/restore.rs
+++ b/tests/unix/restore.rs
@@ -14,8 +14,8 @@ use crate::helper::*;
 /// The daemon should start in the same state as before shutdown, if no tasks are queued.
 /// This function tests for the running state.
 async fn test_start_running() -> Result<()> {
-    let (settings, tempdir) = daemon_base_setup()?;
-    let child = standalone_daemon(tempdir.path()).await?;
+    let (settings, _tempdir) = daemon_base_setup()?;
+    let child = standalone_daemon(&settings.shared).await?;
     let shared = &settings.shared;
 
     // Kill the daemon and wait for it to shut down.
@@ -23,7 +23,7 @@ async fn test_start_running() -> Result<()> {
     wait_for_shutdown(child.id().try_into()?)?;
 
     // Boot it up again
-    let mut child = standalone_daemon(tempdir.path()).await?;
+    let mut child = standalone_daemon(&settings.shared).await?;
 
     // Assert that the group is still running.
     let state = get_state(shared).await?;
@@ -40,8 +40,8 @@ async fn test_start_running() -> Result<()> {
 /// The daemon should start in the same state as before shutdown, if no tasks are queued.
 /// This function tests for the paused state.
 async fn test_start_paused() -> Result<()> {
-    let (settings, tempdir) = daemon_base_setup()?;
-    let child = standalone_daemon(tempdir.path()).await?;
+    let (settings, _tempdir) = daemon_base_setup()?;
+    let child = standalone_daemon(&settings.shared).await?;
     let shared = &settings.shared;
 
     // This pauses the daemon
@@ -52,7 +52,7 @@ async fn test_start_paused() -> Result<()> {
     wait_for_shutdown(child.id().try_into()?)?;
 
     // Boot it up again
-    let mut child = standalone_daemon(tempdir.path()).await?;
+    let mut child = standalone_daemon(&settings.shared).await?;
 
     // Assert that the group is still paused.
     let state = get_state(shared).await?;
@@ -69,7 +69,7 @@ async fn test_start_paused() -> Result<()> {
 /// The daemon will load new settings, when restoring a previous state.
 async fn test_load_config() -> Result<()> {
     let (mut settings, tempdir) = daemon_base_setup()?;
-    let child = standalone_daemon(tempdir.path()).await?;
+    let child = standalone_daemon(&settings.shared).await?;
 
     // Kill the daemon and wait for it to shut down.
     assert_success(shutdown_daemon(&settings.shared).await?);
@@ -81,7 +81,7 @@ async fn test_load_config() -> Result<()> {
     settings.shared.daemon_key = Some(PathBuf::from("/tmp/daemon.key"));
     settings.save(&Some(tempdir.path().join("pueue.yml")))?;
 
-    let mut child = standalone_daemon(tempdir.path()).await?;
+    let mut child = standalone_daemon(&settings.shared).await?;
 
     // Get the new state and make sure the settings actually changed.
     let state = get_state(&settings.shared).await?;
diff --git a/tests/unix/shutdown.rs b/tests/unix/shutdown.rs
index deaa4d323..13f57fbf0 100644
--- a/tests/unix/shutdown.rs
+++ b/tests/unix/shutdown.rs
@@ -9,8 +9,8 @@ use crate::helper::*;
 /// Spin up the daemon and send a SIGTERM shortly afterwards.
 /// This should trigger the graceful shutdown and kill the process.
 async fn test_ctrlc() -> Result<()> {
-    let (_, tempdir) = daemon_base_setup()?;
-    let mut child = standalone_daemon(tempdir.path()).await?;
+    let (settings, _tempdir) = daemon_base_setup()?;
+    let mut child = standalone_daemon(&settings.shared).await?;
 
     use nix::sys::signal::{kill, Signal};
     // Send SIGTERM signal to process via nix
@@ -32,8 +32,8 @@ async fn test_ctrlc() -> Result<()> {
 /// Spin up the daemon and send a graceful shutdown message afterwards.
 /// The daemon should shutdown normally and exit with a 0.
 async fn test_graceful_shutdown() -> Result<()> {
-    let (settings, tempdir) = daemon_base_setup()?;
-    let mut child = standalone_daemon(tempdir.path()).await?;
+    let (settings, _tempdir) = daemon_base_setup()?;
+    let mut child = standalone_daemon(&settings.shared).await?;
 
     // Kill the daemon gracefully and wait for it to shut down.
     assert_success(shutdown_daemon(&settings.shared).await?);