Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
75 commits
Select commit Hold shift + click to select a range
e2015fb
fix(uninstall): stop managed services before removing the tooling tha…
fredespi Aug 21, 2026
1f9ca82
fix(uninstall): pin the stop-before-remove ordering with tests and ve…
fredespi Sep 9, 2026
20364b6
Merge remote-tracking branch 'origin/main' into rocm-uninstall-report…
fredespi Sep 10, 2026
6b697a3
test(e2e): index the new lifecycle scenario per the naming convention
fredespi Sep 10, 2026
451a8ee
fix(uninstall): stop judging stale service records by their recorded …
fredespi Sep 10, 2026
911a399
fix(uninstall): one atomic-write helper, a resolvable probe host, an …
fredespi Sep 10, 2026
1f4d1a9
fix(uninstall): keep the gate honest across a retry, and stop the dae…
fredespi Sep 10, 2026
4db3a7f
fix(uninstall): gate the test-only port constant to the platform that…
fredespi Sep 10, 2026
4fa47a8
fix(uninstall): never signal a daemon pid it cannot prove is rocmd
fredespi Sep 11, 2026
bdf3661
test(rocm-core): cover the atomic write helper directly
fredespi Sep 11, 2026
89f69c5
fix(uninstall): treat an unrecorded daemon identity as unverified, no…
fredespi Sep 11, 2026
4c9a81a
fix(uninstall): probe a wildcard-bound endpoint at the address it ans…
fredespi Sep 11, 2026
074b37b
test(rocm-core): pin atomic publishing at the service-record level
fredespi Sep 11, 2026
a908893
fix(uninstall): make the daemon-state abort followable, and pin the r…
fredespi Sep 11, 2026
08d5ddd
docs: say what the local atomic-write helpers share, and what they do…
fredespi Sep 11, 2026
746b81f
fix(uninstall): classify the daemon pid from a single start-time reading
fredespi Sep 14, 2026
3cc2f4d
docs(uninstall): say what each remediation test pins, and name the ga…
fredespi Sep 14, 2026
24dbb5a
fix(uninstall): probe both loopback families, honor a stopped daemon,…
fredespi Sep 14, 2026
9e2dd91
fix(uninstall): answer the platform question with a process known to …
fredespi Sep 14, 2026
1cd5c95
style(uninstall): satisfy rustfmt on the two lines the last push missed
fredespi Sep 14, 2026
d6f97c5
test(rocmd): pin the start-time capture the uninstall guard depends on
fredespi Sep 14, 2026
541db48
fix(uninstall): stop nothing on a doomed run, and stop guessing from …
fredespi Sep 14, 2026
50cff02
fix(uninstall): restore the cfg gate my helper insertion displaced
fredespi Sep 14, 2026
64d5a20
fix(uninstall): pin the platform discriminator, and read before destr…
fredespi Sep 14, 2026
a3bc596
test(uninstall): pin the platform conjunct on the only lane that can …
fredespi Sep 14, 2026
5a21760
fix(uninstall): check before destroying, and make each gate outcome a…
fredespi Sep 14, 2026
617c17f
test(uninstall): drive the stranger-on-a-recycled-port arm end to end
fredespi Sep 15, 2026
0907d82
fix(uninstall): disclose the third fail-open, and pin the other two
fredespi Sep 15, 2026
e9bd2af
fix(uninstall): make every failure class prove it carries a way out
fredespi Sep 15, 2026
5202140
fix(uninstall): make the remedy-order assertion able to fail, and stop
fredespi Sep 15, 2026
f323e7f
test(uninstall): pin the split failure reason, and bound the Linux-on…
fredespi Sep 15, 2026
64e8914
Merge origin/main into rocm-uninstall-reports-success-while-leaving-a
fredespi Oct 2, 2026
63b8f69
fix(uninstall): scenario for the refusal path, docs, plan-warning tes…
fredespi Oct 2, 2026
b7423d5
fix(rocmd): add daemon_start_ticks to the relocated audit-log test
fredespi Oct 2, 2026
22283cd
fix(uninstall): order lifecycle-25 after 24, gate the new plan-warnin…
fredespi Oct 2, 2026
bb460ef
Merge remote-tracking branch 'origin/main' into rocm-uninstall-report…
fredespi Oct 5, 2026
27e1092
refactor(uninstall): extract the stop gate into uninstall_gate.rs, ad…
fredespi Oct 5, 2026
5450c37
fix(uninstall): record a Windows process start-time so the daemon kil…
fredespi Oct 5, 2026
b062582
test(uninstall): Windows now reads start-times, so the bare-record te…
fredespi Oct 5, 2026
c8bdf89
Merge origin/main into rocm-uninstall-reports-success-while-leaving-a
fredespi Oct 7, 2026
9606c37
test(uninstall): pin the daemon-stop failure, per-class advice dedup,…
fredespi Oct 7, 2026
ffecf1d
Merge origin/main into rocm-uninstall-reports-success-while-leaving-a
fredespi Oct 8, 2026
1edde9c
docs(uninstall): correct comments that say only Linux can read a star…
fredespi Oct 8, 2026
824a3eb
fix(uninstall): probe already-stopped records before stopping anythin…
fredespi Oct 8, 2026
43fbb58
Merge origin/main; fix port-reuse false abort, public-bind advice, pl…
fredespi Oct 9, 2026
e0ae769
fix(uninstall): match hosts that name one listener, own remedy for an…
fredespi Oct 9, 2026
7720885
fix(uninstall): keep the read-error test off the real binaries, drop …
fredespi Oct 9, 2026
cbb5d1f
fix(uninstall): remove-record advice for a stranger on an old port, d…
fredespi Oct 9, 2026
6555f0e
fix(uninstall): refuse live tick-less service pids, one advice per cl…
fredespi Oct 12, 2026
d1e2aae
test(e2e): plant the supervisor start time in the lifecycle managed-s…
fredespi Oct 12, 2026
e35dd14
fix(uninstall): vLLM engine ticks, tick-less advice that works, Windo…
fredespi Oct 12, 2026
0ad0b6b
Merge origin/main
fredespi Oct 12, 2026
6c48991
fix(core): split the Windows start-time doc summary for clippy
fredespi Oct 12, 2026
a361d94
fix(uninstall): record the supervisor start time with its pid so a li…
fredespi Oct 13, 2026
b472ef6
test(e2e): pin the background-helper stop and the cache-only exemptio…
fredespi Oct 13, 2026
c27bd89
Merge origin/main
fredespi Oct 13, 2026
777fe0c
test(e2e): fill the new ServiceRecordOptions fields from main in the …
fredespi Oct 13, 2026
df51a02
fix(ci): import OsStrExt for the Windows ReplaceFileW path, flatten t…
fredespi Oct 14, 2026
3124f2e
Merge origin/main
fredespi Oct 14, 2026
8ec977c
test(uninstall): make the wildcard-host test port-independent, pin th…
fredespi Oct 14, 2026
4624138
test(uninstall): pin the 403 arm, the stored key reaching the identit…
fredespi Oct 14, 2026
97ea46e
fix(serve): name a runnable relaunch command in the lost-key refusals
fredespi Oct 14, 2026
7e1eb0b
docs(uninstall): state the helper force-stop and the tick-less refusa…
fredespi Oct 14, 2026
01da6f3
fix(uninstall): a truthful plan, a partial-removal report, and one pi…
fredespi Oct 14, 2026
82ded68
Merge origin/main into rocm-uninstall-reports-success-while-leaving-a
fredespi Oct 14, 2026
4541c89
fix(uninstall): report a failed removal as potentially partial
fredespi Oct 14, 2026
14f17e6
fix(uninstall): one shared non-destructive pre-flight for the plan an…
fredespi Oct 15, 2026
3168314
test(uninstall): lifecycle scenarios for a tickless server and unread…
fredespi Oct 15, 2026
32fc519
chore(uninstall): truthful comments and flags, helper identity reason…
fredespi Oct 15, 2026
3932470
Merge origin/main into rocm-uninstall-reports-success-while-leaving-a
fredespi Oct 15, 2026
aab76d1
fix(uninstall): gate the Unix-only test helpers so the Windows test b…
fredespi Oct 15, 2026
9c40b3b
fix(uninstall): address review of aab76d10
fredespi Oct 15, 2026
6273d3d
test(uninstall): pin the partial-removal note, the key drop, and the …
fredespi Oct 15, 2026
c6e3107
Merge origin/main into rocm-uninstall-reports-success-while-leaving-a
fredespi Oct 15, 2026
f88d72c
Merge origin/main into rocm-uninstall-reports-success-while-leaving-a
fredespi Oct 15, 2026
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
2 changes: 0 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

19 changes: 19 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -970,6 +970,25 @@ first: if it cannot be, nothing is removed, so you can fix the cause and run
`rocm uninstall` again. Each step is reported as it finishes, so if a later
step fails, the output still shows what was already removed.

`rocm uninstall` stops any managed model server that is still running before it
removes anything, and also stops the background helper (`rocmd`), which is
force-stopped (on Linux its whole process tree, on Windows only the recorded
process) because it would otherwise restart the servers.
If a server cannot be stopped (or its service record cannot be read), the
command exits non-zero, leaves every file in place, and says what to repair or
stop by hand, so the tooling needed to stop it is never deleted out from under
a running server. A live server or helper recorded without a start time (written
by an older version) cannot be proven to be ours, so it is refused rather than
killed: stop it yourself, then re-run. The same refusal applies when the
helper's runtime state file cannot be read. The plan shows the refusal before
you confirm it. The stop pass is not atomic: if it aborts after stopping some
servers (or the helper), those stay stopped and their endpoint keys are gone;
the error names each one and the `rocm serve` command that brings it back. A
run that keeps the binaries and data (`--keep-binaries --keep-data`) leaves
running servers and the helper alone.
If a removal fails partway, the error lists what was removed and what is left,
because the binaries and service records may already be gone.

On Linux, if a config, data, or cache folder is a link to somewhere else (for
example a cache moved to a bigger disk), `rocm uninstall` removes the link and
leaves the folder it points to alone, whether or not the setting ends in a `/`.
Expand Down
1 change: 0 additions & 1 deletion apps/rocm/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,6 @@ libc.workspace = true
[target.'cfg(windows)'.dependencies]
self-replace = "1.5"
windows-native-keyring-store = "1.1"
windows-sys = { version = "0.61", features = ["Win32_Storage_FileSystem"] }

[target.'cfg(target_os = "macos")'.dependencies]
apple-native-keyring-store = { version = "1.0", features = ["keychain"] }
Expand Down
4,449 changes: 4,263 additions & 186 deletions apps/rocm/src/main.rs

Large diffs are not rendered by default.

41 changes: 31 additions & 10 deletions apps/rocm/src/service_launch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -199,18 +199,18 @@ pub(crate) fn ensure_public_service_has_endpoint_key(
"managed service was launched with `--require-api-key` but has no endpoint API key, \
so restarting it would reopen it without authentication. Something outside this \
machine may still be publishing its port. The key is dropped when a service stops \
and cannot be recovered. Launch it again with \
`rocm serve --require-api-key` (add `--api-key <key>`, or set ROCM_SERVE_API_KEY, \
to choose the key instead of generating one)."
and cannot be recovered. Launch it again with `{}` {}.",
rocm_engine_protocol::relaunch_command(host, true),
rocm_engine_protocol::RELAUNCH_KEY_CHOICE
);
}
if rocm_engine_protocol::is_public_bind_host(host) && !key_present {
bail!(
"managed service is bound to the public host `{host}` but has no endpoint API key, \
so restarting it would reopen it without authentication. The key is dropped when a \
service stops and cannot be recovered. Launch it again with \
`rocm serve --host {host} --allow-public-bind` (add `--api-key <key>`, or set \
ROCM_SERVE_API_KEY, to choose the key instead of generating one)."
service stops and cannot be recovered. Launch it again with `{}` {}.",
rocm_engine_protocol::relaunch_command(host, false),
rocm_engine_protocol::RELAUNCH_KEY_CHOICE
);
}
Ok(())
Expand Down Expand Up @@ -1149,7 +1149,7 @@ fn stream_attached_logs_no_tty(log_path: &Path, child_pid: u32) -> Result<Attach
}

#[cfg(test)]
mod tests {
pub(crate) mod tests {
use super::*;
use crate::tests::test_paths;

Expand Down Expand Up @@ -1577,9 +1577,26 @@ mod tests {
// And the loopback-with-forced-auth case still reports its own reason.
let error = ensure_public_service_has_endpoint_key("127.0.0.1", false, true)
.expect_err("a service that demanded auth must not come back without it");
let rendered = format!("{error:#}");
assert!(rendered.contains("--require-api-key"), "{rendered}");
// The relaunch command has to be one that can be run: it names the model
// and port to fill in, and the recorded host, like its public-bind sibling.
assert!(
format!("{error:#}").contains("--require-api-key"),
"{error:#}"
rendered.contains("rocm serve <model> --host 127.0.0.1 --port <port>"),
"{rendered}"
);
assert!(
!rendered.contains("--allow-public-bind"),
"a loopback service is not told to open a public bind: {rendered}"
);

// A public host that also demanded auth is told how to bind publicly.
let error = ensure_public_service_has_endpoint_key("0.0.0.0", false, true)
.expect_err("a public service that demanded auth must not come back without it");
let rendered = format!("{error:#}");
assert!(
rendered.contains("--host 0.0.0.0 --port <port> --allow-public-bind --require-api-key"),
"{rendered}"
);
}

Expand Down Expand Up @@ -1641,11 +1658,15 @@ mod tests {
}

/// Build an `AutomationRuntimeState` for the no-double-spawn guard tests.
fn runtime_state(running: bool, daemon_pid: u32) -> AutomationRuntimeState {
///
/// `daemon_start_ticks` is left unrecorded; tests that care about the
/// daemon's identity set it explicitly.
pub(crate) fn runtime_state(running: bool, daemon_pid: u32) -> AutomationRuntimeState {
AutomationRuntimeState {
running,
automations_enabled: true,
daemon_pid,
daemon_start_ticks: None,
started_at_unix_ms: 1,
last_tick_unix_ms: 1,
local_webhook_endpoint: None,
Expand Down
68 changes: 15 additions & 53 deletions apps/rocm/src/therock.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4716,6 +4716,18 @@ fn temp_sibling_path(path: &Path, suffix: &OsStr) -> Result<PathBuf> {
Ok(parent.join(file_name))
}

/// Stage-and-publish a file here, sharing only the publish step with `rocm-core`.
///
/// Deliberately not [`rocm_core::write_file_atomically`], and not a copy of it
/// either: only the Windows-sensitive publish (`ReplaceFileW` and its fallbacks)
/// is single-sourced, via [`publish_temp_file`]. The staging half stays local
/// because it carries the `suffix_for_attempt` and `before_publish` seams the
/// tests below drive to force temp-name collisions, write failures and rename
/// races — injection points `rocm_core`'s caller-facing helper does not expose.
///
/// Consequence worth knowing: this path does **not** `sync_all` before
/// publishing, so unlike the `rocm-core` helper it is atomic but carries no
/// crash-durability guarantee for the staged bytes.
pub(crate) fn write_file_atomically(path: &Path, bytes: &[u8]) -> Result<()> {
let temp_id = format!("{}-{}", std::process::id(), unix_time_millis());
write_file_atomically_with(
Expand Down Expand Up @@ -4832,60 +4844,10 @@ where
.with_context(|| format!("failed to publish {}", path.display()))
}

#[cfg(not(windows))]
fn publish_temp_file(tmp: &Path, path: &Path) -> io::Result<()> {
fs::rename(tmp, path)
}

#[cfg(windows)]
/// The publish step lives in `rocm-core` so there is one implementation of the
/// Windows `ReplaceFileW` handling for the whole workspace.
fn publish_temp_file(tmp: &Path, path: &Path) -> io::Result<()> {
if path.try_exists()? {
return replace_file_windows(path, tmp);
}

match fs::rename(tmp, path) {
Ok(()) => Ok(()),
Err(rename_error) => {
if path.try_exists()? {
replace_file_windows(path, tmp)
} else {
Err(rename_error)
}
}
}
}

#[cfg(windows)]
#[allow(unsafe_code)]
fn replace_file_windows(path: &Path, replacement: &Path) -> io::Result<()> {
use std::os::windows::ffi::OsStrExt;
use windows_sys::Win32::Storage::FileSystem::ReplaceFileW;

let path_wide: Vec<u16> = path.as_os_str().encode_wide().chain(Some(0)).collect();
let replacement_wide: Vec<u16> = replacement
.as_os_str()
.encode_wide()
.chain(Some(0))
.collect();

// SAFETY: both path buffers are valid, NUL-terminated UTF-16 strings and
// remain alive for the duration of the synchronous Windows API call. The
// optional backup, exclude, and reserved pointers are intentionally null.
let replaced = unsafe {
ReplaceFileW(
path_wide.as_ptr(),
replacement_wide.as_ptr(),
std::ptr::null(),
0,
std::ptr::null(),
std::ptr::null(),
)
};
if replaced == 0 {
Err(io::Error::last_os_error())
} else {
Ok(())
}
rocm_core::publish_temp_file(tmp, path)
}

fn extract_tarball(archive_path: &Path, target_dir: &Path) -> Result<()> {
Expand Down
Loading
Loading