feat(maestro): default daemon TMUX_TMPDIR to $XDG_RUNTIME_DIR (NAP-234)
ci / rust (push) Failing after 2m13s
ci / rust (push) Failing after 2m13s
Plain `tmux ls` in the user's shell now sees maestro-managed sessions without the `TMUX_TMPDIR=/tmp` workaround, because most shells already set `TMUX_TMPDIR=$XDG_RUNTIME_DIR` and the daemon now matches. Precedence: env `MAESTRO_DAEMON_TMUX_TMPDIR` > `[daemon].tmux_tmpdir` > `dirs::runtime_dir()` > None (tmux's own `/tmp` default). Set `tmux_tmpdir = "/tmp"` in config.toml if the daemon must outlive the user's login without linger enabled. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -146,6 +146,14 @@ pub struct DaemonSection {
|
||||
/// `None` ⇒ daemon default (~64 KB); `Some(0)` disables replay.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub pty_replay_bytes: Option<usize>,
|
||||
/// `TMUX_TMPDIR` the daemon exports for every `tmux` invocation (NAP-234).
|
||||
/// When unset, the daemon defaults to `$XDG_RUNTIME_DIR` so that bare
|
||||
/// `tmux ls` in a user shell (which usually has the same env var) sees
|
||||
/// maestro sessions. Set to `/tmp` to opt back into tmux's own default —
|
||||
/// useful if the daemon outlives the user's login session and you can't
|
||||
/// enable `loginctl enable-linger`.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub tmux_tmpdir: Option<PathBuf>,
|
||||
/// `[daemon.policy]` — per-daemon attach policy (NAP-247).
|
||||
#[serde(default, skip_serializing_if = "DaemonPolicySection::is_empty")]
|
||||
pub policy: DaemonPolicySection,
|
||||
@@ -162,6 +170,7 @@ impl DaemonSection {
|
||||
&& self.perimeter_id.is_none()
|
||||
&& self.private_addr.is_none()
|
||||
&& self.pty_replay_bytes.is_none()
|
||||
&& self.tmux_tmpdir.is_none()
|
||||
&& self.policy.is_empty()
|
||||
}
|
||||
}
|
||||
@@ -338,6 +347,7 @@ mod tests {
|
||||
perimeter_id: Some("wg-naps62".into()),
|
||||
private_addr: Some("10.0.0.7:7070".into()),
|
||||
pty_replay_bytes: Some(131_072),
|
||||
tmux_tmpdir: Some(PathBuf::from("/run/user/1000")),
|
||||
policy: DaemonPolicySection {
|
||||
allow_inbound: vec!["konishi".into()],
|
||||
},
|
||||
|
||||
@@ -39,6 +39,8 @@ pub const ENV_DAEMON_BIND_ADDR: &str = "MAESTRO_DAEMON_BIND_ADDR";
|
||||
pub const ENV_DAEMON_PERIMETER_ID: &str = "MAESTRO_DAEMON_PERIMETER_ID";
|
||||
/// Env var that overrides `[daemon].private_addr`.
|
||||
pub const ENV_DAEMON_PRIVATE_ADDR: &str = "MAESTRO_DAEMON_PRIVATE_ADDR";
|
||||
/// Env var that overrides `[daemon].tmux_tmpdir`.
|
||||
pub const ENV_DAEMON_TMUX_TMPDIR: &str = "MAESTRO_DAEMON_TMUX_TMPDIR";
|
||||
|
||||
fn default_bind_addr() -> SocketAddr {
|
||||
DEFAULT_BIND_ADDR.parse().expect("valid default bind_addr")
|
||||
@@ -90,6 +92,11 @@ pub struct DaemonConfig {
|
||||
/// receives this much of the prior PTY output before the live stream.
|
||||
/// Default `pty::DEFAULT_REPLAY_BYTES`; `0` disables.
|
||||
pub pty_replay_bytes: usize,
|
||||
/// `TMUX_TMPDIR` exported for every `tmux` invocation (NAP-234).
|
||||
/// `None` lets tmux pick its own default (`/tmp/tmux-$UID`); `Some(p)`
|
||||
/// forces sockets under `p` so bare `tmux ls` in a matching user shell
|
||||
/// sees maestro sessions.
|
||||
pub tmux_tmpdir: Option<PathBuf>,
|
||||
}
|
||||
|
||||
impl DaemonConfig {
|
||||
@@ -113,6 +120,7 @@ pub struct EnvOverrides {
|
||||
pub token: Option<String>,
|
||||
pub perimeter_id: Option<String>,
|
||||
pub private_addr: Option<String>,
|
||||
pub tmux_tmpdir: Option<String>,
|
||||
}
|
||||
|
||||
impl EnvOverrides {
|
||||
@@ -126,6 +134,7 @@ impl EnvOverrides {
|
||||
token: std::env::var(ENV_TOKEN).ok(),
|
||||
perimeter_id: std::env::var(ENV_DAEMON_PERIMETER_ID).ok(),
|
||||
private_addr: std::env::var(ENV_DAEMON_PRIVATE_ADDR).ok(),
|
||||
tmux_tmpdir: std::env::var(ENV_DAEMON_TMUX_TMPDIR).ok(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -205,6 +214,11 @@ pub fn load_config(path: &Path, env: EnvOverrides) -> Result<DaemonConfig, Confi
|
||||
let pty_replay_bytes = daemon
|
||||
.pty_replay_bytes
|
||||
.unwrap_or(crate::pty::DEFAULT_REPLAY_BYTES);
|
||||
let tmux_tmpdir = env
|
||||
.tmux_tmpdir
|
||||
.map(PathBuf::from)
|
||||
.or(daemon.tmux_tmpdir)
|
||||
.or_else(dirs::runtime_dir);
|
||||
|
||||
Ok(DaemonConfig {
|
||||
daemon_id,
|
||||
@@ -219,6 +233,7 @@ pub fn load_config(path: &Path, env: EnvOverrides) -> Result<DaemonConfig, Confi
|
||||
private_addr,
|
||||
allow_inbound,
|
||||
pty_replay_bytes,
|
||||
tmux_tmpdir,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -477,4 +492,67 @@ private_addr = "10.0.0.7:7070"
|
||||
let h = system_hostname();
|
||||
assert!(!h.is_empty(), "gethostname returned empty string");
|
||||
}
|
||||
|
||||
// NAP-234: tmux_tmpdir precedence is env > file > dirs::runtime_dir().
|
||||
// Tested without mutating the process env (which races other tests) by
|
||||
// feeding `EnvOverrides` directly.
|
||||
|
||||
#[test]
|
||||
fn tmux_tmpdir_env_beats_file_and_runtime_dir() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let path = dir.path().join("config.toml");
|
||||
std::fs::write(
|
||||
&path,
|
||||
r#"
|
||||
[shared]
|
||||
registry_url = "http://r"
|
||||
token = "t"
|
||||
|
||||
[daemon]
|
||||
daemon_url = "ws://x"
|
||||
tmux_tmpdir = "/from-file"
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
let env = EnvOverrides {
|
||||
tmux_tmpdir: Some("/from-env".into()),
|
||||
..EnvOverrides::default()
|
||||
};
|
||||
let cfg = load_config(&path, env).unwrap();
|
||||
assert_eq!(cfg.tmux_tmpdir.as_deref(), Some(Path::new("/from-env")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tmux_tmpdir_file_beats_runtime_dir() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let path = dir.path().join("config.toml");
|
||||
std::fs::write(
|
||||
&path,
|
||||
r#"
|
||||
[shared]
|
||||
registry_url = "http://r"
|
||||
token = "t"
|
||||
|
||||
[daemon]
|
||||
daemon_url = "ws://x"
|
||||
tmux_tmpdir = "/from-file"
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
let cfg = load_config(&path, EnvOverrides::default()).unwrap();
|
||||
assert_eq!(cfg.tmux_tmpdir.as_deref(), Some(Path::new("/from-file")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tmux_tmpdir_defaults_to_xdg_runtime_dir_when_unset() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let path = dir.path().join("config.toml");
|
||||
std::fs::write(&path, minimal_file()).unwrap();
|
||||
let cfg = load_config(&path, EnvOverrides::default()).unwrap();
|
||||
// dirs::runtime_dir() is `Some(_)` on any Linux host with
|
||||
// $XDG_RUNTIME_DIR set (CI included). When it's `None`, the daemon
|
||||
// falls through to None and tmux uses /tmp; either is acceptable —
|
||||
// but on this test runner the env var is always present.
|
||||
assert_eq!(cfg.tmux_tmpdir, dirs::runtime_dir());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -131,6 +131,9 @@ pub fn run(
|
||||
// None ⇒ daemon falls back to pty::DEFAULT_REPLAY_BYTES. Override in
|
||||
// config.toml as `[daemon] pty_replay_bytes = ...` (NAP-229).
|
||||
pty_replay_bytes: None,
|
||||
// None ⇒ daemon resolves to $XDG_RUNTIME_DIR at startup. Override in
|
||||
// config.toml as `[daemon] tmux_tmpdir = "/tmp"` (NAP-234).
|
||||
tmux_tmpdir: None,
|
||||
};
|
||||
file.save(path).map_err(ConfigError::from)?;
|
||||
let _ = handle; // stdout handle only used during prompts; binary prints "wrote ..."
|
||||
|
||||
@@ -120,7 +120,10 @@ async fn run(config_override: Option<PathBuf>) -> Result<(), Box<dyn Error>> {
|
||||
|
||||
let sidecar = SidecarStore::new(config.sidecar_dir());
|
||||
let worktree_mgr = WorktreeManager::new(config.worktree_root.clone());
|
||||
let tmux = Tmux::new();
|
||||
let tmux = match config.tmux_tmpdir.clone() {
|
||||
Some(p) => Tmux::new().with_tmpdir(p),
|
||||
None => Tmux::new(),
|
||||
};
|
||||
let manager = SessionManager::new_with_replay(
|
||||
config.daemon_id.clone(),
|
||||
tmux,
|
||||
|
||||
@@ -109,6 +109,23 @@ where you want browser access answer yes — both binaries end up in
|
||||
| `--web-exec-start <path>` | Override the web unit's `ExecStart` (default `~/.cargo/bin/maestro-web`). |
|
||||
| `-y`, `--yes` | Non-interactive: install binaries + drop systemd unit + reload + enable, but **don't** auto-run `configure` or auto-start the unit (those need real config). Also skips the web step unless an existing `maestro-web.service` is being refreshed. |
|
||||
|
||||
## Where tmux sockets land
|
||||
|
||||
`maestrod` defaults `TMUX_TMPDIR` to `$XDG_RUNTIME_DIR` (= `/run/user/$UID`)
|
||||
so a plain `tmux ls` in your shell — which typically uses the same env var
|
||||
— sees maestro-managed sessions. Override in
|
||||
`~/.config/maestro/config.toml`:
|
||||
|
||||
```toml
|
||||
[daemon]
|
||||
tmux_tmpdir = "/tmp"
|
||||
```
|
||||
|
||||
Pick `/tmp` if the daemon must outlive your login session and you can't run
|
||||
`loginctl enable-linger` (the runtime dir is wiped on logout, taking the
|
||||
tmux sockets with it). The env var `MAESTRO_DAEMON_TMUX_TMPDIR` takes
|
||||
precedence over the file.
|
||||
|
||||
## What the systemd unit looks like
|
||||
|
||||
The wizard writes a *user* unit (`systemctl --user`), since `maestrod` runs
|
||||
|
||||
Reference in New Issue
Block a user