port clide runtime to Windows (ConPTY, AF_UNIX, PATHEXT)
Bring the runtime up on Windows without disturbing the POSIX paths. PTY: introduce a platform-neutral PtySession contract with a factory that picks NativePty (posix_openpt/posix_spawn) or the new WindowsPty (ConPTY via CreatePseudoConsole). The pane registry programs against the interface; NativePty now implements it. IPC: the per-workspace AF_UNIX socket lives under %LOCALAPPDATA% and is hashed from a canonical workspace key (backslash + ASCII-folded case) so the Dart server and the C client agree despite NTFS case- insensitivity. The C client grows a Win32 shim (winsock afunix); chmod is a no-op on Windows where the per-user ACL is the gate. Toolchain: PATH probing splits on ';' and tries PATHEXT extensions; the shell defaults to PowerShell (pwsh, then powershell); tmux is treated as optional since it has no Windows build; dugite falls back to PATH git for now. Build: add `make build-windows`, a clide-cli MSVC build wrapped by ci/build_cli_windows.sh, and a ConPTY smoke-test suite that self- skips off-platform. Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
+156
-41
@@ -7,7 +7,9 @@
|
||||
* root, same definition the Flutter app uses on boot).
|
||||
* 2. Hashes that path with FNV-1a 64-bit and resolves the per-
|
||||
* workspace socket path per D-70 (Linux: $XDG_RUNTIME_DIR/clide/
|
||||
* <hash>.sock; macOS: $HOME/Library/Caches/clide/<hash>.sock).
|
||||
* <hash>.sock; macOS: $HOME/Library/Caches/clide/<hash>.sock;
|
||||
* Windows: %LOCALAPPDATA%\clide\<hash>.sock — AF_UNIX works on
|
||||
* Windows 10 1803+ via afunix.h).
|
||||
* 3. Connects, sends `{"v":1,"type":"request","id":"<pid>",
|
||||
* "cmd":"_argv","args":{"argv":[...]}}` (the server runs
|
||||
* parseArgv on it per T-125), reads the JSON-line response,
|
||||
@@ -15,21 +17,40 @@
|
||||
* exits with the response's exit code.
|
||||
*
|
||||
* Design notes:
|
||||
* - No third-party deps. Standard POSIX + a minimal JSON writer
|
||||
* (string-escape only — we never PARSE JSON, just emit argv into
|
||||
* it; the response is read whole then printed as-is to stdout).
|
||||
* - No third-party deps. Standard POSIX / Win32 + a minimal JSON
|
||||
* writer (string-escape only — we never PARSE JSON, just emit argv
|
||||
* into it; the response is read whole then printed as-is).
|
||||
* - The argv→IpcRequest translator lives in Dart (T-125). We just
|
||||
* ship argv across the wire under a sentinel cmd `_argv`; the
|
||||
* server unpacks it.
|
||||
* - Workspace-root discovery: we look for `.git` (dir OR file —
|
||||
* submodules use a file). If we don't find one walking upward,
|
||||
* exit with EX_USAGE.
|
||||
* - Windows hashes the CANONICAL workspace key: backslash
|
||||
* separators + ASCII-lower-cased UTF-8 bytes, matching
|
||||
* `canonicalWorkspaceKey` in lib/src/ipc/paths.dart. NTFS is
|
||||
* case-insensitive, so the same workspace can be spelled many
|
||||
* ways; both sides fold to one spelling before hashing.
|
||||
*
|
||||
* Build: `make clide-cli` (see Makefile). Pure C99, builds with
|
||||
* any gcc / clang / cc.
|
||||
* Build: `make clide-cli` (see Makefile). Pure C99: gcc / clang / cc
|
||||
* on POSIX, MSVC cl (+ ws2_32.lib) on Windows.
|
||||
*/
|
||||
|
||||
#ifdef _WIN32
|
||||
#define WIN32_LEAN_AND_MEAN
|
||||
#include <winsock2.h>
|
||||
#include <afunix.h>
|
||||
#include <windows.h>
|
||||
#include <process.h>
|
||||
#else
|
||||
#define _POSIX_C_SOURCE 200809L
|
||||
#include <sys/socket.h>
|
||||
#include <sys/stat.h>
|
||||
#include <sys/types.h>
|
||||
#include <sys/un.h>
|
||||
#include <unistd.h>
|
||||
#endif
|
||||
|
||||
#include <ctype.h>
|
||||
#include <errno.h>
|
||||
#include <inttypes.h>
|
||||
@@ -37,11 +58,6 @@
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/socket.h>
|
||||
#include <sys/stat.h>
|
||||
#include <sys/types.h>
|
||||
#include <sys/un.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#ifdef __APPLE__
|
||||
#include <TargetConditionals.h>
|
||||
@@ -52,6 +68,32 @@
|
||||
#define EX_OSERR 71
|
||||
#define EX_UNAVAILABLE 69
|
||||
|
||||
/* -- tiny platform shim ------------------------------------------------- */
|
||||
|
||||
#ifdef _WIN32
|
||||
typedef SOCKET sock_t;
|
||||
#define NET_INVALID INVALID_SOCKET
|
||||
static int net_read(sock_t s, char *buf, int n) { return recv(s, buf, n, 0); }
|
||||
static int net_write(sock_t s, const char *buf, int n) { return send(s, buf, n, 0); }
|
||||
static void net_close(sock_t s) { closesocket(s); }
|
||||
static int net_errno(void) { return WSAGetLastError(); }
|
||||
static const char *net_strerror(int e) {
|
||||
static char msg[256];
|
||||
snprintf(msg, sizeof(msg), "winsock error %d", e);
|
||||
return msg;
|
||||
}
|
||||
#define clide_getpid _getpid
|
||||
#else
|
||||
typedef int sock_t;
|
||||
#define NET_INVALID (-1)
|
||||
static int net_read(sock_t s, char *buf, int n) { return (int)read(s, buf, (size_t)n); }
|
||||
static int net_write(sock_t s, const char *buf, int n) { return (int)write(s, buf, (size_t)n); }
|
||||
static void net_close(sock_t s) { close(s); }
|
||||
static int net_errno(void) { return errno; }
|
||||
static const char *net_strerror(int e) { return strerror(e); }
|
||||
#define clide_getpid getpid
|
||||
#endif
|
||||
|
||||
static const uint64_t FNV_OFFSET = 0xcbf29ce484222325ULL;
|
||||
static const uint64_t FNV_PRIME = 0x100000001b3ULL;
|
||||
|
||||
@@ -65,6 +107,64 @@ static void fnv1a64_hex(const char *s, char out[17]) {
|
||||
snprintf(out, 17, "%016" PRIx64, h);
|
||||
}
|
||||
|
||||
#ifdef _WIN32
|
||||
|
||||
/* Walk CWD upward looking for `.git` using the wide API (the path can
|
||||
* contain anything; ANSI getcwd would mangle non-ACP characters), then
|
||||
* emit the CANONICAL UTF-8 key: backslashes + ASCII-folded lower case.
|
||||
* Mirrors `canonicalWorkspaceKey` in lib/src/ipc/paths.dart. */
|
||||
static int find_workspace_root(const char *start, char *out, size_t out_size) {
|
||||
(void)start; /* CWD-only on Windows; start override is unused. */
|
||||
wchar_t cwd[4096];
|
||||
DWORD n = GetCurrentDirectoryW(4096, cwd);
|
||||
if (n == 0 || n >= 4096) return -1;
|
||||
while (1) {
|
||||
size_t len = wcslen(cwd);
|
||||
wchar_t probe[4200];
|
||||
_snwprintf(probe, 4200, (len > 0 && cwd[len - 1] == L'\\') ? L"%s.git" : L"%s\\.git", cwd);
|
||||
probe[4199] = L'\0';
|
||||
if (GetFileAttributesW(probe) != INVALID_FILE_ATTRIBUTES) {
|
||||
int r = WideCharToMultiByte(CP_UTF8, 0, cwd, -1, out, (int)out_size, NULL, NULL);
|
||||
if (r <= 0) return -1;
|
||||
/* Canonical fold: '/' -> '\', ASCII upper -> lower. UTF-8
|
||||
* continuation bytes have the high bit set, so the ASCII
|
||||
* fold never touches multi-byte sequences. */
|
||||
for (char *p = out; *p; p++) {
|
||||
if (*p == '/') *p = '\\';
|
||||
else if (*p >= 'A' && *p <= 'Z') *p = (char)(*p + 32);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
/* Climb one. `C:\foo` -> `C:\`; stop once the drive/UNC root
|
||||
* itself has been probed. */
|
||||
wchar_t *slash = wcsrchr(cwd, L'\\');
|
||||
if (!slash) return -1;
|
||||
if (len <= 3 && cwd[1] == L':') return -1; /* at "X:\" already */
|
||||
if (slash == cwd + 2 && cwd[1] == L':') {
|
||||
cwd[3] = L'\0'; /* keep the root's backslash: "X:\" */
|
||||
} else if (slash == cwd) {
|
||||
return -1;
|
||||
} else {
|
||||
*slash = L'\0';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* `%LOCALAPPDATA%\clide\<hash>.sock` */
|
||||
static int socket_path_for(const char *workspace_root, char *out, size_t out_size) {
|
||||
char hash[17];
|
||||
fnv1a64_hex(workspace_root, hash);
|
||||
const char *local = getenv("LOCALAPPDATA");
|
||||
if (!local || !*local) {
|
||||
const char *prof = getenv("USERPROFILE");
|
||||
if (!prof || !*prof) return -1;
|
||||
return snprintf(out, out_size, "%s\\AppData\\Local\\clide\\%s.sock", prof, hash);
|
||||
}
|
||||
return snprintf(out, out_size, "%s\\clide\\%s.sock", local, hash);
|
||||
}
|
||||
|
||||
#else /* !_WIN32 */
|
||||
|
||||
/* Walk `start` upward looking for an entry named `.git`. Writes the
|
||||
* containing directory into `out` (PATH_MAX). Returns 0 on success,
|
||||
* -1 if no .git was found before /. */
|
||||
@@ -112,25 +212,39 @@ static int socket_path_for(const char *workspace_root, char *out, size_t out_siz
|
||||
#endif
|
||||
}
|
||||
|
||||
/* Open a UNIX-domain stream socket connected to `path`. Returns fd
|
||||
* on success, -1 on failure (errno set). */
|
||||
static int connect_unix(const char *path) {
|
||||
int fd = socket(AF_UNIX, SOCK_STREAM, 0);
|
||||
if (fd < 0) return -1;
|
||||
#endif /* _WIN32 */
|
||||
|
||||
/* Open a UNIX-domain stream socket connected to `path`. Returns the
|
||||
* socket on success, NET_INVALID on failure (net_errno() set). */
|
||||
static sock_t connect_unix(const char *path) {
|
||||
#ifdef _WIN32
|
||||
WSADATA wsa;
|
||||
if (WSAStartup(MAKEWORD(2, 2), &wsa) != 0) return NET_INVALID;
|
||||
SOCKADDR_UN addr;
|
||||
#else
|
||||
struct sockaddr_un addr;
|
||||
#endif
|
||||
sock_t fd = socket(AF_UNIX, SOCK_STREAM, 0);
|
||||
if (fd == NET_INVALID) return NET_INVALID;
|
||||
memset(&addr, 0, sizeof(addr));
|
||||
addr.sun_family = AF_UNIX;
|
||||
if (strlen(path) >= sizeof(addr.sun_path)) {
|
||||
close(fd);
|
||||
net_close(fd);
|
||||
#ifndef _WIN32
|
||||
errno = ENAMETOOLONG;
|
||||
return -1;
|
||||
#endif
|
||||
return NET_INVALID;
|
||||
}
|
||||
strncpy(addr.sun_path, path, sizeof(addr.sun_path) - 1);
|
||||
if (connect(fd, (struct sockaddr *)&addr, sizeof(addr)) < 0) {
|
||||
int saved = errno;
|
||||
close(fd);
|
||||
int saved = net_errno();
|
||||
net_close(fd);
|
||||
#ifndef _WIN32
|
||||
errno = saved;
|
||||
return -1;
|
||||
#else
|
||||
WSASetLastError(saved);
|
||||
#endif
|
||||
return NET_INVALID;
|
||||
}
|
||||
return fd;
|
||||
}
|
||||
@@ -165,11 +279,11 @@ static void json_escape(const char *s, char *out, size_t out_size) {
|
||||
|
||||
/* Build the request envelope and write it to `out`. Returns 0 on
|
||||
* success, -1 if any input was too large. */
|
||||
static int build_request(int argc, char **argv, pid_t pid, char *out, size_t out_size) {
|
||||
static int build_request(int argc, char **argv, long long pid, char *out, size_t out_size) {
|
||||
/* Compute argv array size: each arg gets its own escaped JSON. */
|
||||
int n = snprintf(out, out_size,
|
||||
"{\"type\":\"request\",\"v\":1,\"id\":\"c%lld\",\"cmd\":\"_argv\",\"args\":{\"argv\":[",
|
||||
(long long)pid);
|
||||
pid);
|
||||
if (n < 0 || (size_t)n >= out_size) return -1;
|
||||
for (int i = 0; i < argc; i++) {
|
||||
char esc[4096];
|
||||
@@ -181,15 +295,17 @@ static int build_request(int argc, char **argv, pid_t pid, char *out, size_t out
|
||||
return (n < 0 || (size_t)n >= out_size) ? -1 : 0;
|
||||
}
|
||||
|
||||
/* Read one line (terminated by \n) from fd into out. Returns 0 on
|
||||
* success, -1 on EOF / error. The trailing \n is stripped. */
|
||||
static int read_line(int fd, char *out, size_t out_size) {
|
||||
/* Read one line (terminated by \n) from the socket into out. Returns
|
||||
* 0 on success, -1 on EOF / error. The trailing \n is stripped. */
|
||||
static int read_line(sock_t fd, char *out, size_t out_size) {
|
||||
size_t i = 0;
|
||||
while (i + 1 < out_size) {
|
||||
char c;
|
||||
ssize_t r = read(fd, &c, 1);
|
||||
int r = net_read(fd, &c, 1);
|
||||
if (r <= 0) {
|
||||
#ifndef _WIN32
|
||||
if (r < 0 && errno == EINTR) continue;
|
||||
#endif
|
||||
return -1;
|
||||
}
|
||||
if (c == '\n') {
|
||||
@@ -263,32 +379,31 @@ int main(int argc, char **argv) {
|
||||
return EX_SOFTWARE;
|
||||
}
|
||||
|
||||
int fd = connect_unix(sock_path);
|
||||
if (fd < 0) {
|
||||
fprintf(stderr, "clide: cannot connect to %s: %s\n", sock_path, strerror(errno));
|
||||
sock_t fd = connect_unix(sock_path);
|
||||
if (fd == NET_INVALID) {
|
||||
fprintf(stderr, "clide: cannot connect to %s: %s\n", sock_path, net_strerror(net_errno()));
|
||||
return EX_UNAVAILABLE;
|
||||
}
|
||||
|
||||
/* Build + send request. Worst-case envelope sizing: argv totals
|
||||
* plus JSON overhead. 64 KB envelope handles 4 KB args * 16. */
|
||||
char req[65536];
|
||||
if (build_request(argc - 1, argv + 1, getpid(), req, sizeof(req)) != 0) {
|
||||
if (build_request(argc - 1, argv + 1, (long long)clide_getpid(), req, sizeof(req)) != 0) {
|
||||
fprintf(stderr, "clide: request payload too large\n");
|
||||
close(fd);
|
||||
net_close(fd);
|
||||
return EX_USAGE;
|
||||
}
|
||||
if (write(fd, req, strlen(req)) != (ssize_t)strlen(req)) {
|
||||
fprintf(stderr, "clide: write failed: %s\n", strerror(errno));
|
||||
close(fd);
|
||||
if (net_write(fd, req, (int)strlen(req)) != (int)strlen(req)) {
|
||||
fprintf(stderr, "clide: write failed: %s\n", net_strerror(net_errno()));
|
||||
net_close(fd);
|
||||
return EX_OSERR;
|
||||
}
|
||||
|
||||
/* Read the response — one JSON line. */
|
||||
char resp[65536];
|
||||
if (read_line(fd, resp, sizeof(resp)) != 0) {
|
||||
fprintf(stderr, "clide: response read failed: %s\n",
|
||||
errno ? strerror(errno) : "short read");
|
||||
close(fd);
|
||||
fprintf(stderr, "clide: response read failed: %s\n", net_strerror(net_errno()));
|
||||
net_close(fd);
|
||||
return EX_OSERR;
|
||||
}
|
||||
|
||||
@@ -325,14 +440,14 @@ int main(int argc, char **argv) {
|
||||
fputc('\n', stdout);
|
||||
fflush(stdout);
|
||||
}
|
||||
close(fd);
|
||||
net_close(fd);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
close(fd);
|
||||
net_close(fd);
|
||||
return 0;
|
||||
}
|
||||
close(fd);
|
||||
net_close(fd);
|
||||
const char *code_v = json_value(resp, "code", &code_len);
|
||||
const char *msg_v = json_value(resp, "message", &msg_len);
|
||||
int exit_code = code_v ? (int)strtol(code_v, NULL, 10) : EX_SOFTWARE;
|
||||
|
||||
Reference in New Issue
Block a user