map POSIX errno to actionable IPC error kinds (T-79)

pane.spawn (via PtyException.errno) and editor.open (via
FileSystemException.osError.errorCode) now route ENOENT to
not_found, EACCES/EPERM to user_error with a permissions hint,
EISDIR/ENOTDIR/EEXIST to distinct user-error/conflict, and
EMFILE/ENFILE to tool_error with a "fd limit hit" hint. The
mapping lives in lib/src/ipc/errno_mapping.dart so other handlers
can adopt the same surface as they pick up errno-bearing failures.

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2026-05-06 09:56:04 +02:00
co-authored by Claude
parent 41fd586ca0
commit 5fb3df84ed
6 changed files with 224 additions and 5 deletions
+19
View File
@@ -9,8 +9,11 @@
/// one-to-one onto these in `bin/clide.dart`.
library;
import 'dart:io' show FileSystemException;
import '../editor/registry.dart';
import '../ipc/envelope.dart';
import '../ipc/errno_mapping.dart';
import '../ipc/schema_v1.dart';
import 'dispatcher.dart';
@@ -64,6 +67,22 @@ Future<IpcResponse> _open(IpcRequest req, EditorRegistry r) async {
try {
final buf = await r.open(path);
return IpcResponse.ok(id: req.id, data: buf.toJson());
} on FileSystemException catch (e) {
final errno = e.osError?.errorCode;
if (errno != null) {
return IpcResponse.err(
id: req.id,
error: errnoToIpcError(errno: errno, op: 'editor.open', target: path),
);
}
return IpcResponse.err(
id: req.id,
error: IpcError(
code: IpcExitCode.toolError,
kind: IpcErrorKind.toolError,
message: 'editor.open failed: ${e.message}',
),
);
} catch (e) {
return IpcResponse.err(
id: req.id,
+22
View File
@@ -12,9 +12,11 @@ library;
import 'dart:convert';
import '../ipc/envelope.dart';
import '../ipc/errno_mapping.dart';
import '../ipc/schema_v1.dart';
import '../panes/pane.dart';
import '../panes/registry.dart';
import '../pty/errors.dart';
import 'dispatcher.dart';
void registerPaneCommands(DaemonDispatcher d, PaneRegistry registry) {
@@ -84,6 +86,26 @@ Future<IpcResponse> _spawn(IpcRequest req, PaneRegistry registry) async {
title: args['title'] as String?,
);
return IpcResponse.ok(id: req.id, data: pane.toJson());
} on PtyException catch (e) {
final errno = e.errno;
if (errno != null) {
return IpcResponse.err(
id: req.id,
error: errnoToIpcError(
errno: errno,
op: 'pane.spawn',
target: argv.isNotEmpty ? argv.first : null,
),
);
}
return IpcResponse.err(
id: req.id,
error: IpcError(
code: IpcExitCode.toolError,
kind: IpcErrorKind.toolError,
message: 'pane.spawn failed: ${e.message}',
),
);
} catch (e) {
return IpcResponse.err(
id: req.id,
+103
View File
@@ -0,0 +1,103 @@
/// Map POSIX errno values to IPC error envelopes with actionable
/// messages. Used by command handlers that wrap syscall-backed work
/// (PTY spawn, file open) so the client can distinguish "binary not
/// found" from "permission denied" from "system limit hit" instead
/// of seeing the same generic `tool_error: foo failed`.
library;
import 'envelope.dart';
import 'schema_v1.dart';
/// Selected POSIX errno values we map specially. Others fall through
/// to a generic toolError. Values match Linux glibc and macOS Darwin
/// (the two platforms that share the same numbers for these entries).
abstract class PosixErrno {
static const int eperm = 1;
static const int enoent = 2;
static const int esrch = 3;
static const int eio = 5;
static const int ebadf = 9;
static const int eagain = 11;
static const int enomem = 12;
static const int eacces = 13;
static const int eexist = 17;
static const int enotdir = 20;
static const int eisdir = 21;
static const int emfile = 24;
static const int enfile = 23;
static const int epipe = 32;
}
/// Build an [IpcError] from a POSIX [errno] for an operation [op]
/// (e.g. `pane.spawn`, `editor.open`) on optional [target] (a path,
/// command name, etc.). The returned error uses `notFound`,
/// `userError`, or `toolError` based on what's actionable.
IpcError errnoToIpcError({
required int errno,
required String op,
String? target,
String? raw,
}) {
final what = target != null ? ' ($target)' : '';
switch (errno) {
case PosixErrno.enoent:
return IpcError(
code: IpcExitCode.notFound,
kind: IpcErrorKind.notFound,
message: '$op: not found$what',
);
case PosixErrno.eacces:
case PosixErrno.eperm:
return IpcError(
code: IpcExitCode.userError,
kind: IpcErrorKind.userError,
message: '$op: permission denied$what',
hint: 'check file permissions or run with appropriate access',
);
case PosixErrno.eisdir:
return IpcError(
code: IpcExitCode.userError,
kind: IpcErrorKind.userError,
message: '$op: is a directory$what',
);
case PosixErrno.enotdir:
return IpcError(
code: IpcExitCode.userError,
kind: IpcErrorKind.userError,
message: '$op: not a directory$what',
);
case PosixErrno.eexist:
return IpcError(
code: IpcExitCode.conflict,
kind: IpcErrorKind.conflict,
message: '$op: already exists$what',
);
case PosixErrno.emfile:
case PosixErrno.enfile:
return IpcError(
code: IpcExitCode.toolError,
kind: IpcErrorKind.toolError,
message: '$op: too many open files',
hint: 'system or per-process file descriptor limit reached',
);
case PosixErrno.enomem:
return IpcError(
code: IpcExitCode.toolError,
kind: IpcErrorKind.toolError,
message: '$op: out of memory',
);
case PosixErrno.eagain:
return IpcError(
code: IpcExitCode.toolError,
kind: IpcErrorKind.toolError,
message: '$op: resource temporarily unavailable',
hint: 'retry may succeed',
);
default:
return IpcError(
code: IpcExitCode.toolError,
kind: IpcErrorKind.toolError,
message: '$op failed${raw != null ? ': $raw' : ' (errno=$errno)'}',
);
}
}