Speed up Studio startup path (#6899)
* Speed up Studio startup path * Studio: recheck managed binary executability on preflight cache hit and ignore stale unauthenticated platform fetches Preflight: a matching capability cache fingerprint no longer skips the runnability check when the managed binary's executable bit was cleared (size and mtime unchanged, since chmod bumps ctime not mtime). The cache fast path now confirms the binary is still executable, otherwise it falls back to the CLI help probe so preflight reports Stale and can repair, instead of returning Ready and failing later at backend start. Adds a regression test. Frontend: now that first render is no longer gated on fetchDeviceType, the initial unauthenticated health call can resolve after an authenticated platform fetch. Guard the store so a late unauthenticated or failed non-forced response cannot overwrite an already authoritative device type, tunnel URL, or secure flag. Forced refreshes and the first unauthenticated load are unaffected. * Studio: use access(X_OK) for the preflight cache executability guard A mode bitmask treats any execute bit as launchable, but the executable bits can be set only for another owner or group, or be denied by an ACL, so the current user could still hit PermissionDenied at launch and the cached fast path would wrongly return Ready. access(X_OK) checks real executability for the calling user, so an ownership or permission change correctly falls back to the CLI help probe and the Stale repair path. * Studio: ignore any stale non-forced platform fetch once authoritative Extend the platform store guard so a non-forced health response never overwrites an already authoritative result, not only unauthenticated ones. With a saved token the post-render non-forced request can be authenticated but older than a later forced refresh that already picked up the tunnel URL and secure flag; if that earlier request resolves last it would null those fields. Now any non-forced response is dropped once the store holds a server-reported platform. Forced refreshes and the first authoritative write are unaffected. * Studio: run the managed CLI help probe before trusting the preflight cache Restore running the managed CLI help probe before returning Ready from the desktop capability cache, so a managed install whose venv interpreter or a runtime dependency is broken (while path, size, mtime, and markers are unchanged) is reported Stale for repair rather than proceeding to a backend start that cannot spawn. The capability cache still skips the heavier desktop-capabilities probe on a hit, so a warm cache runs one probe instead of two. Removes the executable-access shortcut, which the help probe now subsumes. --------- Co-authored-by: Daniel Han <danielhanchen@gmail.com>
This commit is contained in:
parent
01b8085dc2
commit
49d1fb3863
10 changed files with 308 additions and 44 deletions
|
|
@ -498,19 +498,68 @@ mod tests {
|
|||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
fn remove_managed_capability_cache() {
|
||||
let _ = std::fs::remove_file(
|
||||
dirs::home_dir()
|
||||
static MANAGED_CAPABILITY_CACHE_TEST_LOCK: std::sync::LazyLock<tokio::sync::Mutex<()>> =
|
||||
std::sync::LazyLock::new(|| tokio::sync::Mutex::new(()));
|
||||
|
||||
#[cfg(unix)]
|
||||
struct ManagedCapabilityCacheHome {
|
||||
path: PathBuf,
|
||||
previous: Option<std::ffi::OsString>,
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
impl ManagedCapabilityCacheHome {
|
||||
fn new(test_name: &str) -> Self {
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
let nanos = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.join(".unsloth")
|
||||
.join("studio")
|
||||
.join("desktop_capability_cache.json"),
|
||||
);
|
||||
.as_nanos();
|
||||
let path = std::env::temp_dir().join(format!(
|
||||
"unsloth-preflight-cache-{test_name}-{}-{nanos}",
|
||||
std::process::id()
|
||||
));
|
||||
std::fs::create_dir_all(&path).unwrap();
|
||||
let previous = std::env::var_os("UNSLOTH_TEST_DESKTOP_CAPABILITY_CACHE_HOME");
|
||||
std::env::set_var("UNSLOTH_TEST_DESKTOP_CAPABILITY_CACHE_HOME", &path);
|
||||
Self { path, previous }
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
impl Drop for ManagedCapabilityCacheHome {
|
||||
fn drop(&mut self) {
|
||||
if let Some(previous) = &self.previous {
|
||||
std::env::set_var("UNSLOTH_TEST_DESKTOP_CAPABILITY_CACHE_HOME", previous);
|
||||
} else {
|
||||
std::env::remove_var("UNSLOTH_TEST_DESKTOP_CAPABILITY_CACHE_HOME");
|
||||
}
|
||||
let _ = std::fs::remove_dir_all(&self.path);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
fn managed_capability_cache_path_for_test() -> PathBuf {
|
||||
std::env::var_os("UNSLOTH_TEST_DESKTOP_CAPABILITY_CACHE_HOME")
|
||||
.map(PathBuf::from)
|
||||
.or_else(dirs::home_dir)
|
||||
.unwrap()
|
||||
.join(".unsloth")
|
||||
.join("studio")
|
||||
.join("desktop_capability_cache.json")
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
fn remove_managed_capability_cache() {
|
||||
let _ = std::fs::remove_file(managed_capability_cache_path_for_test());
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[tokio::test]
|
||||
async fn managed_cli_capability_probe_classifies_core_cases() {
|
||||
let _cache_guard = MANAGED_CAPABILITY_CACHE_TEST_LOCK.lock().await;
|
||||
let _cache_home = ManagedCapabilityCacheHome::new("core-cases");
|
||||
remove_managed_capability_cache();
|
||||
|
||||
for (name, script, stale_reason) in [
|
||||
|
|
@ -567,6 +616,75 @@ exit 1
|
|||
}
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[tokio::test]
|
||||
async fn managed_cli_capability_help_probe_runs_before_cache() {
|
||||
use std::fs;
|
||||
|
||||
let _cache_guard = MANAGED_CAPABILITY_CACHE_TEST_LOCK.lock().await;
|
||||
let _cache_home = ManagedCapabilityCacheHome::new("cache-hit");
|
||||
|
||||
remove_managed_capability_cache();
|
||||
// `-h` always succeeds unless `modeh` exists; the desktop-capabilities
|
||||
// probe always succeeds unless `modecap` exists. Toggling those lets us
|
||||
// prove the ordering: -h runs on every probe (even a cache hit), while
|
||||
// the heavier capability probe is skipped once the cache is warm.
|
||||
let fake = fake_cli(
|
||||
"cap-cache-hit",
|
||||
r#"#!/bin/sh
|
||||
log="$0.calls"
|
||||
modeh="$0.modeh"
|
||||
modecap="$0.modecap"
|
||||
printf '%s\n' "$*" >> "$log"
|
||||
if [ "$1" = "-h" ]; then
|
||||
if [ -f "$modeh" ]; then exit 42; fi
|
||||
exit 0
|
||||
fi
|
||||
if [ "$1" = "studio" ] && [ "$2" = "desktop-capabilities" ] && [ "$3" = "--json" ]; then
|
||||
if [ -f "$modecap" ]; then exit 42; fi
|
||||
printf '{"desktop_protocol_version":1,"desktop_manageability_version":1,"supports_api_only":true,"supports_provision_desktop_auth":true,"supports_desktop_backend_ownership":true,"version":"2026.5.3"}'
|
||||
exit 0
|
||||
fi
|
||||
exit 1
|
||||
"#,
|
||||
);
|
||||
let bin = fake.bin.clone();
|
||||
let calls = bin.with_extension("calls");
|
||||
let modeh = bin.with_extension("modeh");
|
||||
let modecap = bin.with_extension("modecap");
|
||||
|
||||
// Cold probe: runs -h and the capability probe, then caches the result.
|
||||
assert!(matches!(
|
||||
probe_managed_bin(bin.clone()).await,
|
||||
ManagedProbe::Ready { .. }
|
||||
));
|
||||
let first_calls = fs::read_to_string(&calls).unwrap();
|
||||
assert!(first_calls.contains("-h"));
|
||||
assert!(first_calls.contains("studio desktop-capabilities --json"));
|
||||
|
||||
// Cache hit: -h still runs, but the capability probe is skipped (breaking
|
||||
// it via `modecap` proves it is not invoked).
|
||||
fs::write(&modecap, "broken").unwrap();
|
||||
fs::write(&calls, "").unwrap();
|
||||
assert!(matches!(
|
||||
probe_managed_bin(bin.clone()).await,
|
||||
ManagedProbe::Ready { .. }
|
||||
));
|
||||
assert_eq!(fs::read_to_string(&calls).unwrap(), "-h\n");
|
||||
|
||||
// A non-launchable CLI is caught by the -h probe even with a warm cache:
|
||||
// preflight reports Stale (for repair) and never trusts the cache.
|
||||
fs::write(&modeh, "broken").unwrap();
|
||||
fs::write(&calls, "").unwrap();
|
||||
assert!(matches!(
|
||||
probe_managed_bin(bin).await,
|
||||
ManagedProbe::Stale { .. }
|
||||
));
|
||||
assert_eq!(fs::read_to_string(&calls).unwrap(), "-h\n");
|
||||
|
||||
remove_managed_capability_cache();
|
||||
}
|
||||
|
||||
const EXPECTED_ROOT_ID: &str =
|
||||
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
|
||||
const OTHER_ROOT_ID: &str = "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb";
|
||||
|
|
|
|||
|
|
@ -188,6 +188,16 @@ fn managed_bin_fingerprint(bin: &Path) -> Option<ManagedBinFingerprint> {
|
|||
}
|
||||
|
||||
fn capability_cache_path() -> Option<PathBuf> {
|
||||
#[cfg(test)]
|
||||
if let Some(home) = std::env::var_os("UNSLOTH_TEST_DESKTOP_CAPABILITY_CACHE_HOME") {
|
||||
return Some(
|
||||
PathBuf::from(home)
|
||||
.join(".unsloth")
|
||||
.join("studio")
|
||||
.join("desktop_capability_cache.json"),
|
||||
);
|
||||
}
|
||||
|
||||
dirs::home_dir().map(|home| {
|
||||
home.join(".unsloth")
|
||||
.join("studio")
|
||||
|
|
@ -400,6 +410,12 @@ fn desktop_capability_ready(capability: &DesktopCapability) -> bool {
|
|||
|
||||
pub(super) async fn probe_managed_bin(bin: PathBuf) -> ManagedProbe {
|
||||
let started = Instant::now();
|
||||
// Always verify the managed CLI actually launches before trusting the cache.
|
||||
// A matching capability fingerprint does not prove the binary can still run:
|
||||
// its venv interpreter or a runtime dependency can be broken while the
|
||||
// path/size/mtime/markers are unchanged, so the -h probe runs first and a
|
||||
// non-launchable install is reported Stale for repair. The capability cache
|
||||
// below still skips the heavier desktop-capabilities probe on a hit.
|
||||
if !run_cli_probe(&bin, &["-h"]).await {
|
||||
info!(
|
||||
"Managed preflight: cli unusable for {:?} in {}ms",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue