mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-11-01 07:33:12 +08:00
Keep child-process executables byte-identical without the new listener APIs
#2468 shipped child.off()/removeListener() and child.signalCode in the gated
scr_child_lifecycle.c unit, but it also restructured the always-linked
scr_child.c terminal dispatch loops to detach their listener arrays before
emitting. scr_child.c is compiled once per program and linked into all of
them, so its object moved and every child-process executable grew: the
zero-cost gate reported 55 runtime-only corpus programs.
Restore scr_child.c byte for byte and move the emit-snapshot discipline into
the gated unit instead. The base loops already bracket each terminal emit
with a state flag that only the emitter writes -- `settled` for 'exit' and
'error' (set after the stream drain, immediately before the terminal branch)
and `close_done` for 'close' (set at the top of the close emit) -- and run no
user code in between. A removal inside that window is therefore a no-op: the
entry stays, the in-flight emit still delivers it, which is exactly Node's
answer (emit copies the listener array), and the emitter's own drop releases
it. Outside the window no emit of that event is in flight, so removal in
place is Node-exact, and the base already releases late registrations once
the window has closed. 'spawn' detaches its array before invoking its
listeners, so removal during it finds an empty registry.
scr_child_private.h repeats the structure layout for the gated unit only;
scr_child.c keeps its own definitions untouched.
Validation: zero-cost gate against 68980c241 reports no runtime-only program
and no scr_child symbol in any IR, LLVM or executable difference; scr_child.o
is byte-identical to the base in all six shipped flavors, and the
scr_child_off_*/scr_child_signal_code entries live in scr_child_lifecycle.o.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
3a15562085
commit
1962a28fd7
+134
-104
@@ -1368,7 +1368,21 @@ ScrSpawnRes *scr_exec_capture(ScrStr *cmd, ScrArr *args, bool shell, ScrStr *cwd
|
||||
* fallback). Listener ordering, settle discipline, unref/teardown, and
|
||||
* the unhandled-'error' exit are copied unchanged. */
|
||||
|
||||
#include "scr_child_private.h"
|
||||
typedef enum {
|
||||
SCR_CHILD_RUNNING = 0,
|
||||
SCR_CHILD_EXITED = 1,
|
||||
SCR_CHILD_SPAWN_FAILED = 2
|
||||
} ScrChildState;
|
||||
|
||||
typedef struct {
|
||||
ScrClosure *cb;
|
||||
ScrChildExitFn fn;
|
||||
} ScrChildExitEntry;
|
||||
|
||||
typedef struct {
|
||||
ScrClosure *cb;
|
||||
ScrChildErrFn fn;
|
||||
} ScrChildErrEntry;
|
||||
|
||||
typedef struct {
|
||||
ScrClosure *cb; /* owned */
|
||||
@@ -1444,6 +1458,48 @@ static bool scr_child_writers_pending(void);
|
||||
static void scr_child_writers_service(void);
|
||||
static void scr_child_writer_child_settled(ScrChildWriter *w);
|
||||
|
||||
struct ScrChild {
|
||||
size_t rc;
|
||||
HANDLE proc; /* NULL after the reap (or spawn failure) */
|
||||
DWORD pid;
|
||||
ScrChildState state;
|
||||
bool settled;
|
||||
bool close_done;
|
||||
bool spawn_done;
|
||||
ScrClosure **spawn_cbs;
|
||||
size_t n_spawn, cap_spawn;
|
||||
bool has_code;
|
||||
bool killed;
|
||||
bool reffed;
|
||||
int spawn_uv_errno; /* spawn failure: Node's exitCode (negative) */
|
||||
const char *spawn_errname; /* spawn failure: the `code` stamp */
|
||||
double code;
|
||||
const char *exit_signal; /* we killed it: the signal's name (static) */
|
||||
const char *kill_signal; /* recorded when kill() terminates the child */
|
||||
ScrStr *err_msg; /* spawn failure only: "spawn <cmd> <ERRNAME>" */
|
||||
ScrChildExitEntry *exit_cbs;
|
||||
size_t n_exit, cap_exit;
|
||||
ScrChildExitEntry *close_cbs;
|
||||
size_t n_close, cap_close;
|
||||
ScrChildErrEntry *err_cbs;
|
||||
size_t n_err, cap_err;
|
||||
ScrClosure *exec_cb;
|
||||
ScrExecFileFn exec_fn;
|
||||
ScrStr *exec_display;
|
||||
ScrStr *exec_file; /* the spawned file and its arguments (+1 each), for */
|
||||
ScrArr *exec_args; /* a spawn failure's path and spawnargs */
|
||||
void (*exec_props)(struct ScrChild *c, ScrError *error); /* Node's error properties */
|
||||
ScrChildStream *out_stream;
|
||||
ScrChildStream *err_stream;
|
||||
ScrChildWriter *in_stream;
|
||||
ScrDyn *out_dyn, *err_dyn, *in_dyn;
|
||||
ScrIpc *ipc;
|
||||
/* The AsyncLocalStorage context of the spawn() call (+1, NULL outside
|
||||
* a store): 'spawn', 'error', 'exit' and 'close' run in it. */
|
||||
ScrAlsCtx *als;
|
||||
struct ScrChild *next; /* the pending registry */
|
||||
};
|
||||
|
||||
static SCR_TL ScrChild *scr_children = NULL;
|
||||
static SCR_TL ScrChild *scr_children_closing = NULL;
|
||||
static SCR_TL size_t scr_children_reffed_n = 0;
|
||||
@@ -2721,38 +2777,6 @@ static bool scr_child_close_ready(const ScrChild *c) {
|
||||
scr_ipc_close_ready(c->ipc);
|
||||
}
|
||||
|
||||
/* Node's EventEmitter copies the listener array before it emits, so a
|
||||
* listener that calls `off`/`removeListener` on a sibling must not shift
|
||||
* the entries this event is still delivering (the removed listener still
|
||||
* runs for the event in flight; one added during the emit does not).
|
||||
* Detach the registry here and answer its owned entries; the caller
|
||||
* invokes and releases each one, then frees the array. A registration
|
||||
* made during the emit is already refused by the close_done/settled
|
||||
* guard in scr_child_on_*. */
|
||||
static ScrChildExitEntry *scr_child_detach_exit(ScrChild *c, size_t *count) {
|
||||
ScrChildExitEntry *entries = c->exit_cbs;
|
||||
*count = entries != NULL ? c->n_exit : 0;
|
||||
c->exit_cbs = NULL;
|
||||
c->n_exit = c->cap_exit = 0;
|
||||
return entries;
|
||||
}
|
||||
|
||||
static ScrChildExitEntry *scr_child_detach_close(ScrChild *c, size_t *count) {
|
||||
ScrChildExitEntry *entries = c->close_cbs;
|
||||
*count = entries != NULL ? c->n_close : 0;
|
||||
c->close_cbs = NULL;
|
||||
c->n_close = c->cap_close = 0;
|
||||
return entries;
|
||||
}
|
||||
|
||||
static ScrChildErrEntry *scr_child_detach_err(ScrChild *c, size_t *count) {
|
||||
ScrChildErrEntry *entries = c->err_cbs;
|
||||
*count = entries != NULL ? c->n_err : 0;
|
||||
c->err_cbs = NULL;
|
||||
c->n_err = c->cap_err = 0;
|
||||
return entries;
|
||||
}
|
||||
|
||||
static void scr_child_fire_close_in(ScrChild *c);
|
||||
|
||||
/* 'close' runs in the spawn() call's context. */
|
||||
@@ -2771,14 +2795,11 @@ static void scr_child_fire_close_in(ScrChild *c) {
|
||||
/* Node counts the channel toward 'close' only through its EOF read; a
|
||||
* channel disconnected from this side never reports one, so 'close'
|
||||
* never fires. */
|
||||
size_t n = 0;
|
||||
ScrChildExitEntry *callbacks = NULL;
|
||||
if (!scr_ipc_close_dropped(c->ipc)) callbacks = scr_child_detach_close(c, &n);
|
||||
size_t n = scr_ipc_close_dropped(c->ipc) ? 0 : c->n_close;
|
||||
for (size_t i = 0; i < n; i++) {
|
||||
if (!scr_exc_pending()) callbacks[i].fn(callbacks[i].cb, has_code, code, c->exit_signal);
|
||||
scr_closure_release(callbacks[i].cb);
|
||||
c->close_cbs[i].fn(c->close_cbs[i].cb, has_code, code, c->exit_signal);
|
||||
if (scr_exc_pending()) break;
|
||||
}
|
||||
free(callbacks);
|
||||
scr_child_drop_close_listeners(c);
|
||||
scr_child_release(c); /* the closing/settle registry's reference */
|
||||
}
|
||||
@@ -2866,24 +2887,17 @@ static void scr_child_settle_in(ScrChild *c) {
|
||||
_Exit(1);
|
||||
}
|
||||
scr_child_err_code = c->spawn_errname;
|
||||
size_t n_err = 0;
|
||||
ScrChildErrEntry *err_cbs = scr_child_detach_err(c, &n_err);
|
||||
for (size_t i = 0; i < n_err; i++) {
|
||||
if (!scr_exc_pending()) err_cbs[i].fn(err_cbs[i].cb, c->err_msg);
|
||||
scr_closure_release(err_cbs[i].cb);
|
||||
for (size_t i = 0; i < c->n_err; i++) {
|
||||
c->err_cbs[i].fn(c->err_cbs[i].cb, c->err_msg);
|
||||
if (scr_exc_pending()) break; /* the loop surfaces it */
|
||||
}
|
||||
free(err_cbs);
|
||||
scr_child_err_code = NULL;
|
||||
if (!scr_exc_pending()) scr_exec_file_fire(c);
|
||||
} else {
|
||||
size_t n_exit = 0;
|
||||
ScrChildExitEntry *exit_cbs = scr_child_detach_exit(c, &n_exit);
|
||||
for (size_t i = 0; i < n_exit; i++) {
|
||||
if (!scr_exc_pending())
|
||||
exit_cbs[i].fn(exit_cbs[i].cb, c->has_code, c->code, c->exit_signal);
|
||||
scr_closure_release(exit_cbs[i].cb);
|
||||
for (size_t i = 0; i < c->n_exit; i++) {
|
||||
c->exit_cbs[i].fn(c->exit_cbs[i].cb, c->has_code, c->code, c->exit_signal);
|
||||
if (scr_exc_pending()) break;
|
||||
}
|
||||
free(exit_cbs);
|
||||
if (!scr_exc_pending()) scr_exec_file_fire(c);
|
||||
}
|
||||
scr_child_drop_terminal_listeners(c);
|
||||
@@ -3877,7 +3891,21 @@ ScrSpawnRes *scr_exec_capture(ScrStr *cmd, ScrArr *args, bool shell, ScrStr *cwd
|
||||
* registered after settling is released immediately and never fires
|
||||
* (Node: listeners added after 'exit' emitted don't fire). */
|
||||
|
||||
#include "scr_child_private.h"
|
||||
typedef enum {
|
||||
SCR_CHILD_RUNNING = 0,
|
||||
SCR_CHILD_EXITED = 1, /* reaped; has_code/code valid */
|
||||
SCR_CHILD_SPAWN_FAILED = 2 /* err_msg valid */
|
||||
} ScrChildState;
|
||||
|
||||
typedef struct {
|
||||
ScrClosure *cb;
|
||||
ScrChildExitFn fn;
|
||||
} ScrChildExitEntry;
|
||||
|
||||
typedef struct {
|
||||
ScrClosure *cb;
|
||||
ScrChildErrFn fn;
|
||||
} ScrChildErrEntry;
|
||||
|
||||
/* ── piped child output (child.stdout / child.stderr) ────────────────────
|
||||
*
|
||||
@@ -3986,6 +4014,50 @@ static bool scr_child_writers_pending(void);
|
||||
static void scr_child_writers_service(void);
|
||||
static void scr_child_writer_child_settled(ScrChildWriter *w);
|
||||
|
||||
struct ScrChild {
|
||||
size_t rc;
|
||||
pid_t pid;
|
||||
ScrChildState state;
|
||||
bool settled; /* terminal event delivered; listeners released */
|
||||
bool close_done;
|
||||
bool spawn_done;
|
||||
ScrClosure **spawn_cbs;
|
||||
size_t n_spawn, cap_spawn;
|
||||
bool unwatched; /* no kqueue NOTE_EXIT armed: the loop must poll for it */
|
||||
bool has_code;
|
||||
bool killed; /* a kill() successfully sent a signal (Node's flag) */
|
||||
bool reffed; /* keeps the loop alive; unref() clears it (Node) */
|
||||
int spawn_errno; /* spawn failure only: Node's exitCode is -errno there */
|
||||
double code;
|
||||
/* Signal death only: the terminating signal's name (static storage) —
|
||||
* Node's second exit-listener parameter. NULL for a normal exit. */
|
||||
const char *exit_signal;
|
||||
ScrStr *err_msg; /* spawn failure only: "spawn <cmd> <ERRNAME>" */
|
||||
ScrChildExitEntry *exit_cbs;
|
||||
size_t n_exit, cap_exit;
|
||||
ScrChildExitEntry *close_cbs;
|
||||
size_t n_close, cap_close;
|
||||
ScrChildErrEntry *err_cbs;
|
||||
size_t n_err, cap_err;
|
||||
ScrClosure *exec_cb;
|
||||
ScrExecFileFn exec_fn;
|
||||
ScrStr *exec_display;
|
||||
ScrStr *exec_file; /* the spawned file and its arguments (+1 each), for */
|
||||
ScrArr *exec_args; /* a spawn failure's path and spawnargs */
|
||||
void (*exec_props)(struct ScrChild *c, ScrError *error); /* Node's error properties */
|
||||
/* Piped stdio (stdio mode 3): the stream handles child.stdout /
|
||||
* child.stderr answer — owned (+1 each), NULL when not piped. */
|
||||
ScrChildStream *out_stream;
|
||||
ScrChildStream *err_stream;
|
||||
ScrChildWriter *in_stream;
|
||||
ScrDyn *out_dyn, *err_dyn, *in_dyn;
|
||||
ScrIpc *ipc;
|
||||
/* The AsyncLocalStorage context of the spawn() call (+1, NULL outside
|
||||
* a store): 'spawn', 'error', 'exit' and 'close' run in it. */
|
||||
ScrAlsCtx *als;
|
||||
struct ScrChild *next; /* the pending registry */
|
||||
};
|
||||
|
||||
static SCR_TL ScrChild *scr_children = NULL; /* unsettled children (registry +1) */
|
||||
static SCR_TL ScrChild *scr_children_closing = NULL; /* exited, waiting for stdio EOF */
|
||||
static SCR_TL size_t scr_children_reffed_n = 0; /* unsettled AND reffed (liveness) */
|
||||
@@ -5638,38 +5710,6 @@ static bool scr_child_close_ready(const ScrChild *c) {
|
||||
scr_ipc_close_ready(c->ipc);
|
||||
}
|
||||
|
||||
/* Node's EventEmitter copies the listener array before it emits, so a
|
||||
* listener that calls `off`/`removeListener` on a sibling must not shift
|
||||
* the entries this event is still delivering (the removed listener still
|
||||
* runs for the event in flight; one added during the emit does not).
|
||||
* Detach the registry here and answer its owned entries; the caller
|
||||
* invokes and releases each one, then frees the array. A registration
|
||||
* made during the emit is already refused by the close_done/settled
|
||||
* guard in scr_child_on_*. */
|
||||
static ScrChildExitEntry *scr_child_detach_exit(ScrChild *c, size_t *count) {
|
||||
ScrChildExitEntry *entries = c->exit_cbs;
|
||||
*count = entries != NULL ? c->n_exit : 0;
|
||||
c->exit_cbs = NULL;
|
||||
c->n_exit = c->cap_exit = 0;
|
||||
return entries;
|
||||
}
|
||||
|
||||
static ScrChildExitEntry *scr_child_detach_close(ScrChild *c, size_t *count) {
|
||||
ScrChildExitEntry *entries = c->close_cbs;
|
||||
*count = entries != NULL ? c->n_close : 0;
|
||||
c->close_cbs = NULL;
|
||||
c->n_close = c->cap_close = 0;
|
||||
return entries;
|
||||
}
|
||||
|
||||
static ScrChildErrEntry *scr_child_detach_err(ScrChild *c, size_t *count) {
|
||||
ScrChildErrEntry *entries = c->err_cbs;
|
||||
*count = entries != NULL ? c->n_err : 0;
|
||||
c->err_cbs = NULL;
|
||||
c->n_err = c->cap_err = 0;
|
||||
return entries;
|
||||
}
|
||||
|
||||
static void scr_child_fire_close_in(ScrChild *c);
|
||||
|
||||
/* 'close' runs in the spawn() call's context. */
|
||||
@@ -5688,14 +5728,11 @@ static void scr_child_fire_close_in(ScrChild *c) {
|
||||
/* Node counts the channel toward 'close' only through its EOF read; a
|
||||
* channel disconnected from this side never reports one, so 'close'
|
||||
* never fires. */
|
||||
size_t n = 0;
|
||||
ScrChildExitEntry *callbacks = NULL;
|
||||
if (!scr_ipc_close_dropped(c->ipc)) callbacks = scr_child_detach_close(c, &n);
|
||||
size_t n = scr_ipc_close_dropped(c->ipc) ? 0 : c->n_close;
|
||||
for (size_t i = 0; i < n; i++) {
|
||||
if (!scr_exc_pending()) callbacks[i].fn(callbacks[i].cb, has_code, code, c->exit_signal);
|
||||
scr_closure_release(callbacks[i].cb);
|
||||
c->close_cbs[i].fn(c->close_cbs[i].cb, has_code, code, c->exit_signal);
|
||||
if (scr_exc_pending()) break;
|
||||
}
|
||||
free(callbacks);
|
||||
scr_child_drop_close_listeners(c);
|
||||
scr_child_release(c); /* the closing/settle registry's reference */
|
||||
}
|
||||
@@ -5775,24 +5812,17 @@ static void scr_child_settle_in(ScrChild *c) {
|
||||
/* Node's spawn-failure error is an errnoException: `code` carries the
|
||||
* errno name. The thunk reads this while the pass runs. */
|
||||
scr_child_err_code = scr_errname(c->spawn_errno);
|
||||
size_t n_err = 0;
|
||||
ScrChildErrEntry *err_cbs = scr_child_detach_err(c, &n_err);
|
||||
for (size_t i = 0; i < n_err; i++) {
|
||||
if (!scr_exc_pending()) err_cbs[i].fn(err_cbs[i].cb, c->err_msg);
|
||||
scr_closure_release(err_cbs[i].cb);
|
||||
for (size_t i = 0; i < c->n_err; i++) {
|
||||
c->err_cbs[i].fn(c->err_cbs[i].cb, c->err_msg);
|
||||
if (scr_exc_pending()) break; /* the loop surfaces it */
|
||||
}
|
||||
free(err_cbs);
|
||||
scr_child_err_code = NULL;
|
||||
if (!scr_exc_pending()) scr_exec_file_fire(c);
|
||||
} else {
|
||||
size_t n_exit = 0;
|
||||
ScrChildExitEntry *exit_cbs = scr_child_detach_exit(c, &n_exit);
|
||||
for (size_t i = 0; i < n_exit; i++) {
|
||||
if (!scr_exc_pending())
|
||||
exit_cbs[i].fn(exit_cbs[i].cb, c->has_code, c->code, c->exit_signal);
|
||||
scr_closure_release(exit_cbs[i].cb);
|
||||
for (size_t i = 0; i < c->n_exit; i++) {
|
||||
c->exit_cbs[i].fn(c->exit_cbs[i].cb, c->has_code, c->code, c->exit_signal);
|
||||
if (scr_exc_pending()) break;
|
||||
}
|
||||
free(exit_cbs);
|
||||
if (!scr_exc_pending()) scr_exec_file_fire(c);
|
||||
}
|
||||
scr_child_drop_terminal_listeners(c);
|
||||
|
||||
@@ -27,7 +27,23 @@ ScrStr *scr_child_signal_code(ScrChild *c) {
|
||||
* argument is BORROWED — compared, never retained; a value the registry
|
||||
* does not hold (a fresh inline arrow, an adapter closure) removes
|
||||
* nothing, exactly Node's answer. Removing does not change what the loop
|
||||
* watches: a child with no listeners is still reaped. */
|
||||
* watches: a child with no listeners is still reaped.
|
||||
*
|
||||
* Emit snapshot: Node copies a listener array when the event is emitted,
|
||||
* so a listener that removes a SIBLING from inside the emit must not cut
|
||||
* the in-flight event short — the removed listener still runs for it.
|
||||
* scr_child.c's dispatch loops walk the live registry, so a removal in
|
||||
* that window may not shift or free an entry. The unit marks the window
|
||||
* before it iterates and never runs user code in between: `settled` for
|
||||
* 'exit' and 'error' (base sets it immediately before the terminal
|
||||
* branch, after the stream drain) and `close_done` for 'close' (set at
|
||||
* the top of the close emit). A removal there is a no-op and the
|
||||
* emitter's own drop releases the entry; the listener was registered
|
||||
* before the emit, so delivering it is exactly the snapshot's answer.
|
||||
* Outside it no emit of that event is in flight (it is either still to
|
||||
* come or already over and the array freed), so removal in place is safe
|
||||
* and Node-exact. The 'spawn' emit detaches its array before invoking its
|
||||
* listeners, so removal during it already finds an empty registry. */
|
||||
void scr_child_off_spawn(ScrChild *c, ScrClosure *cb) {
|
||||
for (size_t i = c->n_spawn; i > 0; i--) {
|
||||
if (c->spawn_cbs[i - 1] != cb) continue;
|
||||
@@ -39,6 +55,7 @@ void scr_child_off_spawn(ScrChild *c, ScrClosure *cb) {
|
||||
}
|
||||
|
||||
void scr_child_off_exit(ScrChild *c, ScrClosure *cb) {
|
||||
if (c->settled) return; /* the 'exit' emit is in flight (or done) */
|
||||
for (size_t i = c->n_exit; i > 0; i--) {
|
||||
if (c->exit_cbs[i - 1].cb != cb) continue;
|
||||
scr_closure_release(c->exit_cbs[i - 1].cb);
|
||||
@@ -49,6 +66,7 @@ void scr_child_off_exit(ScrChild *c, ScrClosure *cb) {
|
||||
}
|
||||
|
||||
void scr_child_off_close(ScrChild *c, ScrClosure *cb) {
|
||||
if (c->close_done) return; /* the 'close' emit is in flight (or done) */
|
||||
for (size_t i = c->n_close; i > 0; i--) {
|
||||
if (c->close_cbs[i - 1].cb != cb) continue;
|
||||
scr_closure_release(c->close_cbs[i - 1].cb);
|
||||
@@ -59,6 +77,7 @@ void scr_child_off_close(ScrChild *c, ScrClosure *cb) {
|
||||
}
|
||||
|
||||
void scr_child_off_error(ScrChild *c, ScrClosure *cb) {
|
||||
if (c->settled) return; /* the 'error' emit is in flight (or done) */
|
||||
for (size_t i = c->n_err; i > 0; i--) {
|
||||
if (c->err_cbs[i - 1].cb != cb) continue;
|
||||
scr_closure_release(c->err_cbs[i - 1].cb);
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
/* The child unit's private layout.
|
||||
/* The child unit's private layout, for the gated lifecycle unit.
|
||||
*
|
||||
* scr_child.c owns the implementation and every entry point the compiled
|
||||
* programs call. The gated scr_child_lifecycle.c unit (a later addition's
|
||||
* listener removal and signalCode) reads the same registries, so the
|
||||
* layout lives here instead of in the .c. This is not a public surface:
|
||||
* nothing outside the child units includes it, and the struct stays
|
||||
* opaque (scr_child.h's typedef) to everything else.
|
||||
* programs call; its `ScrChild`, state and entry types stay defined there,
|
||||
* unchanged, because that unit is always linked and its object must not
|
||||
* move. The gated scr_child_lifecycle.c unit (listener removal and
|
||||
* signalCode) reads the same registries, so it repeats the layout here.
|
||||
* This is not a public surface: nothing outside the child units includes
|
||||
* it, and the struct stays opaque (scr_child.h's typedef) to everything
|
||||
* else.
|
||||
*
|
||||
* Each platform arm below is the definition its unit arm used verbatim;
|
||||
* a field's position is load-bearing for both units at once. */
|
||||
* Each platform arm below must stay field-for-field identical to the
|
||||
* definition in scr_child.c's matching arm; a field's position is
|
||||
* load-bearing for both units at once. */
|
||||
#ifndef SCR_CHILD_PRIVATE_H
|
||||
#define SCR_CHILD_PRIVATE_H
|
||||
|
||||
|
||||
Reference in New Issue
Block a user