chore(vendor): import pi 1.3.0
Imported as a linear vendor commit so forge rebases do not replay the upstream root tree. git-subtree-dir: third_party/pi git-subtree-split: 71023aec1546d6d677a65669c5665c0927ce0dd0
This commit is contained in:
1 parent
719370c8f7
commit
6081e3e325
1743 files changed
+205459
-131704
No files matched your search
@@ -0,0 +1,31 @@
|
||||
const { createHash } = require("node:crypto");
|
||||
const assert = require("node:assert/strict");
|
||||
const { readFileSync } = require("node:fs");
|
||||
|
||||
(async () => {
|
||||
let ticks = 0;
|
||||
const timer = setInterval(() => ticks++, 10);
|
||||
const started = performance.now();
|
||||
try {
|
||||
const helper = require(process.argv[2]);
|
||||
const pending = helper[process.argv[3]]();
|
||||
assert.ok(pending instanceof Promise, "Native reads must return promises");
|
||||
const value = await pending;
|
||||
const bytes = typeof value === "string" || Buffer.isBuffer(value) ? Buffer.from(value) : undefined;
|
||||
console.log(JSON.stringify({
|
||||
ok: true,
|
||||
value: value === null || (typeof value === "string" && value.length <= 100) ? value : undefined,
|
||||
unavailable: value === undefined || undefined,
|
||||
length: bytes?.length,
|
||||
hash: bytes && createHash("sha256").update(bytes).digest("hex"),
|
||||
}));
|
||||
} catch (error) {
|
||||
console.log(JSON.stringify({ ok: false, error: String(error) }));
|
||||
} finally {
|
||||
clearInterval(timer);
|
||||
if (performance.now() - started > 200) assert.ok(ticks > 5, "Clipboard I/O must not block the JS event loop");
|
||||
// Check before Node exits: process exit must not hide a leaked child or zombie.
|
||||
assert.equal(readFileSync(`/proc/self/task/${process.pid}/children`, "utf8").trim(), "");
|
||||
}
|
||||
|
||||
})().catch((error) => { console.error(error); process.exitCode = 1; });
|
||||
@@ -0,0 +1,64 @@
|
||||
#include <assert.h>
|
||||
#include <stdatomic.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
static atomic_int allocations, reads;
|
||||
static atomic_bool released;
|
||||
|
||||
static void* tracked_calloc(size_t count, size_t size) {
|
||||
void* pointer = calloc(count, size);
|
||||
if (pointer) atomic_fetch_add(&allocations, 1);
|
||||
return pointer;
|
||||
}
|
||||
|
||||
static void tracked_free(void* pointer) {
|
||||
if (pointer) atomic_fetch_sub(&allocations, 1);
|
||||
free(pointer);
|
||||
}
|
||||
|
||||
#define calloc tracked_calloc
|
||||
#define free tracked_free
|
||||
#include "../../native/linux/src/clipboard-worker.h"
|
||||
#undef calloc
|
||||
#undef free
|
||||
|
||||
// Exercise the production executor with a controlled blocking callback, without X11.
|
||||
static void read_clipboard(clipboard_job* job) {
|
||||
job->data = tracked_calloc(5, 1);
|
||||
assert(job->data);
|
||||
memcpy(job->data, "text", 4);
|
||||
job->length = 4;
|
||||
job->format = job->operation == CLIPBOARD_IMAGE ? CLIPBOARD_BUFFER : CLIPBOARD_UTF8;
|
||||
atomic_fetch_add(&reads, 1);
|
||||
while (!atomic_load(&released)) {
|
||||
struct timespec delay = {.tv_nsec = 1000000};
|
||||
nanosleep(&delay, 0);
|
||||
}
|
||||
}
|
||||
|
||||
static napi_value release_reader(napi_env env, napi_callback_info info) {
|
||||
(void)info;
|
||||
atomic_store(&released, true);
|
||||
return undefined_value(env);
|
||||
}
|
||||
|
||||
static napi_value get_state(napi_env env, napi_callback_info info) {
|
||||
(void)info;
|
||||
char state[80];
|
||||
pthread_mutex_lock(&worker_mutex);
|
||||
snprintf(state, sizeof(state), "%d,%d,%d", atomic_load(&reads), atomic_load(&allocations), worker_busy);
|
||||
pthread_mutex_unlock(&worker_mutex);
|
||||
napi_create_string_utf8_fn create_string = (napi_create_string_utf8_fn)node_symbol("napi_create_string_utf8");
|
||||
napi_value result = 0;
|
||||
create_string(env, state, NAPI_AUTO_LENGTH, &result);
|
||||
return result;
|
||||
}
|
||||
|
||||
PI_NAPI_EXPORT napi_value napi_register_module_v1(napi_env env, napi_value exports) {
|
||||
set_function_export(env, exports, "getText", get_clipboard_text);
|
||||
set_function_export(env, exports, "getImage", get_clipboard_image);
|
||||
set_function_export(env, exports, "release", release_reader);
|
||||
set_function_export(env, exports, "state", get_state);
|
||||
return exports;
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
const assert = require("node:assert/strict");
|
||||
const { once } = require("node:events");
|
||||
const { readFile } = require("node:fs/promises");
|
||||
const { setTimeout: delay } = require("node:timers/promises");
|
||||
const { isMainThread, parentPort, Worker, workerData } = require("node:worker_threads");
|
||||
|
||||
async function waitFor(predicate) {
|
||||
const deadline = performance.now() + 5000;
|
||||
while (!predicate()) {
|
||||
assert.ok(performance.now() < deadline, "Timed out waiting for the private reader");
|
||||
await delay(10);
|
||||
}
|
||||
}
|
||||
|
||||
(async () => {
|
||||
if (!isMainThread) {
|
||||
const helper = require(workerData);
|
||||
void helper.getText();
|
||||
await waitFor(() => helper.state().startsWith("1,"));
|
||||
parentPort.postMessage("reading");
|
||||
return;
|
||||
}
|
||||
const [addon, mode] = process.argv.slice(2);
|
||||
if (mode === "worker-exit") {
|
||||
// The originating environment unloads while its private callback still waits.
|
||||
const worker = new Worker(__filename, { workerData: addon });
|
||||
await once(worker, "message");
|
||||
await worker.terminate();
|
||||
const helper = require(addon);
|
||||
assert.equal(helper.state(), "1,1,1", "The detached operation survives environment teardown");
|
||||
helper.release();
|
||||
await waitFor(() => helper.state() === "1,0,0");
|
||||
assert.equal(await helper.getText(), "text");
|
||||
console.log("passed");
|
||||
return;
|
||||
}
|
||||
const helper = require(addon);
|
||||
const started = performance.now();
|
||||
const pending = helper.getText();
|
||||
await waitFor(() => helper.state().startsWith("1,"));
|
||||
if (mode === "process-exit") {
|
||||
assert.equal(await pending, undefined);
|
||||
console.log("passed");
|
||||
return; // The private callback never finishes; Node must still exit naturally.
|
||||
}
|
||||
const concurrentStarted = performance.now();
|
||||
const concurrent = Array.from({ length: 20 }, (_, index) => helper[index % 2 ? "getText" : "getImage"]());
|
||||
// With UV_THREADPOOL_SIZE=2, a second blocked native task would delay this read.
|
||||
await readFile(__filename);
|
||||
assert.deepEqual(await Promise.all(concurrent), Array(20).fill(undefined));
|
||||
assert.ok(performance.now() - concurrentStarted < 1500, "Concurrent reads and filesystem I/O must remain available");
|
||||
assert.equal(await pending, undefined);
|
||||
assert.ok(performance.now() - started < 4500, "The shared worker must leave its bounded wait");
|
||||
assert.equal(helper.state(), "1,1,1", "Only one private reader may remain blocked");
|
||||
assert.equal(await helper.getImage(), undefined);
|
||||
assert.equal(helper.state(), "1,1,1", "Another request must not start another private reader");
|
||||
helper.release();
|
||||
await waitFor(() => helper.state() === "1,0,0");
|
||||
assert.equal(await helper.getText(), "text");
|
||||
assert.deepEqual(await helper.getImage(), Buffer.from("text"));
|
||||
assert.equal(helper.state(), "3,0,0", "Late and successful results must all be freed");
|
||||
console.log("passed");
|
||||
})().catch((error) => {
|
||||
console.error(error);
|
||||
process.exitCode = 1;
|
||||
});
|
||||
@@ -0,0 +1,177 @@
|
||||
#include <xcb/xcb.h>
|
||||
#include <assert.h>
|
||||
#include <stdbool.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
// Track only allocations made by the clipboard implementation, not libxcb.
|
||||
// This makes the partial-transfer leak regression deterministic, unlike RSS.
|
||||
static void* allocations[64];
|
||||
static size_t sizes[64];
|
||||
static size_t allocated_bytes;
|
||||
static size_t peak_bytes;
|
||||
|
||||
static void* tracked_realloc(void* pointer, size_t size) {
|
||||
size_t index = 0;
|
||||
while (index < 64 && allocations[index] != pointer) index++;
|
||||
assert(index < 64);
|
||||
void* result = realloc(pointer, size);
|
||||
if (result) {
|
||||
allocated_bytes = allocated_bytes - sizes[index] + size;
|
||||
sizes[index] = size;
|
||||
allocations[index] = result;
|
||||
if (allocated_bytes > peak_bytes) peak_bytes = allocated_bytes;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
static void tracked_free(void* pointer) {
|
||||
for (size_t index = 0; pointer && index < 64; index++) {
|
||||
if (allocations[index] == pointer) {
|
||||
allocated_bytes -= sizes[index];
|
||||
sizes[index] = 0;
|
||||
allocations[index] = 0;
|
||||
break;
|
||||
}
|
||||
}
|
||||
free(pointer);
|
||||
}
|
||||
|
||||
#define realloc tracked_realloc
|
||||
#define free tracked_free
|
||||
#include "../../native/linux/src/linux-platform-x11.c"
|
||||
#undef realloc
|
||||
#undef free
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
if (argc != 2) return 1;
|
||||
if (strcmp(argv[1], "metadata") == 0) {
|
||||
xcb_atom_t atom = XCB_ATOM_STRING;
|
||||
property_data chunk = {
|
||||
.data = (unsigned char*)&atom, .length = sizeof(atom),
|
||||
.items = 1, .format = 32, .type = XCB_ATOM_ATOM,
|
||||
};
|
||||
property_data result = {0};
|
||||
assert(append_property(&result, &chunk));
|
||||
assert(append_property(&result, &chunk));
|
||||
assert(result.length == 2 * sizeof(atom) && result.items == 2);
|
||||
|
||||
chunk.format = 8;
|
||||
chunk.items = sizeof(atom);
|
||||
assert(!append_property(&result, &chunk));
|
||||
chunk.format = 32;
|
||||
chunk.items = 1;
|
||||
chunk.type = XCB_ATOM_STRING;
|
||||
assert(!append_property(&result, &chunk));
|
||||
chunk.type = XCB_ATOM_ATOM;
|
||||
chunk.length--;
|
||||
assert(!append_property(&result, &chunk));
|
||||
assert(result.length == 2 * sizeof(atom) && result.items == 2);
|
||||
|
||||
// The terminating chunk must agree with the preceding metadata too.
|
||||
chunk.length = chunk.items = 0;
|
||||
chunk.format = 8;
|
||||
assert(!append_property(&result, &chunk));
|
||||
chunk.format = 32;
|
||||
assert(append_property(&result, &chunk));
|
||||
tracked_free(result.data);
|
||||
assert(allocated_bytes == 0);
|
||||
puts("validated");
|
||||
return 0;
|
||||
}
|
||||
if (strcmp(argv[1], "text") == 0 || strcmp(argv[1], "image") == 0) {
|
||||
x11_clipboard clipboard;
|
||||
assert(open_clipboard(&clipboard));
|
||||
property_data result = {0};
|
||||
bool received = read_selection(&clipboard, strcmp(argv[1], "image") == 0, &result);
|
||||
close_clipboard(&clipboard);
|
||||
assert(!received);
|
||||
assert(!result.data && !result.length);
|
||||
assert(peak_bytes >= 8192); // Both the received chunk and accumulated data were allocated.
|
||||
assert(allocated_bytes == 0);
|
||||
puts("clean");
|
||||
return 0;
|
||||
}
|
||||
|
||||
x11_clipboard clipboard;
|
||||
assert(open_clipboard(&clipboard));
|
||||
xcb_connection_t* connection = clipboard.connection;
|
||||
xcb_atom_t text = intern_atom(&clipboard, "UTF8_STRING");
|
||||
xcb_atom_t image = intern_atom(&clipboard, "image/png");
|
||||
xcb_set_selection_owner(connection, clipboard.window, clipboard.clipboard, XCB_CURRENT_TIME);
|
||||
// Wait for the server to process selection ownership before reporting ready.
|
||||
assert(intern_atom(&clipboard, "READY") != XCB_NONE);
|
||||
puts("ready");
|
||||
fflush(stdout);
|
||||
|
||||
xcb_window_t requestor = XCB_NONE;
|
||||
xcb_atom_t property = XCB_NONE;
|
||||
xcb_atom_t target = XCB_NONE;
|
||||
bool sent_chunk = false;
|
||||
xcb_generic_event_t* event;
|
||||
while ((event = xcb_wait_for_event(connection))) {
|
||||
uint8_t type = event->response_type & 0x7f;
|
||||
if (type == XCB_SELECTION_REQUEST) {
|
||||
xcb_selection_request_event_t* request = (xcb_selection_request_event_t*)event;
|
||||
puts("requested");
|
||||
fflush(stdout);
|
||||
if (strcmp(argv[1], "idle") == 0) {
|
||||
free(event);
|
||||
continue;
|
||||
}
|
||||
xcb_atom_t response_property = request->property;
|
||||
if (strcmp(argv[1], "direct-text") == 0) {
|
||||
if (request->target == text) {
|
||||
const char value[] = "café 日本語";
|
||||
xcb_change_property(connection, XCB_PROP_MODE_REPLACE, request->requestor,
|
||||
request->property, text, 8, sizeof(value) - 1, value);
|
||||
} else {
|
||||
response_property = XCB_NONE;
|
||||
}
|
||||
} else if (request->target == clipboard.targets) {
|
||||
xcb_atom_t targets[] = {text, image};
|
||||
xcb_change_property(connection, XCB_PROP_MODE_REPLACE, request->requestor,
|
||||
request->property, XCB_ATOM_ATOM, 32, 2, targets);
|
||||
} else {
|
||||
requestor = request->requestor;
|
||||
property = request->property;
|
||||
target = request->target;
|
||||
uint32_t mask = XCB_EVENT_MASK_PROPERTY_CHANGE;
|
||||
xcb_change_window_attributes(connection, requestor, XCB_CW_EVENT_MASK, &mask);
|
||||
uint32_t length = 8192;
|
||||
xcb_change_property(connection, XCB_PROP_MODE_REPLACE, requestor,
|
||||
property, clipboard.incr, 32, 1, &length);
|
||||
}
|
||||
union {
|
||||
xcb_selection_notify_event_t selection;
|
||||
char bytes[32];
|
||||
} notification = { .selection = {
|
||||
.response_type = XCB_SELECTION_NOTIFY,
|
||||
.time = request->time,
|
||||
.requestor = request->requestor,
|
||||
.selection = request->selection,
|
||||
.target = request->target,
|
||||
.property = response_property,
|
||||
} };
|
||||
xcb_send_event(connection, false, request->requestor, XCB_EVENT_MASK_NO_EVENT, notification.bytes);
|
||||
xcb_flush(connection);
|
||||
} else if (type == XCB_PROPERTY_NOTIFY) {
|
||||
xcb_property_notify_event_t* notification = (xcb_property_notify_event_t*)event;
|
||||
if (notification->window == requestor && notification->atom == property &&
|
||||
notification->state == XCB_PROPERTY_DELETE) {
|
||||
if (!sent_chunk) {
|
||||
unsigned char chunk[4096] = {0};
|
||||
xcb_change_property(connection, XCB_PROP_MODE_REPLACE, requestor, property, target, 8, sizeof(chunk), chunk);
|
||||
sent_chunk = true;
|
||||
} else if (strcmp(argv[1], "invalid") == 0) {
|
||||
// Reject the terminating chunk after allocating the preceding data.
|
||||
xcb_change_property(connection, XCB_PROP_MODE_REPLACE, requestor, property, XCB_ATOM_ATOM, 32, 0, 0);
|
||||
} // Otherwise never finish the transfer; the reader must time out.
|
||||
xcb_flush(connection);
|
||||
}
|
||||
}
|
||||
free(event);
|
||||
}
|
||||
close_clipboard(&clipboard);
|
||||
}
|
||||
Reference in new issue
Block a user