`flutter build web --wasm` had been broken since the tree-sitter/PTY dart:ffi pivot. Per D-100 (resolving Q-50: keep the web "happy accident" alive), every native binding now sits behind a `dart.library.ffi` conditional import with a graceful web stub. Desktop builds are unchanged — no fidelity loss; the web target degrades (no terminal, native git, or syntax highlighting). Discriminator is `dart.library.ffi`, not `dart.library.io` — dart2wasm provides dart:io, so FFI is the only blocker. Fences: - PTY: pty_session → pty_backend_io / pty_backend_web (stub throws). - tree-sitter: pure types → syntax_result.dart; tree_sitter_service is now a facade over _ffi/_stub; tree_sitter_boot_io/stub fences TreeSitterLib.init(). - watchdog: watchdog_windows_stub (all -1 sampler). - claude ABI probe: native_abi_io/stub (was `dart:ffi show Abi`). - testmode fd-check: fd_check_io/stub. Also dart2js-safe: the 64-bit FNV literals in session_naming.dart + paths.dart (the dual JS fallback rejected them) — split into 32-bit halves, dropped a no-op 64-bit mask. Desktop/wasm hash values unchanged. CI: added a `web-wasm` job (flutter build web --wasm) so the fence can't rot. Two FFI-constructing tree-sitter tests import _ffi.dart directly (the analyzer resolves the conditional facade to the stub branch). Verified: `flutter build web --wasm` → built; `flutter analyze` clean; `make test` green. Full Playwright e2e harness wiring is the tracked follow-on. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
124 lines
5.5 KiB
Dart
124 lines
5.5 KiB
Dart
import 'dart:convert';
|
|
import 'dart:io';
|
|
|
|
/// Resolve the per-workspace Unix-domain socket path served by the
|
|
/// running clide app. Per D-70:
|
|
///
|
|
/// Linux: `$XDG_RUNTIME_DIR/clide/<hash>.sock`
|
|
/// macOS: `$HOME/Library/Caches/clide/<hash>.sock`
|
|
/// Windows: `%LOCALAPPDATA%\clide\<hash>.sock` (AF_UNIX — supported
|
|
/// by winsock since Windows 10 1803 and by dart:io)
|
|
///
|
|
/// The C `clide` client and any other consumer derive the same path
|
|
/// from the same workspace root, so server + client always agree
|
|
/// without configuration.
|
|
String workspaceSocketPath(String workspaceRoot) {
|
|
final dir = socketDirectory();
|
|
return '$dir/${_hash(canonicalWorkspaceKey(workspaceRoot))}.sock';
|
|
}
|
|
|
|
/// Canonical form of the workspace root used as the FNV hash input.
|
|
///
|
|
/// On Windows one directory has many spellings — either slash kind,
|
|
/// any letter case (NTFS is case-insensitive and getcwd preserves
|
|
/// whatever the shell typed) — so the server and the C client could
|
|
/// derive different hashes for the same workspace. Backslash +
|
|
/// ASCII-lower-case is the canonical spelling; the C client applies
|
|
/// the same byte-level fold (which is why this is NOT Unicode
|
|
/// `toLowerCase()` — the fold must be reproducible over raw UTF-8
|
|
/// bytes in C). POSIX paths pass through untouched.
|
|
String canonicalWorkspaceKey(String workspaceRoot) {
|
|
if (!Platform.isWindows) return workspaceRoot;
|
|
final folded = workspaceRoot.replaceAll('/', r'\');
|
|
final units = folded.codeUnits.map((u) => (u >= 0x41 && u <= 0x5a) ? u + 0x20 : u).toList();
|
|
return String.fromCharCodes(units);
|
|
}
|
|
|
|
/// Parent directory that holds every per-workspace socket for this
|
|
/// user. Created with `0700` on bind (see D-71; on Windows the
|
|
/// per-user ACL on `%LOCALAPPDATA%` is the equivalent gate). Exposed
|
|
/// separately so the server can prepare/perm-fix the directory before
|
|
/// binding.
|
|
String socketDirectory() {
|
|
if (Platform.isWindows) {
|
|
final local = Platform.environment['LOCALAPPDATA'];
|
|
final base = (local != null && local.isNotEmpty) ? local : '${Platform.environment['USERPROFILE'] ?? r'C:\'}\\AppData\\Local';
|
|
return '$base\\clide';
|
|
}
|
|
if (Platform.isMacOS) {
|
|
final home = Platform.environment['HOME'] ?? '/tmp';
|
|
return '$home/Library/Caches/clide';
|
|
}
|
|
final xdg = Platform.environment['XDG_RUNTIME_DIR'];
|
|
final base = (xdg != null && xdg.isNotEmpty) ? xdg : '/tmp';
|
|
return '$base/clide';
|
|
}
|
|
|
|
/// Persistent per-platform directory for crash-survivable logs (T-425).
|
|
///
|
|
/// Linux: `$XDG_STATE_HOME/clide/logs` (else `$HOME/.local/state/...`)
|
|
/// macOS: `$HOME/Library/Logs/clide`
|
|
/// Windows: `%LOCALAPPDATA%\clide\logs`
|
|
///
|
|
/// Unlike [socketDirectory] — which intentionally lives in an EPHEMERAL
|
|
/// runtime dir (`$XDG_RUNTIME_DIR`, `~/Library/Caches`) that the OS may wipe
|
|
/// on logout/reboot — this is a DURABLE location. The whole point of the
|
|
/// FileLogSink is that a freeze's last breadcrumbs survive the power-cycle, so
|
|
/// the log dir must outlive a reboot.
|
|
///
|
|
/// `CLIDE_LOG_DIR` overrides everything: CI points it at a workspace dir
|
|
/// outside the build tree so a wedged run's logs can be uploaded as an
|
|
/// artifact (T-436), and tests redirect it to a temp dir.
|
|
/// [env] defaults to [Platform.environment]; injectable for tests.
|
|
String logDirectory([Map<String, String>? env]) {
|
|
final e = env ?? Platform.environment;
|
|
final override = e['CLIDE_LOG_DIR'];
|
|
if (override != null && override.isNotEmpty) return override;
|
|
if (Platform.isWindows) {
|
|
final local = e['LOCALAPPDATA'];
|
|
final base = (local != null && local.isNotEmpty) ? local : '${e['USERPROFILE'] ?? r'C:\'}\\AppData\\Local';
|
|
return '$base\\clide\\logs';
|
|
}
|
|
if (Platform.isMacOS) {
|
|
final home = e['HOME'] ?? '/tmp';
|
|
return '$home/Library/Logs/clide';
|
|
}
|
|
final state = e['XDG_STATE_HOME'];
|
|
final base = (state != null && state.isNotEmpty) ? state : '${e['HOME'] ?? '/tmp'}/.local/state';
|
|
return '$base/clide/logs';
|
|
}
|
|
|
|
/// FNV-1a 64-bit hash of [s] as a 16-char lower-case hex string.
|
|
/// The C client (T-126) reproduces the same algorithm byte-for-byte
|
|
/// so server + client always agree on socket path. Not cryptographic
|
|
/// — D-70 explains why one isn't needed here. The algorithm:
|
|
///
|
|
/// h = 0xcbf29ce484222325 // FNV offset basis
|
|
/// for each byte b in utf-8(s):
|
|
/// h = (h xor b) * 0x100000001b3 mod 2^64 // FNV prime, 64-bit wrap
|
|
///
|
|
/// Reference: <http://isthe.com/chongo/tech/comp/fnv/> — FNV-1a 64-bit.
|
|
String fnv1a64Hex(String s) {
|
|
// Desktop-only (the IPC server is desktop-only per D-56). Dart VM
|
|
// ints are 64-bit; arithmetic wraps modulo 2^64 naturally.
|
|
// FNV-1a 64-bit offset basis. Split into two 32-bit halves so the dart2js
|
|
// web fallback accepts it — a 0xcbf2…2325 literal "can't be represented
|
|
// exactly in JavaScript" (T-438). Correct on the VM/wasm (int64); the web
|
|
// build never hashes a socket path (the IPC server is desktop-only).
|
|
var h = (0xcbf29ce4 << 32) | 0x84222325;
|
|
const prime = 0x100000001b3;
|
|
final bytes = utf8.encode(s);
|
|
for (final b in bytes) {
|
|
h ^= b;
|
|
h = h * prime; // wraps mod 2^64 on the VM (signed int64)
|
|
}
|
|
// Dart's `int` is signed 64-bit on the VM; once the high bit lights
|
|
// up, `toRadixString` would emit a leading minus. Split into two
|
|
// unsigned 32-bit halves (>>> is logical shift) and concatenate.
|
|
final hi = (h >>> 32) & 0xffffffff;
|
|
final lo = h & 0xffffffff;
|
|
return '${hi.toRadixString(16).padLeft(8, '0')}${lo.toRadixString(16).padLeft(8, '0')}';
|
|
}
|
|
|
|
String _hash(String s) => fnv1a64Hex(s);
|