mirror of
https://github.com/hanzoai/dev.git
synced 2026-10-25 03:32:32 +00:00
cli(windows): guard against corrupt/empty downloads and improve spawn errors\n\n- postinstall: robust download + header validation (MZ/ELF/Mach-O), delete on failure\n- coder.js: pre-spawn binary header check; clearer EFTYPE/ENOEXEC/EACCES guidance\n- reduces confusing Windows EFTYPE crashes from partial downloads
This commit is contained in:
+55
-2
@@ -63,7 +63,7 @@ if (!existsSync(binaryPath)) {
|
||||
}
|
||||
|
||||
// Check if binary exists and try to fix permissions if needed
|
||||
import { existsSync, chmodSync } from "fs";
|
||||
import { existsSync, chmodSync, statSync, openSync, readSync, closeSync } from "fs";
|
||||
if (existsSync(binaryPath)) {
|
||||
try {
|
||||
// Ensure binary is executable on Unix-like systems
|
||||
@@ -81,6 +81,50 @@ if (existsSync(binaryPath)) {
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Lightweight header validation to provide clearer errors before spawn
|
||||
const validateBinary = (p) => {
|
||||
try {
|
||||
const st = statSync(p);
|
||||
if (!st.isFile() || st.size === 0) {
|
||||
return { ok: false, reason: "empty or not a regular file" };
|
||||
}
|
||||
const fd = openSync(p, "r");
|
||||
try {
|
||||
const buf = Buffer.alloc(4);
|
||||
const n = readSync(fd, buf, 0, 4, 0);
|
||||
if (n < 2) return { ok: false, reason: "too short" };
|
||||
if (platform === "win32") {
|
||||
if (!(buf[0] === 0x4d && buf[1] === 0x5a)) return { ok: false, reason: "invalid PE header (missing MZ)" };
|
||||
} else if (platform === "linux" || platform === "android") {
|
||||
if (!(buf[0] === 0x7f && buf[1] === 0x45 && buf[2] === 0x4c && buf[3] === 0x46)) return { ok: false, reason: "invalid ELF header" };
|
||||
} else if (platform === "darwin") {
|
||||
const isMachO = (buf[0] === 0xcf && buf[1] === 0xfa && buf[2] === 0xed && buf[3] === 0xfe) ||
|
||||
(buf[0] === 0xca && buf[1] === 0xfe && buf[2] === 0xba && buf[3] === 0xbe);
|
||||
if (!isMachO) return { ok: false, reason: "invalid Mach-O header" };
|
||||
}
|
||||
} finally {
|
||||
closeSync(fd);
|
||||
}
|
||||
return { ok: true };
|
||||
} catch (e) {
|
||||
return { ok: false, reason: e.message };
|
||||
}
|
||||
};
|
||||
|
||||
const validation = validateBinary(binaryPath);
|
||||
if (!validation.ok) {
|
||||
console.error(`The native binary at ${binaryPath} appears invalid: ${validation.reason}`);
|
||||
console.error("This can happen if the download failed or was modified by antivirus/proxy.");
|
||||
console.error("Please try reinstalling:");
|
||||
console.error(" npm uninstall -g @just-every/code");
|
||||
console.error(" npm install -g @just-every/code");
|
||||
if (platform === "win32") {
|
||||
console.error("If the issue persists, clear npm cache and disable antivirus temporarily:");
|
||||
console.error(" npm cache clean --force");
|
||||
}
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Use an asynchronous spawn instead of spawnSync so that Node is able to
|
||||
// respond to signals (e.g. Ctrl-C / SIGINT) while the native binary is
|
||||
// executing. This allows us to forward those signals to the child process
|
||||
@@ -95,10 +139,19 @@ const child = spawn(binaryPath, process.argv.slice(2), {
|
||||
|
||||
child.on("error", (err) => {
|
||||
// Typically triggered when the binary is missing or not executable.
|
||||
if (err.code === 'EACCES') {
|
||||
const code = err && err.code;
|
||||
if (code === 'EACCES') {
|
||||
console.error(`Permission denied: ${binaryPath}`);
|
||||
console.error(`Try running: chmod +x "${binaryPath}"`);
|
||||
console.error(`Or reinstall the package with: npm install -g @just-every/code`);
|
||||
} else if (code === 'EFTYPE' || code === 'ENOEXEC') {
|
||||
console.error(`Failed to execute native binary: ${binaryPath}`);
|
||||
console.error("The file may be corrupt or of the wrong type. Reinstall usually fixes this:");
|
||||
console.error(" npm uninstall -g @just-every/code && npm install -g @just-every/code");
|
||||
if (platform === 'win32') {
|
||||
console.error("On Windows, ensure the .exe downloaded correctly (proxy/AV can interfere).");
|
||||
console.error("Try clearing cache: npm cache clean --force");
|
||||
}
|
||||
} else {
|
||||
console.error(err);
|
||||
}
|
||||
|
||||
+86
-21
@@ -1,7 +1,7 @@
|
||||
#!/usr/bin/env node
|
||||
// Non-functional change to trigger release workflow
|
||||
|
||||
import { existsSync, mkdirSync, createWriteStream, chmodSync, readFileSync, readSync, writeFileSync } from 'fs';
|
||||
import { existsSync, mkdirSync, createWriteStream, chmodSync, readFileSync, readSync, writeFileSync, unlinkSync, statSync, openSync, closeSync } from 'fs';
|
||||
import { join, dirname } from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { get } from 'https';
|
||||
@@ -29,33 +29,77 @@ function getTargetTriple() {
|
||||
return `${rustArch}-${rustPlatform}`;
|
||||
}
|
||||
|
||||
async function downloadBinary(url, dest) {
|
||||
async function downloadBinary(url, dest, maxRedirects = 5) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const file = createWriteStream(dest);
|
||||
|
||||
get(url, (response) => {
|
||||
if (response.statusCode === 302 || response.statusCode === 301) {
|
||||
// Follow redirect
|
||||
get(response.headers.location, (redirectResponse) => {
|
||||
redirectResponse.pipe(file);
|
||||
const attempt = (currentUrl, redirectsLeft) => {
|
||||
get(currentUrl, (response) => {
|
||||
const status = response.statusCode || 0;
|
||||
const location = response.headers.location;
|
||||
|
||||
if ((status === 301 || status === 302 || status === 303 || status === 307 || status === 308) && location) {
|
||||
if (redirectsLeft <= 0) {
|
||||
reject(new Error(`Too many redirects while downloading ${currentUrl}`));
|
||||
return;
|
||||
}
|
||||
attempt(location, redirectsLeft - 1);
|
||||
return;
|
||||
}
|
||||
|
||||
if (status === 200) {
|
||||
// Only create the file stream after we know it's a successful response
|
||||
const file = createWriteStream(dest);
|
||||
response.pipe(file);
|
||||
file.on('finish', () => {
|
||||
file.close();
|
||||
resolve();
|
||||
});
|
||||
}).on('error', reject);
|
||||
} else if (response.statusCode === 200) {
|
||||
response.pipe(file);
|
||||
file.on('finish', () => {
|
||||
file.close();
|
||||
resolve();
|
||||
});
|
||||
} else {
|
||||
reject(new Error(`Failed to download: ${response.statusCode}`));
|
||||
}
|
||||
}).on('error', reject);
|
||||
file.on('error', (err) => {
|
||||
try { unlinkSync(dest); } catch {}
|
||||
reject(err);
|
||||
});
|
||||
} else {
|
||||
reject(new Error(`Failed to download: HTTP ${status}`));
|
||||
}
|
||||
}).on('error', (err) => {
|
||||
try { unlinkSync(dest); } catch {}
|
||||
reject(err);
|
||||
});
|
||||
};
|
||||
|
||||
attempt(url, maxRedirects);
|
||||
});
|
||||
}
|
||||
|
||||
function validateDownloadedBinary(p) {
|
||||
try {
|
||||
const st = statSync(p);
|
||||
if (!st.isFile() || st.size === 0) {
|
||||
return { ok: false, reason: 'empty or not a regular file' };
|
||||
}
|
||||
const fd = openSync(p, 'r');
|
||||
try {
|
||||
const buf = Buffer.alloc(4);
|
||||
const n = readSync(fd, buf, 0, 4, 0);
|
||||
if (n < 2) return { ok: false, reason: 'too short' };
|
||||
const plt = platform();
|
||||
if (plt === 'win32') {
|
||||
if (!(buf[0] === 0x4d && buf[1] === 0x5a)) return { ok: false, reason: 'invalid PE header (missing MZ)' };
|
||||
} else if (plt === 'linux' || plt === 'android') {
|
||||
if (!(buf[0] === 0x7f && buf[1] === 0x45 && buf[2] === 0x4c && buf[3] === 0x46)) return { ok: false, reason: 'invalid ELF header' };
|
||||
} else if (plt === 'darwin') {
|
||||
const isMachO = (buf[0] === 0xcf && buf[1] === 0xfa && buf[2] === 0xed && buf[3] === 0xfe) ||
|
||||
(buf[0] === 0xca && buf[1] === 0xfe && buf[2] === 0xba && buf[3] === 0xbe);
|
||||
if (!isMachO) return { ok: false, reason: 'invalid Mach-O header' };
|
||||
}
|
||||
return { ok: true };
|
||||
} finally {
|
||||
closeSync(fd);
|
||||
}
|
||||
} catch (e) {
|
||||
return { ok: false, reason: e.message };
|
||||
}
|
||||
}
|
||||
|
||||
async function main() {
|
||||
// Detect potential PATH conflict with an existing `code` command (e.g., VS Code)
|
||||
try {
|
||||
@@ -124,7 +168,14 @@ async function main() {
|
||||
console.log(`Downloading ${binaryName}...`);
|
||||
try {
|
||||
await downloadBinary(downloadUrl, localPath);
|
||||
|
||||
|
||||
// Validate header to avoid corrupt binaries causing spawn EFTYPE/ENOEXEC
|
||||
const valid = validateDownloadedBinary(localPath);
|
||||
if (!valid.ok) {
|
||||
try { unlinkSync(localPath); } catch {}
|
||||
throw new Error(`invalid binary (${valid.reason})`);
|
||||
}
|
||||
|
||||
// Make executable on Unix-like systems
|
||||
if (!isWindows) {
|
||||
chmodSync(localPath, 0o755);
|
||||
@@ -143,6 +194,20 @@ async function main() {
|
||||
const mainBinaryPath = join(binDir, mainBinary);
|
||||
|
||||
if (existsSync(mainBinaryPath)) {
|
||||
try {
|
||||
const stats = statSync(mainBinaryPath);
|
||||
if (!stats.size) {
|
||||
throw new Error('binary is empty (download likely failed)');
|
||||
}
|
||||
const valid = validateDownloadedBinary(mainBinaryPath);
|
||||
if (!valid.ok) {
|
||||
console.warn(`⚠ Main code binary appears invalid: ${valid.reason}`);
|
||||
console.warn(' Try reinstalling or check your network/proxy settings.');
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn(`⚠ Main code binary appears invalid: ${e.message}`);
|
||||
console.warn(' Try reinstalling or check your network/proxy settings.');
|
||||
}
|
||||
console.log('Setting up main code binary...');
|
||||
|
||||
// On Windows, we can't use symlinks easily, so update the JS wrapper
|
||||
|
||||
Reference in New Issue
Block a user