Skip to content

Commit 64c60ac

Browse files
committed
fix: improve Windows compatibility and robustness in SEA stub
Add WIN32 constant for platform detection. Fix stale lock file detection with proper EPERM/ESRCH handling. Add retry logic for file operations with EBUSY/EPERM errors. Add home directory fallbacks for containerized environments. Validate MIN_NODE_VERSION environment variable.
1 parent ac37843 commit 64c60ac

File tree

1 file changed

+79
-24
lines changed

1 file changed

+79
-24
lines changed

src/sea/stub.mts

Lines changed: 79 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -19,18 +19,35 @@ import path from 'node:path'
1919

2020
import { parseTarGzip } from 'nanotar'
2121

22+
// Platform detection
23+
const WIN32 = process.platform === 'win32'
24+
2225
// Configurable constants with environment variable overrides.
2326
// os.homedir() can throw if no home directory is available.
2427
let SOCKET_HOME: string
25-
try {
26-
SOCKET_HOME = process.env['SOCKET_HOME'] || path.join(os.homedir(), '.socket')
27-
} catch (error) {
28-
console.error(
29-
'Fatal: Unable to determine home directory. Set SOCKET_HOME environment variable.',
30-
)
31-
console.error(`Error: ${formatError(error)}`)
32-
// eslint-disable-next-line n/no-process-exit
33-
process.exit(1)
28+
if (process.env['SOCKET_HOME']) {
29+
SOCKET_HOME = process.env['SOCKET_HOME']
30+
} else {
31+
// Try multiple fallbacks to determine home directory
32+
let homeDir: string | undefined
33+
try {
34+
homeDir = os.homedir()
35+
} catch {
36+
// os.homedir() can throw in some environments
37+
}
38+
39+
// Fallback to environment variables
40+
if (!homeDir) {
41+
homeDir = process.env['HOME'] || process.env['USERPROFILE']
42+
}
43+
44+
// Last resort: use temp directory
45+
if (!homeDir) {
46+
homeDir = path.join(os.tmpdir(), '.socket-cli-home')
47+
console.warn(`Warning: Using temporary directory as home: ${homeDir}`)
48+
}
49+
50+
SOCKET_HOME = path.join(homeDir, '.socket')
3451
}
3552
const CLI_INSTALL_LOCK_FILE_NAME = '.install.lock'
3653
const DOWNLOAD_MESSAGE_DELAY_MS = 2_000
@@ -49,7 +66,18 @@ const SOCKET_CLI_PACKAGE =
4966
process.env['SOCKET_CLI_PACKAGE'] || '@socketsecurity/cli'
5067
const SOCKET_CLI_PACKAGE_JSON = path.join(SOCKET_CLI_PACKAGE_DIR, 'package.json')
5168
// Minimum Node.js version for system Node (v22 = Active LTS)
52-
const MIN_NODE_VERSION = parseInt(process.env['MIN_NODE_VERSION'] || '22', 10)
69+
// Parse MIN_NODE_VERSION with validation
70+
const MIN_NODE_VERSION = (() => {
71+
const envValue = process.env['MIN_NODE_VERSION']
72+
if (envValue) {
73+
const parsed = parseInt(envValue, 10)
74+
if (!isNaN(parsed) && parsed > 0 && parsed < 100) {
75+
return parsed
76+
}
77+
console.warn(`Warning: Invalid MIN_NODE_VERSION "${envValue}", using default 22`)
78+
}
79+
return 22
80+
})()
5381

5482
// ============================================================================
5583
// Helper utilities
@@ -125,18 +153,30 @@ async function remove(
125153
}
126154
}
127155

128-
// Perform deletion.
156+
// Perform deletion with retry logic for Windows file locks.
129157
try {
130158
const stats = await fs.stat(absolutePath)
131159
if (stats.isDirectory()) {
132-
await fs.rm(absolutePath, { recursive: true, force: true })
160+
// More retries for directories
161+
await retryWithBackoff(
162+
() => fs.rm(absolutePath, { recursive: true, force: true }),
163+
3,
164+
200,
165+
2,
166+
)
133167
} else {
134-
await fs.unlink(absolutePath)
168+
await retryWithBackoff(() => fs.unlink(absolutePath), 2, 200, 2)
135169
}
136170
} catch (error) {
137171
const code = (error as NodeJS.ErrnoException)?.code
138172
// Silently ignore if file doesn't exist.
139173
if (code !== 'ENOENT') {
174+
// Check if it's a permission error on Windows
175+
if (code === 'EPERM' && WIN32) {
176+
debugLog(
177+
`Warning: Unable to delete ${absolutePath} (Windows EPERM). File may be locked by antivirus or another process.`,
178+
)
179+
}
140180
throw error
141181
}
142182
}
@@ -169,15 +209,26 @@ async function acquireLock(): Promise<string> {
169209
const lockContent = await fs.readFile(lockPath, 'utf8')
170210
const lockPid = Number.parseInt(lockContent.trim(), 10)
171211
if (!Number.isNaN(lockPid)) {
172-
// Try to check if process still exists (Unix-like systems).
173-
// On Windows this will always succeed, but that's okay - timeout will handle it.
212+
// Try to check if process still exists.
174213
try {
175214
process.kill(lockPid, 0)
176215
// Process exists, wait and retry.
177-
} catch {
178-
// Process doesn't exist, remove stale lock.
179-
await remove(lockPath)
180-
continue
216+
} catch (killError: any) {
217+
// On Windows, EPERM means the process exists but we don't have permission.
218+
// On Unix, ESRCH means the process doesn't exist.
219+
if (killError.code === 'ESRCH') {
220+
// Process doesn't exist, remove stale lock.
221+
await remove(lockPath)
222+
continue
223+
} else if (killError.code === 'EPERM' && WIN32) {
224+
// On Windows, EPERM could mean process exists but owned by another user.
225+
// We'll continue waiting as if the process exists.
226+
debugLog('Lock file process may exist (Windows EPERM), waiting...')
227+
} else {
228+
// Process doesn't exist or other error, try to remove stale lock.
229+
await remove(lockPath)
230+
continue
231+
}
181232
}
182233
}
183234
} catch {
@@ -618,12 +669,16 @@ async function retryWithBackoff<T>(
618669
return await fn()
619670
} catch (error) {
620671
lastError = error
621-
// Only retry on transient errors (EBUSY, EMFILE, ENFILE).
672+
// Only retry on transient errors (EBUSY, EMFILE, ENFILE, EPERM on Windows).
622673
const code = (error as NodeJS.ErrnoException)?.code
623-
if (
624-
attempt < maxRetries &&
625-
(code === 'EBUSY' || code === 'EMFILE' || code === 'ENFILE')
626-
) {
674+
const shouldRetry =
675+
code === 'EBUSY' ||
676+
code === 'EMFILE' ||
677+
code === 'ENFILE' ||
678+
(code === 'EPERM' && WIN32)
679+
680+
if (attempt < maxRetries && shouldRetry) {
681+
debugLog(`Retrying after ${code} error (attempt ${attempt + 1}/${maxRetries})`)
627682
await new Promise(resolve => setTimeout(resolve, delay))
628683
delay *= backoffFactor
629684
continue

0 commit comments

Comments
 (0)