mirror of
https://github.com/hanzoai/dev.git
synced 2026-10-25 03:32:32 +00:00
fix(cli/windows): avoid EBUSY/EPERM on global upgrades by keeping native .exe in per-user cache; launcher prefers cache\n\n- Postinstall: on Windows, install binary to %LocalAppData%\just-every\code\<version> and do not leave a copy in node_modules.\n- Launcher: run from cache when available; mirror into node_modules only on Unix.\n- Outcome: global npm upgrades no longer fail when the CLI is running.
This commit is contained in:
+38
-14
@@ -177,9 +177,12 @@ const tryBootstrapBinary = async () => {
|
||||
if (existsSync(cachePath)) {
|
||||
const v = validateBinary(cachePath);
|
||||
if (v.ok) {
|
||||
copyFileSync(cachePath, binaryPath);
|
||||
if (platform !== "win32") chmodSync(binaryPath, 0o755);
|
||||
return existsSync(binaryPath);
|
||||
// Prefer running directly from cache; mirror into node_modules on Unix
|
||||
if (platform !== "win32") {
|
||||
copyFileSync(cachePath, binaryPath);
|
||||
try { chmodSync(binaryPath, 0o755); } catch {}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -202,11 +205,13 @@ const tryBootstrapBinary = async () => {
|
||||
const pkgDir = path.dirname(pkgJson);
|
||||
const src = path.join(pkgDir, "bin", `code-${targetTriple}${platform === "win32" ? ".exe" : ""}`);
|
||||
if (existsSync(src)) {
|
||||
copyFileSync(src, binaryPath);
|
||||
if (platform !== "win32") chmodSync(binaryPath, 0o755);
|
||||
// refresh cache
|
||||
try { copyFileSync(binaryPath, cachePath); } catch {}
|
||||
return existsSync(binaryPath);
|
||||
// Always ensure cache has the binary; on Unix mirror into node_modules
|
||||
copyFileSync(src, cachePath);
|
||||
if (platform !== "win32") {
|
||||
copyFileSync(cachePath, binaryPath);
|
||||
try { chmodSync(binaryPath, 0o755); } catch {}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
} catch { /* ignore and fall back */ }
|
||||
}
|
||||
@@ -239,10 +244,19 @@ const tryBootstrapBinary = async () => {
|
||||
try { unlinkSync(tmp); } catch {}
|
||||
}
|
||||
}
|
||||
const v = validateBinary(binaryPath);
|
||||
// On Windows, prefer cache and avoid leaving the executable in node_modules
|
||||
if (platform === "win32") {
|
||||
try {
|
||||
copyFileSync(binaryPath, cachePath);
|
||||
} catch {}
|
||||
try { unlinkSync(binaryPath); } catch {}
|
||||
} else {
|
||||
try { copyFileSync(binaryPath, cachePath); } catch {}
|
||||
}
|
||||
|
||||
const v = validateBinary(platform === "win32" ? cachePath : binaryPath);
|
||||
if (!v.ok) throw new Error(`invalid binary (${v.reason})`);
|
||||
if (platform !== "win32") chmodSync(binaryPath, 0o755);
|
||||
try { copyFileSync(binaryPath, cachePath); } catch {}
|
||||
if (platform !== "win32") try { chmodSync(binaryPath, 0o755); } catch {}
|
||||
return true;
|
||||
})
|
||||
.catch((_e) => false);
|
||||
@@ -262,9 +276,19 @@ if (!existsSync(binaryPath) && !existsSync(legacyBinaryPath)) {
|
||||
}
|
||||
}
|
||||
|
||||
// Fall back to legacy name if primary is still missing
|
||||
if (!existsSync(binaryPath) && existsSync(legacyBinaryPath)) {
|
||||
binaryPath = legacyBinaryPath;
|
||||
// Prefer cached binary when available
|
||||
try {
|
||||
const pkg = JSON.parse(readFileSync(path.join(__dirname, "..", "package.json"), "utf8"));
|
||||
const version = pkg.version;
|
||||
const cached = getCachedBinaryPath(version);
|
||||
const v = existsSync(cached) ? validateBinary(cached) : { ok: false };
|
||||
if (v.ok) {
|
||||
binaryPath = cached;
|
||||
} else if (!existsSync(binaryPath) && existsSync(legacyBinaryPath)) {
|
||||
binaryPath = legacyBinaryPath;
|
||||
}
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
|
||||
// Check if binary exists and try to fix permissions if needed
|
||||
|
||||
+44
-40
@@ -227,30 +227,23 @@ async function main() {
|
||||
const localPath = join(binDir, binaryName);
|
||||
const cachePath = getCachedBinaryPath(version, targetTriple, isWindows);
|
||||
|
||||
// Skip if already exists and has correct permissions
|
||||
if (existsSync(localPath)) {
|
||||
// Always try to fix permissions on Unix-like systems
|
||||
if (!isWindows) {
|
||||
try {
|
||||
chmodSync(localPath, 0o755);
|
||||
console.log(`✓ ${binaryName} already exists (permissions fixed)`);
|
||||
} catch (e) {
|
||||
console.log(`✓ ${binaryName} already exists`);
|
||||
}
|
||||
} else {
|
||||
console.log(`✓ ${binaryName} already exists`);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
// On Windows we avoid placing the executable inside node_modules to prevent
|
||||
// EBUSY/EPERM during global upgrades when the binary is in use.
|
||||
// We treat the user cache path as the canonical home of the native binary.
|
||||
// For macOS/Linux we keep previous behavior and also place a copy in binDir
|
||||
// for convenience.
|
||||
|
||||
// Fast path: if a valid cached binary exists for this version+triple, reuse it.
|
||||
try {
|
||||
if (existsSync(cachePath)) {
|
||||
const valid = validateDownloadedBinary(cachePath);
|
||||
if (valid.ok) {
|
||||
copyFileSync(cachePath, localPath);
|
||||
if (!isWindows) chmodSync(localPath, 0o755);
|
||||
console.log(`✓ Installed ${binaryName} from user cache`);
|
||||
if (!isWindows) {
|
||||
// Mirror into local bin for Unix-like platforms
|
||||
copyFileSync(cachePath, localPath);
|
||||
try { chmodSync(localPath, 0o755); } catch {}
|
||||
}
|
||||
console.log(`✓ ${binaryName} ready from user cache`);
|
||||
continue; // next binary
|
||||
}
|
||||
}
|
||||
@@ -288,15 +281,14 @@ async function main() {
|
||||
if (!existsSync(src)) {
|
||||
throw new Error(`platform package missing binary: ${platformPkg.name}`);
|
||||
}
|
||||
copyFileSync(src, localPath);
|
||||
if (!isWindows) chmodSync(localPath, 0o755);
|
||||
console.log(`✓ Installed ${binaryName} from ${platformPkg.name}`);
|
||||
// Populate cache for future npx runs
|
||||
try {
|
||||
if (!existsSync(cachePath)) {
|
||||
copyFileSync(localPath, cachePath);
|
||||
}
|
||||
} catch {}
|
||||
// Populate cache first (canonical location)
|
||||
copyFileSync(src, cachePath);
|
||||
if (!isWindows) {
|
||||
// Mirror into local bin for Unix-like platforms only
|
||||
copyFileSync(cachePath, localPath);
|
||||
try { chmodSync(localPath, 0o755); } catch {}
|
||||
}
|
||||
console.log(`✓ Installed ${binaryName} from ${platformPkg.name} (cached)`);
|
||||
continue; // next binary
|
||||
} catch (e) {
|
||||
console.warn(`⚠ Failed platform package install (${e.message}), falling back to GitHub download`);
|
||||
@@ -357,9 +349,22 @@ async function main() {
|
||||
}
|
||||
|
||||
// Validate header to avoid corrupt binaries causing spawn EFTYPE/ENOEXEC
|
||||
const valid = validateDownloadedBinary(localPath);
|
||||
// On Windows, archive extraction writes to binDir; move the result to cache
|
||||
// and remove the copy from node_modules to avoid future locks. On Unix,
|
||||
// we keep a copy in binDir and also ensure cache is populated.
|
||||
if (isWindows) {
|
||||
try {
|
||||
// Ensure the extracted file is at localPath; then move it to cachePath
|
||||
copyFileSync(localPath, cachePath);
|
||||
try { unlinkSync(localPath); } catch {}
|
||||
} catch (e) {
|
||||
throw new Error(`failed to move binary to cache: ${e.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
const valid = validateDownloadedBinary(isWindows ? cachePath : localPath);
|
||||
if (!valid.ok) {
|
||||
try { unlinkSync(localPath); } catch {}
|
||||
try { isWindows ? unlinkSync(cachePath) : unlinkSync(localPath); } catch {}
|
||||
throw new Error(`invalid binary (${valid.reason})`);
|
||||
}
|
||||
|
||||
@@ -368,11 +373,11 @@ async function main() {
|
||||
chmodSync(localPath, 0o755);
|
||||
}
|
||||
|
||||
console.log(`✓ Installed ${binaryName}`);
|
||||
// Save into persistent cache for future fast installs
|
||||
try {
|
||||
copyFileSync(localPath, cachePath);
|
||||
} catch {}
|
||||
console.log(`✓ Installed ${binaryName}${isWindows ? ' (cached)' : ''}`);
|
||||
// Ensure persistent cache holds the binary (already true for Windows path)
|
||||
if (!isWindows) {
|
||||
try { copyFileSync(localPath, cachePath); } catch {}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`✗ Failed to install ${binaryName}: ${error.message}`);
|
||||
console.error(` Downloaded from: ${downloadUrl}`);
|
||||
@@ -384,13 +389,12 @@ async function main() {
|
||||
const mainBinary = `code-${targetTriple}${binaryExt}`;
|
||||
const mainBinaryPath = join(binDir, mainBinary);
|
||||
|
||||
if (existsSync(mainBinaryPath)) {
|
||||
if (existsSync(mainBinaryPath) || existsSync(getCachedBinaryPath(version, targetTriple, platform() === 'win32'))) {
|
||||
try {
|
||||
const stats = statSync(mainBinaryPath);
|
||||
if (!stats.size) {
|
||||
throw new Error('binary is empty (download likely failed)');
|
||||
}
|
||||
const valid = validateDownloadedBinary(mainBinaryPath);
|
||||
const probePath = existsSync(mainBinaryPath) ? mainBinaryPath : getCachedBinaryPath(version, targetTriple, platform() === 'win32');
|
||||
const stats = statSync(probePath);
|
||||
if (!stats.size) throw new Error('binary is empty (download likely failed)');
|
||||
const valid = validateDownloadedBinary(probePath);
|
||||
if (!valid.ok) {
|
||||
console.warn(`⚠ Main code binary appears invalid: ${valid.reason}`);
|
||||
console.warn(' Try reinstalling or check your network/proxy settings.');
|
||||
|
||||
+29
-3
@@ -101,6 +101,13 @@ pub(crate) struct App<'a> {
|
||||
/// profile defaults until all cells are explicitly painted.
|
||||
clear_on_first_frame: bool,
|
||||
|
||||
/// Track last known terminal size. If it changes (true resize or a
|
||||
/// tab switch that altered the viewport), perform a full clear on the next
|
||||
/// draw to avoid ghost cells from the previous size. This is cheap and
|
||||
/// happens rarely, but fixes Windows/macOS terminals that don't fully
|
||||
/// repaint after focus/size changes until a manual resize occurs.
|
||||
last_frame_size: Option<ratatui::prelude::Size>,
|
||||
|
||||
// Double‑Esc timing for backtrack/edit‑previous
|
||||
last_esc_time: Option<Instant>,
|
||||
|
||||
@@ -284,6 +291,7 @@ impl App<'_> {
|
||||
commit_anim_running: Arc::new(AtomicBool::new(false)),
|
||||
terminal_info,
|
||||
clear_on_first_frame: true,
|
||||
last_frame_size: None,
|
||||
last_esc_time: None,
|
||||
timing_enabled: enable_perf,
|
||||
timing: TimingStats::default(),
|
||||
@@ -338,6 +346,10 @@ impl App<'_> {
|
||||
// Insert an event to trigger the first render.
|
||||
let app_event_tx = self.app_event_tx.clone();
|
||||
app_event_tx.send(AppEvent::RequestRedraw);
|
||||
// Some Windows/macOS terminals report an initial size that stabilizes
|
||||
// shortly after entering the alt screen. Schedule one follow‑up frame
|
||||
// to catch any late size change without polling.
|
||||
app_event_tx.send(AppEvent::ScheduleFrameIn(Duration::from_millis(120)));
|
||||
|
||||
'main: loop {
|
||||
let event = match self.next_event_priority() { Some(e) => e, None => break 'main };
|
||||
@@ -1275,14 +1287,28 @@ impl App<'_> {
|
||||
}
|
||||
|
||||
fn draw_next_frame(&mut self, terminal: &mut tui::Tui) -> Result<()> {
|
||||
// Hard clear on the very first frame (and while onboarding) to ensure a
|
||||
// clean background across terminals that don't respect our color attrs
|
||||
// during EnterAlternateScreen.
|
||||
if self.clear_on_first_frame || matches!(self.app_state, AppState::Onboarding { .. }) {
|
||||
terminal.clear()?;
|
||||
self.clear_on_first_frame = false;
|
||||
}
|
||||
|
||||
// Terminal resize handling - simplified version since private fields aren't accessible
|
||||
// The terminal will handle resize events internally
|
||||
let _screen_size = terminal.size()?;
|
||||
// If the terminal area changed (actual resize or tab switch that altered
|
||||
// viewport), force a full clear once to prevent ghost artifacts. Some
|
||||
// terminals on Windows/macOS do not reliably deliver Resize events on
|
||||
// focus switches; querying the size each frame is cheap and lets us
|
||||
// detect the change without extra event wiring.
|
||||
let screen_size = terminal.size()?;
|
||||
if self
|
||||
.last_frame_size
|
||||
.map(|prev| prev != screen_size)
|
||||
.unwrap_or(false)
|
||||
{
|
||||
terminal.clear()?;
|
||||
}
|
||||
self.last_frame_size = Some(screen_size);
|
||||
|
||||
terminal.draw(|frame| {
|
||||
match &mut self.app_state {
|
||||
|
||||
Reference in New Issue
Block a user