This commit is contained in:
jarek committed 2026-06-19 08:21:22 +02:00
1 parent efb634701c
commit ce12d7ae9f
17 files changed
+284 -203

No files matched your search

@@ -33,47 +33,64 @@ export const GET: RequestHandler = async ({ params, url, cookies }) => {
try {
const containerId = params.id;
const availableShells: string[] = [];
const shellNames = SHELLS_TO_CHECK.map(s => s.path.split('/').pop()!);
const namedPaths = new Map<string, string>(); // name → resolved absolute path
// Check each shell by testing if the file exists and is executable
// Use a single command to check all shells at once for efficiency
const checkCommand = SHELLS_TO_CHECK.map(s =>
`test -x ${s.path} && echo "${s.path}"`
).join('; ');
// Resolve each shell through the container's own PATH using `command -v`,
// so that images with shells at non-standard locations (e.g. /usr/bin/sh
// where /bin/sh isn't a symlink, or PATH ordering that shadows the
// shell-builtin `test`) still report correctly. `exit 0` at the end
// keeps the exec exit code clean even when some shells are absent —
// otherwise execInContainer treats non-zero as a thrown error and the
// valid output is discarded (issue #1189).
const probe =
`for s in ${shellNames.join(' ')}; do ` +
`p=$(command -v $s 2>/dev/null) && [ -n "$p" ] && echo "$s:$p"; ` +
`done; exit 0`;
try {
const output = await execInContainer(
containerId,
['sh', '-c', checkCommand],
['sh', '-c', probe],
envIdNum
);
// Parse output - each line is an available shell path
const lines = output.trim().split('\n').filter(Boolean);
availableShells.push(...lines);
for (const line of output.trim().split('\n').filter(Boolean)) {
const colon = line.indexOf(':');
if (colon < 0) continue;
const name = line.slice(0, colon);
const resolved = line.slice(colon + 1);
if (resolved.startsWith('/')) namedPaths.set(name, resolved);
}
} catch {
// If even sh fails, try checking with test commands individually
// This handles edge cases where sh might not be available
// `sh` itself was not invocable. Fall back to probing the canonical
// paths directly with a non-shell-resolved test — exec each one
// against its absolute path and rely on docker exec's own
// "executable not found" failure to indicate absence. This handles
// the rare image where there's no `sh` at all but bash exists.
for (const shell of SHELLS_TO_CHECK) {
try {
await execInContainer(
containerId,
['test', '-x', shell.path],
[shell.path, '-c', 'exit 0'],
envIdNum
);
availableShells.push(shell.path);
const name = shell.path.split('/').pop()!;
namedPaths.set(name, shell.path);
} catch {
// Shell not available, continue to next
// Shell not available at this path; try the next.
}
}
}
const availableShells = Array.from(namedPaths.values());
// Determine default shell - prefer bash, then sh, then first available
let defaultShell: string | null = null;
if (availableShells.includes('/bin/bash')) {
defaultShell = '/bin/bash';
} else if (availableShells.includes('/bin/sh')) {
defaultShell = '/bin/sh';
if (namedPaths.has('bash')) {
defaultShell = namedPaths.get('bash')!;
} else if (namedPaths.has('sh')) {
defaultShell = namedPaths.get('sh')!;
} else if (availableShells.length > 0) {
defaultShell = availableShells[0];
}
@@ -81,11 +98,15 @@ export const GET: RequestHandler = async ({ params, url, cookies }) => {
return json({
shells: availableShells,
defaultShell,
allShells: SHELLS_TO_CHECK.map(s => ({
path: s.path,
label: s.label,
available: availableShells.includes(s.path)
}))
allShells: SHELLS_TO_CHECK.map(s => {
const name = s.path.split('/').pop()!;
const resolved = namedPaths.get(name);
return {
path: resolved ?? s.path,
label: s.label,
available: namedPaths.has(name)
};
})
});
} catch (error) {
console.error('Error detecting shells:', error);