Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
- Fixed `devenv shell`/`devenv update` failing with `authentication required but no callback set` when a `url."ssh://git@github.com/".insteadOf` git config rewrites GitHub HTTPS URLs to SSH. GitHub flake inputs now resolve over SSH using your ssh-agent ([#2842](https://github.com/cachix/devenv/issues/2842)).
- Fixed the "N files" counter under "Evaluating shell" inflating from generic Nix log lines. Now only counts actual file read operations.
- Fixed `devenv up`/`test`/`tasks` failing with `error: could not find a flake.nix file` when the devenv shell is loaded from a remote flake via direnv ([#2599](https://github.com/cachix/devenv/issues/2599)).
- Fixed `devenv shell` printing internal reload warnings (e.g. "Watched path became unavailable, forcing reload") to the user's terminal, clobbering scrollback, prompts, and editors.
- Fixed `devenv shell` printing internal reload warnings (e.g. "Watched path became unavailable, forcing reload") to the user's terminal, clobbering scrollback, prompts, and editors.
- Fixed `devenv up` corrupting a running daemon's PID file and socket when started in the foreground, leaving the daemon unmanageable. Foreground `up` now rejects with "Processes already running" when a daemon is active.
- Fixed shell hook spawning a nested `devenv shell` when `devenv shell` was entered manually. Follow-up to [#2815](https://github.com/cachix/devenv/pull/2815).
- Fixed "zoxide: infinite loop detected" when using `zoxide init --cmd=cd fish` and `cd`-ing into a devenv project. The fish hook now defers spawning `devenv shell` to the next prompt instead of spawning inline inside the PWD event handler, so in-progress shell state never leaks into the devenv shell ([#2841](https://github.com/cachix/devenv/issues/2841)).
Expand All @@ -27,8 +27,8 @@
- Added `devenv down` as a shorthand for `devenv processes down`, mirroring `devenv up` ([#2862](https://github.com/cachix/devenv/issues/2862)).
- Bumped secretspec to 0.11, which adds a `[providers]` alias map in `secretspec.toml` and support for a key prefix in the AWS Secrets Manager provider.
- Added a `--include-envrc` flag to `devenv init` (also settable via `DEVENV_INCLUDE_ENVRC`) to scaffold a direnv `.envrc` file. By default `devenv init` no longer creates an `.envrc` ([#2859](https://github.com/cachix/devenv/pull/2859)).
- `processes.<name>.start.enable` (and the new global `process.start` default) now accept `"interactive-shell"`: such processes start when you enter an interactive `devenv shell` and stop when you exit it, replacing the `devenv up -d && devenv shell` two-step plus double-exit (native process manager only) ([#2863](https://github.com/cachix/devenv/issues/2863)).
- `devenv up` now attaches to an already-running process manager (started by `devenv up -d` or `devenv shell`) and starts the requested up-enabled processes over the control socket — honouring the positional process subset (e.g. `devenv up foo`) and `after`/`before` dependency ordering — instead of failing with "Processes already running" ([#2863](https://github.com/cachix/devenv/issues/2863)).
- Added `processes.<name>.start.up` and `processes.<name>.start.shell` as orthogonal per-process flags, with matching global defaults `process.start.up` (default `true`) and `process.start.shell` (default `false`). Setting `start.shell = true` on a process starts it when you enter an interactive `devenv shell` and stops it on exit — no manual `devenv up -d` / `devenv processes down` needed (native process manager only) ([#2863](https://github.com/cachix/devenv/issues/2863)).
- `devenv up` now tells you to use `--restart` when a process manager is already running, then stops and restarts it, instead of failing with an unhelpful error.
- The `devenv shell` status line now shows how many processes are running alongside the shell (e.g. `watching 5 files | 3 processes`), updating live as processes start, become ready, or exit.

### Breaking Changes
Expand Down
4 changes: 2 additions & 2 deletions devenv-processes/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -206,12 +206,12 @@ pub struct LinuxConfig {
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct StartConfig {
#[serde(default = "default_true")]
pub enable: bool,
pub up: bool,
}

impl Default for StartConfig {
fn default() -> Self {
Self { enable: true }
Self { up: true }
}
}

Expand Down
2 changes: 1 addition & 1 deletion devenv-processes/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ pub use config::{
pub use devenv_event_sources::{NotifyMessage, NotifySocket};
pub use manager::{
ApiRequest, ApiResponse, JobHandle, NativeProcessManager, PortInfo, ProcessCommand,
ProcessInfo, ProcessPhase, ProcessResources, ProcessState, UpRequest,
ProcessInfo, ProcessPhase, ProcessResources, ProcessState,
};
pub use pid::{PidStatus, check_pid_file, read_pid, remove_pid, write_pid};
pub use process_compose::ProcessComposeManager;
Expand Down
76 changes: 6 additions & 70 deletions devenv-processes/src/manager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,14 +42,8 @@ pub enum ApiRequest {
Logs { name: String, lines: Option<usize> },
/// Restart a process (or start it if not started).
Restart { name: String },
/// Start a process that has `start.enable = false`.
/// Start a process that has `start.up = false`.
Start { name: String },
/// Bring up the named processes, honouring their `after`/`before`
/// dependencies. Driven by the task scheduler that owns this manager, so
/// already-running and out-of-subset dependencies resolve against the live
/// task graph. Used by `devenv up` attaching to a running manager (the
/// client resolves the up-enabled default set before sending).
Up { names: Vec<String> },
/// Stop a running process.
Stop { name: String },
/// Query all port allocations from running processes.
Expand All @@ -70,9 +64,6 @@ pub struct ProcessInfo {
pub name: String,
pub phase: ProcessPhase,
pub restart_count: usize,
/// Configured ports, formatted as "name:port" (e.g. ["http:8080"]).
#[serde(default)]
pub ports: Vec<String>,
}

/// Response sent by the native manager API socket.
Expand Down Expand Up @@ -148,7 +139,7 @@ pub struct JobHandle {
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ProcessPhase {
/// Process has `start.enable = false`; not yet launched.
/// Process has `start.up = false`; not yet launched.
NotStarted,
/// Process was explicitly stopped by the user.
Stopped,
Expand Down Expand Up @@ -204,7 +195,7 @@ fn active_names(processes: &HashMap<String, ProcessEntry>) -> Vec<String> {

/// A managed process entry: not started, waiting for dependencies, or active.
enum ProcessEntry {
/// Process has `start.enable = false`: visible in TUI but not yet launched.
/// Process has `start.up = false`: visible in TUI but not yet launched.
NotStarted {
config: ProcessConfig,
activity: Activity,
Expand Down Expand Up @@ -237,20 +228,6 @@ pub struct NativeProcessManager {
/// clean them up on drop. Set to false for control-client instances that
/// connect to an existing daemon — they should not delete the daemon's files.
owns_runtime_files: bool,
/// Channel to the owner (the daemon's task scheduler) for servicing
/// `ApiRequest::Up`. The manager can't drive dependency ordering itself —
/// that lives in `devenv-tasks` — so it forwards the request and awaits the
/// names that were scheduled. Set once, after the manager is wrapped in an
/// `Arc`; unset on managers without an owning scheduler.
up_tx: std::sync::OnceLock<mpsc::Sender<UpRequest>>,
}

/// An `ApiRequest::Up` forwarded from the control socket to the owning task
/// scheduler: the requested process names and a reply channel for the names it
/// scheduled.
pub struct UpRequest {
pub names: Vec<String>,
pub reply: tokio::sync::oneshot::Sender<Vec<String>>,
}

/// Build a human-readable description of the readiness probe for TUI display.
Expand Down Expand Up @@ -466,17 +443,9 @@ impl NativeProcessManager {
processes_activity: Arc::new(RwLock::new(None)),
task_notify: None,
owns_runtime_files: true,
up_tx: std::sync::OnceLock::new(),
})
}

/// Set the channel used to forward `ApiRequest::Up` to the owning task
/// scheduler (the daemon). Without it, `Up` requests are rejected. Can be
/// called after the manager is wrapped in an `Arc`; ignored if already set.
pub fn set_up_handler(&self, tx: mpsc::Sender<UpRequest>) {
let _ = self.up_tx.set(tx);
}

/// Mark this instance as a control client that should not clean up
/// runtime files (socket, pid file) on drop.
pub fn set_control_client(&mut self) {
Expand Down Expand Up @@ -650,7 +619,7 @@ impl NativeProcessManager {

/// Start a command with the given configuration.
///
/// If `start.enable` is false, the process is registered as not started (visible
/// If `start.up` is false, the process is registered as not started (visible
/// in TUI as stopped but not running) and `Ok(None)` is returned.
pub async fn start_command(
&self,
Expand All @@ -673,7 +642,7 @@ impl NativeProcessManager {
config: ProcessConfig,
activity: Activity,
) -> Result<Option<Arc<Job>>> {
if !config.start.enable {
if !config.start.up {
activity.set_status(ProcessStatus::NotStarted);
info!("Registered auto start off process: {}", config.name);
self.processes.write().await.insert(
Expand Down Expand Up @@ -1339,23 +1308,10 @@ impl NativeProcessManager {
(ProcessPhase::from(status.phase), status.restart_count)
}
};
let config = match entry {
ProcessEntry::NotStarted { config, .. }
| ProcessEntry::Stopped { config, .. }
| ProcessEntry::Waiting { config, .. } => config,
ProcessEntry::Active(handle) => &handle.resources.config,
};
let mut ports: Vec<String> = config
.ports
.iter()
.map(|(n, p)| format!("{n}:{p}"))
.collect();
ports.sort();
ProcessInfo {
name: name.to_string(),
phase,
restart_count,
ports,
}
}

Expand Down Expand Up @@ -1578,26 +1534,6 @@ impl NativeProcessManager {
None => Self::process_not_found(&name),
}
}
Ok(ApiRequest::Up { names }) => match manager.up_tx.get() {
Some(up_tx) => {
let (reply, rx) = tokio::sync::oneshot::channel();
if up_tx.send(UpRequest { names, reply }).await.is_err() {
ApiResponse::Error {
message: "process scheduler is no longer running".to_string(),
}
} else {
match rx.await {
Ok(_started) => ApiResponse::Ok,
Err(_) => ApiResponse::Error {
message: "process scheduler dropped the request".to_string(),
},
}
}
}
None => ApiResponse::Error {
message: "this manager has no process scheduler to handle `up`".to_string(),
},
},
Ok(ApiRequest::Stop { name }) => match manager.stop(&name).await {
Ok(()) => ApiResponse::Ok,
Err(e) => ApiResponse::Error {
Expand Down Expand Up @@ -2097,7 +2033,7 @@ mod tests {

fn auto_start_off_config(name: &str) -> ProcessConfig {
ProcessConfig {
start: StartConfig { enable: false },
start: StartConfig { up: false },
..test_config(name)
}
}
Expand Down
19 changes: 17 additions & 2 deletions devenv-processes/src/supervisor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -385,7 +385,7 @@ pub fn spawn_supervisor(
let _ = status_tx.send(state.status());
}

_ = job.to_wait() => {
_ = job.to_wait(), if state.phase() != SupervisorPhase::Exited => {
if shutdown.is_cancelled() { break 'supervisor; }
// Extract exit status from the job
let (tx, rx) = tokio::sync::oneshot::channel();
Expand Down Expand Up @@ -433,7 +433,22 @@ pub fn spawn_supervisor(
Action::None => {
debug!("Process {} exited, not restarting", name);
let _ = status_tx.send(state.status());
break;
// When file watching is configured, keep the
// supervisor alive after a clean exit so subsequent
// file changes can re-run the process. This is what
// makes `watch` usable with one-shot commands that
// exit immediately, not just long-running ones.
// The process is reported as Exited, so `@completed`
// / `@started` dependencies are satisfied; the loop
// simply parks until a file change arrives.
if config.watch.paths.is_empty() {
break;
}
debug!(
"Process {} parked after exit; watching {} path(s) for changes",
name,
config.watch.paths.len()
);
}
}
let _ = status_tx.send(state.status());
Expand Down
14 changes: 14 additions & 0 deletions devenv-tasks/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,5 +22,19 @@ pub use ui::TasksUi;
// Re-export process types from devenv-processes
pub use devenv_processes::{ListenKind, ListenSpec, RestartPolicy, SocketActivationConfig};

/// Pre-initialize the on-disk task cache for the given cache directory.
///
/// Creates the SQLite database, switches it to WAL mode, and applies
/// migrations. Running this once before spawning the per-process
/// `devenv-tasks` invocations (which a non-native process manager launches
/// concurrently) avoids those processes racing to create and migrate the same
/// database, which could surface as a connection pool timeout (#2897).
pub async fn warm_cache(cache_dir: &std::path::Path) -> Result<(), Error> {
task_cache::TaskCache::new(cache_dir)
.await
.map(|_| ())
.map_err(|e| Error::io(format!("Failed to initialize task cache: {e}")))
}

#[cfg(test)]
mod tests;
33 changes: 33 additions & 0 deletions devenv-tasks/src/task_cache.rs
Original file line number Diff line number Diff line change
Expand Up @@ -611,6 +611,39 @@ mod tests {
assert!(result.is_ok());
}

#[sqlx::test]
async fn test_warm_cache_allows_concurrent_open() {
let temp_dir = TempDir::new().unwrap();
let cache_dir = temp_dir.path().to_path_buf();

// Warm the cache once, as `devenv up` now does before spawning the
// per-process `devenv-tasks` invocations a non-native process manager
// launches concurrently (#2897).
crate::warm_cache(&cache_dir).await.unwrap();

// The database file should now exist and be migrated.
assert!(cache_dir.join("tasks.db").exists());

// Opening the already-initialized cache from many concurrent tasks must
// all succeed: migrations are applied and the database is in WAL mode, so
// these are read-only opens with no exclusive-lock contention.
let mut handles = Vec::new();
for _ in 0..16 {
let dir = cache_dir.clone();
handles.push(tokio::spawn(async move { TaskCache::new(&dir).await }));
}
for handle in handles {
let cache = handle
.await
.unwrap()
.expect("concurrent cache open should succeed after warming");
sqlx::query("SELECT 1")
.fetch_one(cache.db.pool())
.await
.unwrap();
}
}

#[sqlx::test]
async fn test_file_modification_detection() {
let db_temp_dir = TempDir::new().unwrap();
Expand Down
2 changes: 1 addition & 1 deletion devenv-tasks/src/task_state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ impl OutputCallback for ActivityCallback<'_> {

/// Info returned from `run_process` about how the process was launched.
pub struct ProcessLaunchInfo {
/// Whether the process has auto start off (start.enable = false).
/// Whether the process has auto start off (start.up = false).
pub auto_start_off: bool,
/// Whether the process has a readiness probe that must be awaited.
pub requires_ready_wait: bool,
Expand Down
Loading