mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-08-25 09:46:47 +00:00
fix(error-handling): surface silent errors across the codebase (#326)
Add console.warn/console.error logging to 22 silent catch blocks across 10 files. Errors in cleanup, migrations, SSO, fleet snapshots, shutdown, and validation are now visible in logs. ENOENT guards added to file-system catches to distinguish missing files from permission errors. No control flow changes.
This commit is contained in:
+35
-15
@@ -517,7 +517,8 @@ app.get('/api/auth/sso/providers', (_req: Request, res: Response): void => {
|
||||
try {
|
||||
const providers = SSOService.getInstance().getEnabledProviders();
|
||||
res.json(providers);
|
||||
} catch {
|
||||
} catch (e) {
|
||||
console.warn('[SSO] Failed to list enabled providers, returning empty list:', (e as Error).message);
|
||||
res.json([]);
|
||||
}
|
||||
});
|
||||
@@ -621,7 +622,8 @@ app.get('/api/auth/sso/oidc/:provider/callback', async (req: Request, res: Respo
|
||||
let statePayload: { state: string; codeVerifier: string; provider: string };
|
||||
try {
|
||||
statePayload = JSON.parse(cryptoSvc.decrypt(stateCookie));
|
||||
} catch {
|
||||
} catch (e) {
|
||||
console.error('[SSO] Failed to decrypt SSO state cookie:', (e as Error).message);
|
||||
res.redirect('/?sso_error=Invalid+SSO+session');
|
||||
return;
|
||||
}
|
||||
@@ -1295,8 +1297,8 @@ async function captureLocalNodeFiles(node: Node): Promise<SnapshotNodeData> {
|
||||
try {
|
||||
const composeContent = await fsService.getStackContent(stackName);
|
||||
files.push({ filename: 'compose.yaml', content: composeContent });
|
||||
} catch {
|
||||
// Stack has no compose file - skip
|
||||
} catch (e) {
|
||||
console.warn(`[Fleet Snapshot] Could not read compose file for stack "${stackName}", skipping:`, (e as Error).message);
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
@@ -1339,7 +1341,8 @@ async function captureRemoteNodeFiles(node: Node): Promise<SnapshotNodeData> {
|
||||
const content = await composeRes.text();
|
||||
files.push({ filename: 'compose.yaml', content });
|
||||
}
|
||||
} catch {
|
||||
} catch (e) {
|
||||
console.warn(`[Fleet Snapshot] Failed to fetch remote compose for stack "${stackName}":`, (e as Error).message);
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
@@ -1536,8 +1539,9 @@ app.post('/api/fleet/snapshots/:id/restore', async (req: Request, res: Response)
|
||||
// Backup current files before restore
|
||||
try {
|
||||
await fsService.backupStackFiles(stackName);
|
||||
} catch {
|
||||
// Stack may not exist yet - that's ok
|
||||
} catch (e) {
|
||||
// Stack may not exist yet before first restore — that's ok
|
||||
console.warn(`[Fleet Snapshot] Pre-restore backup failed for stack "${stackName}" (may not exist yet):`, (e as Error).message);
|
||||
}
|
||||
|
||||
for (const file of files) {
|
||||
@@ -2617,7 +2621,11 @@ app.get('/api/stacks/:stackName/env', async (req: Request, res: Response) => {
|
||||
|
||||
try {
|
||||
await fsService.access(envPath);
|
||||
} catch {
|
||||
} catch (e: unknown) {
|
||||
const code = (e as NodeJS.ErrnoException)?.code;
|
||||
if (code !== 'ENOENT') {
|
||||
console.error('[Sencho] Unexpected error checking env file existence:', (e as Error).message);
|
||||
}
|
||||
return res.status(404).json({ error: 'Env file not found' });
|
||||
}
|
||||
|
||||
@@ -3830,7 +3838,8 @@ app.post('/api/scheduled-tasks', (req: Request, res: Response): void => {
|
||||
}
|
||||
}
|
||||
// Validate cron expression
|
||||
try { CronExpressionParser.parse(cron_expression); } catch {
|
||||
try { CronExpressionParser.parse(cron_expression); } catch (e) {
|
||||
console.warn('[Scheduler] Invalid cron expression rejected:', cron_expression, (e as Error).message);
|
||||
res.status(400).json({ error: 'Invalid cron expression.' }); return;
|
||||
}
|
||||
|
||||
@@ -3945,7 +3954,8 @@ app.put('/api/scheduled-tasks/:id', (req: Request, res: Response): void => {
|
||||
}
|
||||
|
||||
if (cron_expression) {
|
||||
try { CronExpressionParser.parse(cron_expression); } catch {
|
||||
try { CronExpressionParser.parse(cron_expression); } catch (e) {
|
||||
console.warn('[Scheduler] Invalid cron expression rejected:', cron_expression, (e as Error).message);
|
||||
res.status(400).json({ error: 'Invalid cron expression.' }); return;
|
||||
}
|
||||
}
|
||||
@@ -4733,11 +4743,21 @@ const gracefulShutdown = (signal: string) => {
|
||||
|
||||
server.close(() => {
|
||||
console.log('[Shutdown] HTTP server closed');
|
||||
try { LicenseService.getInstance().destroy(); } catch { /* already stopped */ }
|
||||
try { MonitorService.getInstance().stop(); } catch { /* already stopped */ }
|
||||
try { ImageUpdateService.getInstance().stop(); } catch { /* already stopped */ }
|
||||
try { SchedulerService.getInstance().stop(); } catch { /* already stopped */ }
|
||||
try { DatabaseService.getInstance().getDb().close(); } catch { /* already closed */ }
|
||||
try { LicenseService.getInstance().destroy(); } catch (e) {
|
||||
console.warn('[Shutdown] LicenseService cleanup failed:', (e as Error).message);
|
||||
}
|
||||
try { MonitorService.getInstance().stop(); } catch (e) {
|
||||
console.warn('[Shutdown] MonitorService cleanup failed:', (e as Error).message);
|
||||
}
|
||||
try { ImageUpdateService.getInstance().stop(); } catch (e) {
|
||||
console.warn('[Shutdown] ImageUpdateService cleanup failed:', (e as Error).message);
|
||||
}
|
||||
try { SchedulerService.getInstance().stop(); } catch (e) {
|
||||
console.warn('[Shutdown] SchedulerService cleanup failed:', (e as Error).message);
|
||||
}
|
||||
try { DatabaseService.getInstance().getDb().close(); } catch (e) {
|
||||
console.warn('[Shutdown] Database close failed:', (e as Error).message);
|
||||
}
|
||||
console.log('[Shutdown] Done - exiting');
|
||||
process.exit(0);
|
||||
});
|
||||
|
||||
@@ -99,7 +99,10 @@ export class ComposeService {
|
||||
PATH: process.env.PATH || '/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin',
|
||||
});
|
||||
} finally {
|
||||
try { fs.unlinkSync(configPath); fs.rmdirSync(tmpDir); } catch { /* best-effort cleanup */ }
|
||||
try { fs.unlinkSync(configPath); fs.rmdirSync(tmpDir); } catch (e) {
|
||||
// Best-effort cleanup: temp config dir may already be removed or locked
|
||||
console.warn('[ComposeService] Could not clean up temp Docker config dir:', (e as Error).message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -24,8 +24,9 @@ export class CryptoService {
|
||||
console.warn(`[CryptoService] Fixing permissive key file permissions (was 0o${mode.toString(8)}, set to 0o600)`);
|
||||
fs.chmodSync(keyPath, 0o600);
|
||||
}
|
||||
} catch {
|
||||
} catch (e) {
|
||||
// chmod not supported on this platform (e.g. Windows) — skip
|
||||
console.warn('[CryptoService] Could not enforce key file permissions (platform may not support chmod):', (e as Error).message);
|
||||
}
|
||||
} else {
|
||||
this.key = crypto.randomBytes(KEY_LENGTH);
|
||||
|
||||
@@ -436,7 +436,12 @@ export class DatabaseService {
|
||||
// Drop legacy SSH/TLS columns from pre-0.7 databases (no longer read or written)
|
||||
const legacyCols = ['host', 'port', 'ssh_port', 'ssh_user', 'ssh_password', 'ssh_key', 'tls_ca', 'tls_cert', 'tls_key'];
|
||||
for (const col of legacyCols) {
|
||||
try { this.db.prepare(`ALTER TABLE nodes DROP COLUMN ${col}`).run(); } catch { /* already dropped or never existed */ }
|
||||
try { this.db.prepare(`ALTER TABLE nodes DROP COLUMN ${col}`).run(); } catch (e: unknown) {
|
||||
// Expected: column already dropped or never existed
|
||||
if (!String((e as Error)?.message).includes('no such column')) {
|
||||
console.warn(`[DatabaseService] Unexpected error dropping legacy column "${col}":`, (e as Error).message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize default global settings if they don't exist
|
||||
@@ -511,7 +516,12 @@ export class DatabaseService {
|
||||
|
||||
private migrateSSOColumns(): void {
|
||||
const maybeAddCol = (table: string, col: string, def: string) => {
|
||||
try { this.db.prepare(`ALTER TABLE ${table} ADD COLUMN ${col} ${def}`).run(); } catch { /* already exists */ }
|
||||
try { this.db.prepare(`ALTER TABLE ${table} ADD COLUMN ${col} ${def}`).run(); } catch (e: unknown) {
|
||||
// Expected: column already exists
|
||||
if (!String((e as Error)?.message).includes('duplicate column')) {
|
||||
console.warn(`[DatabaseService] Unexpected error adding column "${col}" to "${table}":`, (e as Error).message);
|
||||
}
|
||||
}
|
||||
};
|
||||
maybeAddCol('users', 'auth_provider', "TEXT NOT NULL DEFAULT 'local'");
|
||||
maybeAddCol('users', 'provider_id', 'TEXT DEFAULT NULL');
|
||||
@@ -562,7 +572,9 @@ export class DatabaseService {
|
||||
`);
|
||||
try {
|
||||
this.db.exec('CREATE UNIQUE INDEX IF NOT EXISTS idx_role_assignments_unique ON role_assignments(user_id, role, resource_type, resource_id)');
|
||||
} catch { /* already exists */ }
|
||||
} catch (e) {
|
||||
console.warn('[DatabaseService] Could not create role_assignments unique index:', (e as Error).message);
|
||||
}
|
||||
}
|
||||
|
||||
// --- Agents ---
|
||||
|
||||
@@ -651,7 +651,10 @@ class DockerController {
|
||||
// Destroy the Docker stats stream when the WebSocket closes to prevent
|
||||
// orphaned streams polling the daemon after client disconnect.
|
||||
ws.on('close', () => {
|
||||
try { (stats as any).destroy(); } catch { /* stream already ended */ }
|
||||
try { (stats as any).destroy(); } catch (e) {
|
||||
// Stream already ended before client disconnected
|
||||
console.warn('[DockerController] Stats stream already ended on WS close:', (e as Error).message);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -723,8 +726,9 @@ class DockerController {
|
||||
|
||||
case 'resize':
|
||||
if (msg.rows && msg.cols) {
|
||||
exec.resize({ h: msg.rows, w: msg.cols }).catch(() => {
|
||||
// Ignore resize errors (exec may have ended)
|
||||
exec.resize({ h: msg.rows, w: msg.cols }).catch((e: Error) => {
|
||||
// Exec may have ended before resize completes
|
||||
console.warn('[DockerController] Exec resize failed (exec may have ended):', e.message);
|
||||
});
|
||||
}
|
||||
break;
|
||||
@@ -733,8 +737,9 @@ class DockerController {
|
||||
// Keep-alive, no-op
|
||||
break;
|
||||
}
|
||||
} catch {
|
||||
// Non-JSON or malformed message - ignore
|
||||
} catch (e) {
|
||||
// Non-JSON or malformed WebSocket message
|
||||
console.warn('[DockerController] Ignoring malformed exec WS message:', (e as Error).message);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -742,8 +747,9 @@ class DockerController {
|
||||
ws.on('close', () => {
|
||||
try {
|
||||
stream.destroy();
|
||||
} catch {
|
||||
// Ignore destroy errors
|
||||
} catch (e) {
|
||||
// Stream already destroyed before WS close
|
||||
console.warn('[DockerController] Exec stream already destroyed on WS close:', (e as Error).message);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -275,8 +275,11 @@ export class FileSystemService {
|
||||
try {
|
||||
await fsPromises.access(oldEnvPath);
|
||||
await fsPromises.rename(oldEnvPath, newEnvPath);
|
||||
} catch {
|
||||
// No env file to migrate
|
||||
} catch (e: unknown) {
|
||||
const code = (e as NodeJS.ErrnoException)?.code;
|
||||
if (code !== 'ENOENT') {
|
||||
console.warn(`[FileSystemService] Could not migrate env file for ${stackName}:`, (e as Error).message);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -300,8 +303,11 @@ export class FileSystemService {
|
||||
try {
|
||||
await fsPromises.access(src);
|
||||
await fsPromises.copyFile(src, path.join(backupDir, file));
|
||||
} catch {
|
||||
// File doesn't exist, skip
|
||||
} catch (e: unknown) {
|
||||
const code = (e as NodeJS.ErrnoException)?.code;
|
||||
if (code !== 'ENOENT') {
|
||||
console.warn(`[FileSystemService] Could not back up ${file}:`, (e as Error).message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -310,8 +316,11 @@ export class FileSystemService {
|
||||
try {
|
||||
await fsPromises.access(envSrc);
|
||||
await fsPromises.copyFile(envSrc, path.join(backupDir, '.env'));
|
||||
} catch {
|
||||
// No .env to backup
|
||||
} catch (e: unknown) {
|
||||
const code = (e as NodeJS.ErrnoException)?.code;
|
||||
if (code !== 'ENOENT') {
|
||||
console.warn('[FileSystemService] Could not back up .env:', (e as Error).message);
|
||||
}
|
||||
}
|
||||
|
||||
// Write timestamp marker
|
||||
@@ -343,7 +352,8 @@ export class FileSystemService {
|
||||
try {
|
||||
const ts = await fsPromises.readFile(tsFile, 'utf-8');
|
||||
return { exists: true, timestamp: parseInt(ts, 10) || null };
|
||||
} catch {
|
||||
} catch (e) {
|
||||
console.warn('[FileSystemService] Backup timestamp file unreadable:', (e as Error).message);
|
||||
return { exists: true, timestamp: null };
|
||||
}
|
||||
} catch {
|
||||
|
||||
@@ -7,7 +7,8 @@ function getUnixShell() {
|
||||
try {
|
||||
execSync('which bash', { stdio: 'ignore' });
|
||||
return 'bash';
|
||||
} catch {
|
||||
} catch (e) {
|
||||
console.warn('[HostTerminalService] bash not found, falling back to sh:', (e as Error).message);
|
||||
return 'sh';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -180,7 +180,9 @@ export class MonitorService {
|
||||
totalReclaimableBytes += bytes;
|
||||
}
|
||||
}
|
||||
} catch (e) { }
|
||||
} catch (e) {
|
||||
console.warn('[MonitorService] Failed to parse Docker system df output:', e);
|
||||
}
|
||||
}
|
||||
|
||||
const reclaimGb = totalReclaimableBytes / (1024 * 1024 * 1024);
|
||||
|
||||
@@ -18,7 +18,8 @@ export function isValidRemoteUrl(
|
||||
let url: URL;
|
||||
try {
|
||||
url = new URL(raw);
|
||||
} catch {
|
||||
} catch (e) {
|
||||
console.warn('[Validation] URL parse failure:', (e as Error).message, '— input:', raw);
|
||||
return {
|
||||
valid: false,
|
||||
reason: 'API URL must be a valid URL (e.g. https://my-server.example.com:3000)',
|
||||
|
||||
Reference in New Issue
Block a user