mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-11-02 07:43:42 +08:00
The native compiler sends most checker requests one at a time and blocks in recv for each reply. The server answers most of them in a few microseconds, and a reader the reply wakes takes tens of microseconds more per round trip, over about 130,000 round trips on a large program. scriptc_ts7_read now polls the socket with MSG_DONTWAIT for up to 200 us before it blocks. Windows keeps its blocking pipe read.
339 lines
12 KiB
C
339 lines
12 KiB
C
#ifndef _WIN32
|
|
#define _POSIX_C_SOURCE 200809L
|
|
#endif
|
|
/* macOS declares mkdtemp only with the Darwin extensions visible. */
|
|
#if defined(__APPLE__) && !defined(_DARWIN_C_SOURCE)
|
|
#define _DARWIN_C_SOURCE 1
|
|
#endif
|
|
#include "ts7-process.h"
|
|
#include <assert.h>
|
|
#include <errno.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#ifdef _WIN32
|
|
#include <windows.h>
|
|
#else
|
|
#include <fcntl.h>
|
|
#include <poll.h>
|
|
#include <signal.h>
|
|
#include <sys/stat.h>
|
|
#include <sys/wait.h>
|
|
#include <unistd.h>
|
|
#endif
|
|
|
|
static const char *executable;
|
|
static const char *callbacks = "readFile,fileExists";
|
|
|
|
static uint32_t start_with(const char *mode, int timing, int libraries) {
|
|
return scriptc_ts7_open((const uint8_t *)executable, strlen(executable),
|
|
(const uint8_t *)mode, strlen(mode),
|
|
(const uint8_t *)callbacks, strlen(callbacks), (uint8_t)timing, (uint8_t)libraries);
|
|
}
|
|
|
|
static uint32_t start(const char *mode, int timing) { return start_with(mode, timing, 0); }
|
|
|
|
static void fail_message(void) {
|
|
uint8_t message[1024];
|
|
int32_t size = scriptc_ts7_error(message, sizeof(message));
|
|
assert(size > 0);
|
|
assert(scriptc_ts7_error(message, 3) == 3);
|
|
assert(scriptc_ts7_error(NULL, 0) == 0);
|
|
}
|
|
|
|
static void read_exact(uint32_t id, uint8_t *bytes, size_t size) {
|
|
size_t at = 0;
|
|
while (at < size) {
|
|
int32_t count = scriptc_ts7_read(id, bytes, size, (uint32_t)at, (uint32_t)(size - at));
|
|
assert(count > 0 && (size_t)count <= size - at);
|
|
at += (size_t)count;
|
|
}
|
|
}
|
|
|
|
static void write_exact(uint32_t id, const uint8_t *bytes, size_t size, uint32_t offset, uint32_t length) {
|
|
uint32_t at = 0;
|
|
while (at < length) {
|
|
int32_t count = scriptc_ts7_write(id, bytes, size, offset + at, length - at);
|
|
assert(count > 0 && (uint32_t)count <= length - at);
|
|
at += (uint32_t)count;
|
|
}
|
|
}
|
|
|
|
static void assert_reaped(uint32_t pid) {
|
|
#ifdef _WIN32
|
|
HANDLE process = OpenProcess(SYNCHRONIZE, FALSE, pid);
|
|
if (process != NULL) {
|
|
assert(WaitForSingleObject(process, 0) == WAIT_OBJECT_0);
|
|
CloseHandle(process);
|
|
} else assert(GetLastError() == ERROR_INVALID_PARAMETER);
|
|
#else
|
|
int status;
|
|
assert(waitpid((pid_t)pid, &status, WNOHANG) == -1 && errno == ECHILD);
|
|
#endif
|
|
}
|
|
|
|
static unsigned resource_count(void) {
|
|
#ifdef _WIN32
|
|
DWORD count;
|
|
assert(GetProcessHandleCount(GetCurrentProcess(), &count));
|
|
return (unsigned)count;
|
|
#else
|
|
unsigned count = 0;
|
|
for (int fd = 0; fd < 1024; fd++) if (fcntl(fd, F_GETFD) != -1) count++;
|
|
return count;
|
|
#endif
|
|
}
|
|
|
|
static void argument_cases(void) {
|
|
const char *values[] = {
|
|
"", "a b", "double\"quote", "end\\", "many\\\\\"quotes\\\\", "a;&$()`'b", "h\xc3\xa9llo \xf0\x9f\x8c\x8d",
|
|
};
|
|
for (size_t i = 0; i < sizeof(values) / sizeof(values[0]); i++) {
|
|
for (int flags = 0; flags < 4; flags++) {
|
|
int timing = flags & 1, libraries = flags >> 1;
|
|
char mode[256], expected[512];
|
|
snprintf(mode, sizeof(mode), "argv:%s", values[i]);
|
|
snprintf(expected, sizeof(expected), "%s\n--callbacks=%s\n%s\n%s", values[i], callbacks,
|
|
timing ? "timing" : "plain", libraries ? "libraries from disk\n" : "");
|
|
uint32_t id = start_with(mode, timing, libraries);
|
|
assert(id != 0);
|
|
uint8_t actual[512];
|
|
read_exact(id, actual, strlen(expected));
|
|
assert(memcmp(actual, expected, strlen(expected)) == 0);
|
|
assert(scriptc_ts7_read(id, actual, sizeof(actual), 0, sizeof(actual)) == 0);
|
|
uint32_t pid = scriptc_ts7_pid(id);
|
|
scriptc_ts7_close(id);
|
|
assert_reaped(pid);
|
|
}
|
|
}
|
|
}
|
|
|
|
static void exchange_cases(void) {
|
|
uint32_t ids[3];
|
|
for (int i = 0; i < 3; i++) { ids[i] = start("echo", 0); assert(ids[i] != 0); }
|
|
assert(ids[0] != ids[1] && ids[1] != ids[2]);
|
|
uint8_t bytes[1028], result[1028];
|
|
for (unsigned round = 0; round < 200; round++) {
|
|
for (unsigned i = 0; i < sizeof(bytes); i++) bytes[i] = (uint8_t)(i * 31 + round);
|
|
memset(result, 0xcc, sizeof(result));
|
|
uint32_t id = ids[round % 3];
|
|
write_exact(id, bytes, sizeof(bytes), 2, 1024);
|
|
size_t at = 2;
|
|
while (at < 1026) {
|
|
int32_t count = scriptc_ts7_read(id, result, sizeof(result), (uint32_t)at, (uint32_t)(1026 - at));
|
|
assert(count > 0);
|
|
at += (size_t)count;
|
|
}
|
|
assert(memcmp(bytes + 2, result + 2, 1024) == 0);
|
|
assert(result[0] == 0xcc && result[1] == 0xcc && result[1026] == 0xcc && result[1027] == 0xcc);
|
|
}
|
|
for (int i = 0; i < 3; i++) {
|
|
uint32_t id = ids[i], pid = scriptc_ts7_pid(id);
|
|
assert(scriptc_ts7_read(id, NULL, 0, 0, 0) == 0);
|
|
assert(scriptc_ts7_write(id, NULL, 0, 0, 0) == 0);
|
|
assert(scriptc_ts7_read(id, result, sizeof(result), sizeof(result) + 1, 0) == -1);
|
|
assert(scriptc_ts7_read(id, result, sizeof(result), 1, sizeof(result)) == -1);
|
|
assert(scriptc_ts7_write(id, NULL, 1, 0, 1) == -1);
|
|
assert(scriptc_ts7_write(id, bytes, sizeof(bytes), UINT32_MAX, 2) == -1);
|
|
fail_message();
|
|
scriptc_ts7_close(id);
|
|
scriptc_ts7_close(id);
|
|
assert_reaped(pid);
|
|
assert(scriptc_ts7_read(id, result, sizeof(result), 0, 1) == -1);
|
|
assert(scriptc_ts7_write(id, bytes, sizeof(bytes), 0, 1) == -1);
|
|
}
|
|
uint32_t newer = start("echo", 0);
|
|
assert(newer > ids[2]);
|
|
scriptc_ts7_close(ids[0]);
|
|
assert(scriptc_ts7_pid(newer) != 0);
|
|
scriptc_ts7_close(newer);
|
|
}
|
|
|
|
/* A reply that arrives after the read's poll window: the read waits for it. */
|
|
static void slow_reply_cases(void) {
|
|
uint32_t id = start("slow-echo", 0);
|
|
assert(id != 0);
|
|
const uint8_t bytes[3] = {1, 2, 3};
|
|
uint8_t result[3] = {0};
|
|
for (unsigned round = 0; round < 3; round++) {
|
|
write_exact(id, bytes, sizeof(bytes), 0, sizeof(bytes));
|
|
read_exact(id, result, sizeof(result));
|
|
assert(memcmp(bytes, result, sizeof(bytes)) == 0);
|
|
}
|
|
scriptc_ts7_close(id);
|
|
}
|
|
|
|
static void failure_cases(void) {
|
|
uint8_t bytes[1] = {42};
|
|
const char *saved = executable;
|
|
executable = "/scriptc/does-not-exist/tsgo";
|
|
for (int i = 0; i < 30; i++) { assert(start("echo", 0) == 0); fail_message(); }
|
|
executable = saved;
|
|
assert(scriptc_ts7_open(NULL, 0, bytes, 1, bytes, 1, 0, 0) == 0);
|
|
assert(scriptc_ts7_open((const uint8_t *)"abc\0def", 7, bytes, 1, bytes, 1, 0, 0) == 0);
|
|
assert(scriptc_ts7_open((const uint8_t *)executable, strlen(executable), NULL, 0, bytes, 1, 0, 0) == 0);
|
|
assert(scriptc_ts7_open((const uint8_t *)executable, strlen(executable), bytes, 1, NULL, 0, 0, 0) == 0);
|
|
for (int i = 0; i < 30; i++) {
|
|
uint32_t id = start("exit", 0);
|
|
assert(id != 0);
|
|
assert(scriptc_ts7_read(id, bytes, 1, 0, 1) == 0);
|
|
// The peer has closed: writes must report an error, never SIGPIPE.
|
|
assert(scriptc_ts7_write(id, bytes, 1, 0, 1) == -1);
|
|
uint32_t pid = scriptc_ts7_pid(id);
|
|
scriptc_ts7_close(id);
|
|
assert_reaped(pid);
|
|
}
|
|
for (int i = 0; i < 3; i++) {
|
|
uint32_t id = start("stall", 0);
|
|
assert(id != 0);
|
|
read_exact(id, bytes, 1);
|
|
assert(bytes[0] == '!');
|
|
}
|
|
scriptc_ts7_close_all();
|
|
scriptc_ts7_close_all();
|
|
scriptc_ts7_close(0);
|
|
scriptc_ts7_close(UINT32_MAX);
|
|
}
|
|
|
|
#ifndef _WIN32
|
|
static uint32_t attach(const char *name, const char *mode, uint32_t memory) {
|
|
return scriptc_ts7_attach((const uint8_t *)name, strlen(name), (const uint8_t *)executable, strlen(executable),
|
|
(const uint8_t *)mode, strlen(mode), (const uint8_t *)callbacks, strlen(callbacks),
|
|
0, 0, 1, memory);
|
|
}
|
|
|
|
static void expect_line(uint32_t id, const char *expected) {
|
|
char line[512];
|
|
size_t at = 0;
|
|
do {
|
|
assert(at + 1 < sizeof(line));
|
|
read_exact(id, (uint8_t *)line + at, 1);
|
|
} while (line[at++] != '\n');
|
|
line[at] = '\0';
|
|
assert(strcmp(line, expected) == 0);
|
|
}
|
|
|
|
static void attach_cases(void) {
|
|
char runtime[] = "/tmp/scriptc-ts7-attach-XXXXXX";
|
|
assert(mkdtemp(runtime) != NULL);
|
|
assert(setenv("XDG_RUNTIME_DIR", runtime, 1) == 0);
|
|
assert(unsetenv("TSC_TS_LISTEN_MEMORY") == 0);
|
|
char directory[256], socket_path[512], expected[600];
|
|
uint8_t bytes[4];
|
|
snprintf(directory, sizeof(directory), "%s/scriptc", runtime);
|
|
snprintf(socket_path, sizeof(socket_path), "%s/server.sock", directory);
|
|
// The first build starts the server with its idle time and memory limit,
|
|
// in the root directory. The server holds none of the build's descriptors,
|
|
// such as an output pipe its caller reads to the end.
|
|
int output[2];
|
|
assert(pipe(output) == 0);
|
|
uint32_t first = attach("server", "listen", 123);
|
|
assert(first != 0 && scriptc_ts7_pid(first) == 0);
|
|
close(output[1]);
|
|
struct pollfd ended = {output[0], POLLIN, 0};
|
|
assert(poll(&ended, 1, 5000) == 1);
|
|
assert(read(output[0], bytes, 1) == 0);
|
|
close(output[0]);
|
|
expect_line(first, "client 1\n");
|
|
snprintf(expected, sizeof(expected), "%s 1\n", socket_path);
|
|
expect_line(first, expected);
|
|
expect_line(first, "123\n");
|
|
expect_line(first, "/\n");
|
|
write_exact(first, (const uint8_t *)"ping", 4, 0, 4);
|
|
read_exact(first, bytes, 4);
|
|
assert(memcmp(bytes, "ping", 4) == 0);
|
|
// A build that finds the server in use gets no handle.
|
|
for (int i = 0; i < 3; i++) { assert(attach("server", "listen", 123) == 0); fail_message(); }
|
|
scriptc_ts7_close(first);
|
|
scriptc_ts7_close(first);
|
|
// A later build connects to the same server.
|
|
uint32_t second = attach("server", "listen", 123);
|
|
assert(second != 0);
|
|
expect_line(second, "client 2\n");
|
|
scriptc_ts7_close(second);
|
|
// An executable without --listen exits as it starts.
|
|
assert(attach("old", "listen-exit", 0) == 0);
|
|
fail_message();
|
|
// A server directory others can access is not used.
|
|
assert(chmod(directory, 0755) == 0);
|
|
assert(attach("server", "listen", 123) == 0);
|
|
fail_message();
|
|
assert(chmod(directory, 0700) == 0);
|
|
// A name that would leave the directory, or make the path too long.
|
|
assert(attach("../server", "listen", 123) == 0);
|
|
fail_message();
|
|
char long_name[200];
|
|
memset(long_name, 'x', sizeof(long_name) - 1);
|
|
long_name[sizeof(long_name) - 1] = '\0';
|
|
assert(attach(long_name, "listen", 123) == 0);
|
|
fail_message();
|
|
// The server exits after its idle second and removes its socket.
|
|
unlink(socket_path);
|
|
snprintf(expected, sizeof(expected), "%s.lock", socket_path);
|
|
unlink(expected);
|
|
snprintf(expected, sizeof(expected), "%s/old.sock.lock", directory);
|
|
unlink(expected);
|
|
rmdir(directory);
|
|
rmdir(runtime);
|
|
assert(unsetenv("XDG_RUNTIME_DIR") == 0);
|
|
}
|
|
#endif
|
|
|
|
static int process_main(int argc, char **argv) {
|
|
assert(argc >= 2);
|
|
executable = argv[1];
|
|
#ifndef _WIN32
|
|
assert(signal(SIGPIPE, SIG_DFL) != SIG_ERR);
|
|
int saved_input = -1, saved_output = -1;
|
|
if (argc > 2 && strcmp(argv[2], "closed-standard") == 0) {
|
|
saved_input = dup(STDIN_FILENO);
|
|
saved_output = dup(STDOUT_FILENO);
|
|
close(STDIN_FILENO);
|
|
close(STDOUT_FILENO);
|
|
}
|
|
#endif
|
|
// Windows initializes process creation, Unicode conversion and error
|
|
// formatting facilities lazily. Measure repeated use after exercising
|
|
// those paths; their one-time handles are not owned by this transport.
|
|
#ifdef _WIN32
|
|
argument_cases();
|
|
exchange_cases();
|
|
failure_cases();
|
|
#endif
|
|
unsigned before = resource_count();
|
|
argument_cases();
|
|
exchange_cases();
|
|
slow_reply_cases();
|
|
failure_cases();
|
|
#ifndef _WIN32
|
|
attach_cases();
|
|
#endif
|
|
assert(resource_count() == before);
|
|
#ifndef _WIN32
|
|
if (saved_input >= 0) { assert(dup2(saved_input, STDIN_FILENO) >= 0); close(saved_input); }
|
|
if (saved_output >= 0) { assert(dup2(saved_output, STDOUT_FILENO) >= 0); close(saved_output); }
|
|
#endif
|
|
puts("native process contracts passed");
|
|
return 0;
|
|
}
|
|
|
|
#ifdef _WIN32
|
|
int wmain(int argc, wchar_t **wide) {
|
|
char **argv = calloc((size_t)argc, sizeof(*argv));
|
|
assert(argv != NULL);
|
|
for (int i = 0; i < argc; i++) {
|
|
int size = WideCharToMultiByte(CP_UTF8, 0, wide[i], -1, NULL, 0, NULL, NULL);
|
|
assert(size > 0);
|
|
argv[i] = malloc((size_t)size);
|
|
assert(argv[i] != NULL);
|
|
assert(WideCharToMultiByte(CP_UTF8, 0, wide[i], -1, argv[i], size, NULL, NULL) == size);
|
|
}
|
|
int result = process_main(argc, argv);
|
|
for (int i = 0; i < argc; i++) free(argv[i]);
|
|
free(argv);
|
|
return result;
|
|
}
|
|
#else
|
|
int main(int argc, char **argv) { return process_main(argc, argv); }
|
|
#endif
|