#if !defined(_UTILS_H_) #define _UTILS_H_ #include "config.h" #include #include #include #include #include #include #include "controller.capnp.h" #include "xce/errnox.h" #include "xce/types.h" #define CONTAINER_OF(P, T, F) ((T*)(((char*)(P)) - offsetof(T, F))) #define MAKE_JOBID(cluster, apiserver, timestamp, tag, sequence) \ ((((uint64_t)cluster) << CLUSTER_ID_SHIFT) \ | (((uint64_t)apiserver) << APISERVER_ID_SHIFT) \ | (((uint64_t)timestamp) << TIMESTAMP_SHIFT) \ | (((uint64_t)tag) << TAG_SHIFT) | (sequence)) #define EXTRACT_BITS(id, shift, bits) (((id) >> (shift)) & (UMAX_BITS(bits))) #define EXTRACT_ID_TAG(id) EXTRACT_BITS(id, TAG_SHIFT, N_TAG_BITS) #define EXTRACT_ID_CLUSTER(id) \ EXTRACT_BITS(id, CLUSTER_ID_SHIFT, N_CLUSTER_ID_BITS) #define EXTRACT_ID_APISERVER(id) \ EXTRACT_BITS(id, APISERVER_ID_SHIFT, N_APISERVER_ID_BITS) #define EXTRACT_ID_TIMESTAMP(id) \ EXTRACT_BITS(id, TIMESTAMP_SHIFT, N_TIMESTAMP_BITS) #define EXTRACT_ID_SEQUENCE(id) EXTRACT_BITS(id, 0, N_SEQUENCE_BITS) #define EXTRACT_ARRAY_JOB(id) \ ((((id) >> SEQUENCE_HIGH_SHIFT) \ & UMAX_BITS(N_SEQUENCE_BITS - SEQUENCE_HIGH_SHIFT)) \ << SEQUENCE_HIGH_SHIFT) #define EXTRACT_ARRAY_BASEID(id) \ MAKE_JOBID(EXTRACT_ID_CLUSTER(id), EXTRACT_ID_APISERVER(id), \ EXTRACT_ID_TIMESTAMP(id), 0x0, EXTRACT_ARRAY_JOB(id)) #define EXTRACT_ARRAY_IDX(id) EXTRACT_BITS(id, 0, N_ARRAY_IDX_BITS) #define ROUNDUP(v, bits) (((v) + UMAX_BITS(bits)) & ~UMAX_BITS(bits)) #define xce_use(x) (void)(x) #define float_near_zero (0.000001) #define float_equal(a, b) (fabs((a) - (b)) < float_near_zero) /* NULL-safe string equality: non-zero iff a and b are both NULL, or both * non-NULL and strcmp(a, b) == 0; zero otherwise (exactly one NULL, or * different contents). Reads only; never modifies either argument. */ extern int str_eq(const char* a, const char* b); #define STR(x) _STR(x) #define _STR(x) #x #define MK_VERSION(ver, commit) ver commit /* SUPERSEDED for version-string composition (build-time-versioning task * 3.1). MK_VERSION/STR (immediately above) cannot express an * empty-value -> "unknown" fallback: an unset commit_id/build_time meson * option makes STR(x) expand to an empty string literal ("", not a missing * token), and a preprocessor macro has no way to branch on "is this string * empty" at expansion time. xce_version_field()/xce_compose_version() * (extern decls below, implementation + doc comments in utils.c) are the * runtime replacement — they DO perform that substitution, and are what * compose the version string everywhere it now matters. Use those, not * MK_VERSION, for any new version-string composition. * * MK_VERSION/STR are kept in place here (not deleted) only because task 3.2 * — which will migrate controller/main.c's and scheduler/src/main.c's * argp_program_version construction to xce_compose_version() — has not * landed yet. Grep finding (2026-08-20, `grep -rn 'STR(\|MK_VERSION(' src/`, * excluding NULLSTR/comment/definition matches): STR() has ZERO call sites * anywhere in this codebase outside this exact MK_VERSION-based * argp_program_version pattern — controller/main.c:23, * scheduler/src/main.c:26, AND controller/testsuite/test.c:662 (the * xce_test REPL tool). That third caller is NOT listed in task 3.2's * boundary (`controller/main.c, scheduler/main.c` only, per tasks.md) — * worth confirming whether it's in scope when 3.2 lands, or it will keep * printing a commit-id-only, build-time-less version string after the * other two are migrated. STR() itself is a generic single-argument * stringification macro (not inherently version-specific), but per this * grep it currently has no unrelated use, so it is not being kept around * for any other live caller — only in service of the not-yet-migrated * MK_VERSION call sites above. Once task 3.2 (and any update to * controller/testsuite/test.c) lands and no caller of MK_VERSION remains, * both MK_VERSION and this STR usage can be deleted outright — not done * here, out of this task's boundary. * * NOTE FOR TASK 3.2 (or whoever migrates argp_program_version): * xce_compose_version() is a REGULAR RUNTIME FUNCTION, not a constant * expression — unlike MK_VERSION's pure string-literal concatenation, it * CANNOT initialize a file-scope variable. `const char* argp_program_version * = xce_compose_version(...)` at file scope will not compile (C file-scope * initializers must be constant expressions). controller/main.c:22-23 and * scheduler/src/main.c:25-26 currently declare argp_program_version exactly * that way — a file-scope const initialized at compile time — so this is * not a one-line macro-call swap. Instead: compute the version string at * RUNTIME into a `static char buf[...]` (module scope or local to main()), * via xce_compose_version(), early in main() and BEFORE argp_parse() runs * (argp reads argp_program_version by pointer when --version/-V is * processed) — then assign argp_program_version (no longer const-qualified * at its declaration; assigned at runtime instead of initialized) to point * at that buffer. See xce_compose_version()'s doc comment in utils.c for * the buffer-sizing contract. */ #ifndef FREEUP #define FREEUP(p) \ if ((p) != NULL) { \ free(p); \ (p) = NULL; \ } #endif #define EQUAL_STRING(s1, s2) \ (((s1) == (s2)) \ || (((s1) != NULL) && ((s2) != NULL) && (strcmp((s1), (s2)) == 0))) #define N_EL_ARRAY(arr) (sizeof(arr) / sizeof(arr[0])) #define PTR_STEP(p, delta) ((void*)(((char*)p) + (delta))) /* move the owner ship of pointer from src to dst */ #define MOVE_PTR(dst, src) \ do { \ (dst) = (src); \ (src) = NULL; \ } while (0) #define NULLSTR(p) (((p) == NULL) || (strlen(p) == 0)) #define SET_ERROR(code) \ do { \ set_error((char*)__FILE__, (char*)__func__, __LINE__, (code)); \ } while (0) #define TRACE(...) tlog_debug3(__VA_ARGS__) /* Named DBG, not DEBUG -- DEBUG is a bare command-line macro (-DDEBUG, * src/meson.build) defined for both the debug and asan buildtypes, so * `#define DEBUG(...)` here would trigger a "DEBUG redefined" warning in * every translation unit that includes this header under either profile. */ #define DBG(...) tlog_debug(__VA_ARGS__) #define INFO(...) tlog_info(__VA_ARGS__) #define ERROR(...) tlog_error(__VA_ARGS__) #define WARN(...) tlog_warn(__VA_ARGS__) #define PERROR(s) xce_perror(s) typedef struct { char* file; char* func; int line; int code; } ectx_t; extern int parse_argv(char*, char***); extern void free_argv(char**, int); extern int get_myname(char*, size_t); extern int mylog_init(char*, char*, int, int); extern void mylog_fini(void); extern int mylog_setlevel(char*); extern char* verbose_to_loglevel(int); /* See the SUPERSEDED/NOTE comment above MK_VERSION for what these replace * and why, and the constraint they impose on any file-scope * argp_program_version-style global (xce_compose_version() is not a * constant expression). */ extern const char* xce_version_field(const char*); extern int xce_compose_version(char*, size_t, const char*, const char*, const char*); /* dirty-content-fingerprint task 3.1: --version-only sibling of * xce_compose_version() above. Same composition (prefix + commit + * " built " + build_time), plus one more optional field: when * dirty_fingerprint is non-empty, a trailing "-dirty" on commit is * stripped and "-" is appended in its place, producing * "-". When dirty_fingerprint is NULL or empty, * this function delegates straight to xce_compose_version() and produces * byte-identical output -- commit_id's own computation and its existing * -dirty suffix are never touched by this function. The startup-log * composition call sites (controller/scheduler/worker main.c) must keep * calling xce_compose_version() directly, never this function -- this is * what keeps the startup log's format unchanged per Requirement 3.2 of the * dirty-content-fingerprint spec. */ extern int xce_compose_version_ex(char*, size_t, const char*, const char*, const char*, const char*); extern void set_error(char*, char*, int, int); extern ectx_t* get_ectx(void); extern int xce_vprintf(const char*, va_list); extern int xce_strerror_r(int, char*, size_t); extern void xce_perror(const char*); extern char* frame_symbol(int); extern int copy_int_list(int**, int*, int); extern int copy_strlist(xce_strlist_t*, xce_strlist_t); extern void free_timewindow_config(xce_timewindow_config_t*); extern xce_timewindow_config_t* dup_timewindow_config(xce_timewindow_config_t*); extern void free_resource_thresholds(xce_res_threshold_t**, int); extern xce_res_threshold_t** dup_resource_thresholds(xce_res_threshold_t**, int); extern void free_name_val_list(xce_name_val_t***, int*); extern void free_strlist(xce_strlist_t*); extern int fmt_jobid_ex(char*, size_t, uint64_t, char*, char*); extern int fmt_jobid(char*, size_t, uint64_t); extern int parse_jobid(uint64_t*, char*); extern uint64_t array_jobid(uint64_t); extern uint64_t array_idx(uint64_t); /* * Bounded job-identity / element-selection parser (job-array-one-based-index, * Req 1.5, 5.7, 5.8, 5.12, 5.14, 5.15, 8.1). * * Grammar: optional leading/trailing spaces or tabs, then * `num-num-num-num` (cluster-apiserver-timestamp-sequence), optionally * followed by `[num]` or `[num-num]`. `num` is one or more decimal digits * (leading zeros allowed, no sign, no inner whitespace). * * Checks, in this order; only the first failure is reported: * JOBSEL_ESYNTAX grammar (5.15) * JOBSEL_EWIDTH a field wider than its bit width (5.14) * JOBSEL_EALIGN element part present but sequence % 32768 != 0 (5.8) * JOBSEL_EINDEX an element number above JOBID_FIELD_MAX_INDEX (5.7) * JOBSEL_EREVERSED range whose last number is below its first (5.5) * Digits are accumulated with an overflow guard, so oversized numbers fail * the width or index check instead of wrapping. * * On JOBSEL_OK: BARE -> `key` is the exact untagged ID, lo == hi == 0; * SINGLE -> `key` is the array ID (tag 0, low 15 bits clear), lo == hi == k; * RANGE -> `key` is the array ID, [lo, hi]. A NULL `text` is * JOBSEL_ESYNTAX; `out` must not be NULL. */ #define JOBID_FIELD_MAX_CLUSTER MAX_CLUSTER_ID #define JOBID_FIELD_MAX_APISERVER MAX_APISERVER_ID #define JOBID_FIELD_MAX_TIMESTAMP (UMAX_BITS(N_TIMESTAMP_BITS)) #define JOBID_FIELD_MAX_SEQUENCE (UMAX_BITS(N_SEQUENCE_BITS)) #define JOBID_FIELD_MAX_INDEX MAX_ARRAY_IDX typedef enum { JOBSEL_OK = 0, JOBSEL_ESYNTAX, /* 5.15 */ JOBSEL_EWIDTH, /* 5.14 */ JOBSEL_EALIGN, /* 5.8 */ JOBSEL_EINDEX, /* 5.7 */ JOBSEL_EREVERSED /* 5.5 */ } jobsel_err_t; typedef enum { JOBSEL_BARE = 0, JOBSEL_SINGLE, JOBSEL_RANGE } jobsel_form_t; typedef struct { uint64_t key; /* BARE: the exact (untagged) ID; else the array ID */ jobsel_form_t form; uint32_t lo; /* SINGLE: lo == hi == k */ uint32_t hi; } jobsel_t; extern jobsel_err_t parse_jobsel(const char* text, jobsel_t* out); extern const char* jobsel_strerror(jobsel_err_t err); /* Non-zero iff the ID's tag equals TAG_JOB_ARRAY (tags 2 and 3 are not). */ extern int is_array_tag(uint64_t jobid); /* Element ID of number k (masked to 15 bits) in the array `s->key`; 0 if * `s` is NULL. */ extern uint64_t jobsel_element_id(const jobsel_t* s, uint32_t k); /* * xce_units_mode_t / xce_format_bytes_human() / xce_is_bytes_valued_resource() * / xce_format_units_value() -- scheduler-sim-diagnostics spec, task 7 (Req * 6.1, 6.3, 6.5). Shared-library promotion of the scheduler REPL's * byte-unit scaling algorithm (formerly repl_fmt.c's * repl_format_bytes_human()), its byte-valued-resource classification * (formerly repl_fmt.c's repl_is_bytes_valued_resource()), and a * display-mode-aware value composer (formerly repl_info.c's * repl_format_numeric_resource_value()'s byte-valued branch), so any * `libxce` consumer -- not only the scheduler's REPL -- can reach this * display-only formatting. `fmt_jobid()`/`fmt_jobid_ex()` immediately above * are the precedent this follows: a small, generic, cross-component * display-formatting helper with no domain coupling. * * All three functions are pure and display-only -- none of them ever * mutate a caller-supplied value or feed back into a stored/wire value. * `xce_is_bytes_valued_resource()` is a fixed 5-name list (`mem`, `tmp`, * `maxmem`, `maxtmp`, `maxswap`); a `NULL` name returns `false`. * `xce_format_bytes_human()` adaptively scales `bytes` to the smallest * B/KB/MB/GB/TB/PB unit it fits, writing e.g. "20.56 GB" into `buf`. * `xce_format_units_value()` assumes the caller has already decided * `bytes_value` is byte-valued (via `xce_is_bytes_valued_resource()` or the * caller's own equivalent check) -- it does not re-derive that decision: * RAW renders the bare "%f" of `bytes_value` alone, HUMAN renders * `xce_format_bytes_human()`'s scaled form alone, and BOTH (the default for * any other mode value) renders " ()". * * `xce_repl_units_mode_t` (scheduler-private, `repl_internal.h`) is a * deliberately separate, independently-defined enum with the same three * semantic values -- a `src/lib` type must not be defined in terms of a * scheduler-internal concept. The scheduler bridges the two at its one call * site (`repl_info.c`'s `translate_units_mode()`). */ typedef enum { XCE_UNITS_RAW = 1, XCE_UNITS_HUMAN, XCE_UNITS_BOTH } xce_units_mode_t; extern void xce_format_bytes_human(double bytes, char* buf, size_t bufsz); extern bool xce_is_bytes_valued_resource(const char* name); extern void xce_format_units_value(xce_units_mode_t mode, double bytes_value, char* buf, size_t bufsz); extern void unblock_sig(int); extern void block_sig(int); extern uint32_t hash4(uint32_t, uint32_t, uint32_t, uint32_t); extern uint32_t hash64(uint64_t); extern uint32_t hash_string(const char*); extern uint32_t hash_block(char*, size_t); #endif