// GSF's own resources in the platform preset (genesis 8.15.29): the system // information request servers system-info-reqrep.kts declares, over the // server's dictionary, and the status dataservers GSF registers in code // (StatusDataserver.java), over the tables its Resource Daemon fills. // Behaviour read from GSF's source and checked against its jars (the // handoff's research-gsf-system.md). import { createContext, Script } from 'node:vm'; import { auditTableName } from '../db/audit.ts'; import { pkFieldsOf } from '../db/schema.ts'; import { NackError } from '../protocol/errors.ts'; import type { FieldDef, MockServerConfig, QueryDef, RequestReplyDef, Row, TableDef, TypedRequestReply, ViewDef, } from '../types.ts'; import { dto } from './notify.ts'; import { aliasedEnumKeys, fieldFormat, isPlatformTable, platformTable } from './products.ts'; const SYSINFO = 'global.genesis.console.sysinfo'; // The views the preset declares only to serve a resource GSF serves with a // renamed column (platform.ts, auth.ts); GSF's dictionary has none of them. export const SERVING_VIEWS = new Set([ 'PROFILE_PREFIXED', 'RIGHT_PREFIXED', 'USER_PROFILE_NAMES', 'SYSTEM_STATUS_BYTES', ]); // The table an audit table is, when `name` is one (_AUDIT of an // audited table). function auditedTableOf(config: MockServerConfig, name: string): string | undefined { const tables = config.tables ?? {}; const base = Object.keys(tables).find( (table) => auditTableName(table) === name && Boolean(tables[table].audit), ); return base; } /** * GSF's getAllEntityNames (DbReflectionUtils.kt:116-123): the dictionary's * tables, each audit table right after its table, then the views. GSF lists * the tables by product, in its dictionary files' HashMap order (which the * app's own file names decide), then declaration order: here, in the * config's order. */ export function entityNames(config: MockServerConfig): string[] { const tables = Object.keys(config.tables ?? {}); const audits = new Set(tables.filter((name) => auditedTableOf(config, name))); const ordered = tables .filter((name) => !audits.has(name)) .flatMap((name) => (audits.has(auditTableName(name)) ? [name, auditTableName(name)] : [name])); const views = Object.keys(config.views ?? {}).filter((name) => !SERVING_VIEWS.has(name)); return [...ordered, ...views]; } // --- Java patterns ------------------------------------------------------------------- // PatternSyntaxException.getMessage (Java 17): the description, the index // when there is one, the pattern, and a caret under the index when it's // inside the pattern. function patternError(description: string, index: number, pattern: string): Error { const near = index >= 0 ? ` near index ${index}` : ''; const caret = index >= 0 && index < pattern.length ? `\n${' '.repeat(index)}^` : ''; return new Error(`${description}${near}\n${pattern}${caret}`); } // The characters a JavaScript (u-mode) pattern needs escaped to mean // themselves. const JS_SYNTAX = new Set('^$\\.*+?()[]{}|/'); const ALNUM = /[0-9A-Za-z]/; const literal = (char: string) => (JS_SYNTAX.has(char) ? `\\${char}` : char); // Java's anchors, as a whole-string match reads them. const ANCHORS: Record = { A: '^', z: '$', Z: '(?=\\n?$)' }; const CASE_INSENSITIVE = '(?i)'; // The escape at `index` (a backslash), as JavaScript spells it, and the // index of its last character. function escaped(pattern: string, index: number, inClass: boolean): { text: string; end: number } { const next = pattern[index + 1] ?? ''; const end = index + 1; if (inClass) return { text: ALNUM.test(next) || next === '-' ? `\\${next}` : literal(next), end }; if (next === 'Q') { const close = pattern.indexOf('\\E', end + 1); const quoted = pattern.slice(end + 1, close === -1 ? pattern.length : close); return { text: [...quoted].map(literal).join(''), end: close === -1 ? pattern.length : close + 1, }; } if (next in ANCHORS) return { text: ANCHORS[next], end }; // Any other letter or digit as JavaScript reads it (\d, \p{Lu}, ...). return { text: ALNUM.test(next) ? `\\${next}` : literal(next), end }; } // Kotlin's toRegex() and String.matches: a whole-string java.util.regex // match. A pattern Java refuses fails with its PatternSyntaxException // message for the mistakes a name pattern makes (a quantifier with nothing // to repeat, an unmatched or unclosed group, an unclosed class); the Java // spellings JavaScript lacks are translated (\Q...\E, \A, \z, \Z, a leading // (?i), a backslash before a character that needs none), and what // JavaScript can't express at all (an inline flag after the start, Java's // own escapes) fails with JavaScript's message. export function javaPattern(pattern: string): RegExp { let source = ''; let flags = 'u'; let depth = 0; let inClass = false; // What came last: nothing to repeat, something that can be, a quantifier, // or a lazy/possessive mark on one. let last: 'none' | 'atom' | 'quantifier' | 'marked' = 'none'; let index = 0; if (pattern.startsWith(CASE_INSENSITIVE)) { flags += 'i'; index = CASE_INSENSITIVE.length; } for (; index < pattern.length; index += 1) { const char = pattern[index]; if (char === '\\') { const escape = escaped(pattern, index, inClass); source += escape.text; index = escape.end; if (!inClass) last = 'atom'; } else if (inClass) { source += char; if (char === ']') { inClass = false; last = 'atom'; } } else if (char === '[') { source += char; inClass = true; } else if (char === '(') { source += char; depth += 1; // (?:, (?=, (?, ...: a group construct, not a quantifier. if (pattern[index + 1] === '?') { source += '?'; index += 1; } last = 'none'; } else if (char === ')') { depth -= 1; if (depth < 0) throw patternError("Unmatched closing ')'", index - 1, pattern); source += char; last = 'atom'; } else if (char === '|') { source += char; last = 'none'; } else if (char === '*' || char === '+' || char === '?') { // After a quantifier, `?` makes it lazy and `+` possessive; anything // else has nothing to repeat. const marks: boolean = last === 'quantifier' && char !== '*'; if (last === 'none' || last === 'marked' || (last === 'quantifier' && !marks)) { throw patternError(`Dangling meta character '${char}'`, index, pattern); } // JavaScript has no possessive quantifier; on a whole-string match of // a name, a greedy one finds the same names. if (!(marks && char === '+')) source += char; last = marks ? 'marked' : 'quantifier'; } else { source += char === ']' || char === '}' ? `\\${char}` : char; last = char === '{' ? 'quantifier' : 'atom'; if (char === '{') { // A counted closure, copied whole. const close = pattern.indexOf('}', index + 1); if (close !== -1) { source += pattern.slice(index + 1, close + 1); index = close; last = 'atom'; } } } } if (inClass) throw patternError('Unclosed character class', pattern.length - 1, pattern); if (depth > 0) throw patternError('Unclosed group', pattern.length, pattern); return new RegExp(`^(?:${source})$`, flags); } // How long matching names may take. A pattern that backtracks badly ties // up one of the real server's request threads; here it would stall every // socket, so it fails instead (GENERIC_ERROR, with this text). const MATCH_BUDGET_MS = 1000; const matchScript = new Script('names.filter((name) => pattern.test(name))'); // The names `pattern` matches whole, in their order. export function matchingNames(pattern: RegExp, names: string[]): string[] { try { return matchScript.runInContext(createContext({ pattern, names }), { timeout: MATCH_BUDGET_MS, }) as string[]; } catch (error) { if ((error as { code?: string }).code !== 'ERR_SCRIPT_EXECUTION_TIMEOUT') throw error; throw new Error( `The mock stopped matching ${pattern.source} after ${MATCH_BUDGET_MS} ms: it backtracks too much`, ); } } // --- SYSTEM_ENTITY and SYSTEM_ENTITY_FIELDS ---------------------------------------------- // system-info-reqrep.kts: SystemEntityRequest(entityName: String), and the // two reply classes (console API.kt). SystemFieldResponse's TYPE is the // dictionary's Field.Type. const FIELD_TYPES = [ 'STRING', 'ENUM', 'INT', 'SHORT', 'DOUBLE', 'LONG', 'BOOLEAN', 'RAW', 'BIGDECIMAL', 'NANO_TIMESTAMP', 'DATETIME', 'DATE', ]; const entityRequest = { className: `${SYSINFO}.SystemEntityRequest`, fields: [dto('ENTITY_NAME', 'STRING')], }; const optionalString = (name: string) => dto(name, 'STRING', { nullable: true, optional: true }); export const SYSTEM_ENTITY_TYPES: TypedRequestReply = { request: entityRequest, reply: { className: `${SYSINFO}.SystemEntityResponse`, fields: [dto('ENTITY_NAME', 'STRING')] }, }; const SYSTEM_FIELD_TYPES: TypedRequestReply = { request: entityRequest, reply: { className: `${SYSINFO}.SystemFieldResponse`, fields: [ dto('DEFAULT_VALUE', 'ANY', { nullable: true, optional: true }), optionalString('FILE_NAME'), optionalString('FORMAT'), dto('MAX_SIZE', 'INT', { nullable: true, optional: true }), dto('NAME', 'STRING'), dto('NULLABLE', 'BOOLEAN', { optional: true }), optionalString('SENSITIVE_MASK'), dto('TYPE', 'ENUM', { values: FIELD_TYPES, enumClass: 'global.genesis.dictionary.Field.Type', }), optionalString('VALUES'), ], }, }; // A request without ENTITY_NAME fails to decode into SystemEntityRequest // (GenesisJacksonExceptionTranslator); not recorded end to end. function entityNameOf(request: Row): string { const name = request.ENTITY_NAME; if (name === undefined || name === null) { throw NackError.field( 'ENTITY_NAME', 'Missing mandatory field ENTITY_NAME', '$.REQUEST.ENTITY_NAME', 'MISSING_FIELD', ); } return String(name); } // One SystemFieldResponse, as GSF's generated config classes give a field: // never a MAX_SIZE or a FILE_NAME (the generator drops both), nulls sent. const fieldReply = (field: Partial & { NAME: string; TYPE: string }): Row => ({ NAME: field.NAME, TYPE: field.TYPE, VALUES: field.VALUES ?? null, DEFAULT_VALUE: field.DEFAULT_VALUE ?? null, MAX_SIZE: null, FILE_NAME: null, FORMAT: field.FORMAT ?? null, SENSITIVE_MASK: null, NULLABLE: field.NULLABLE ?? true, }); // GSF adds every table's RECORD_ID and TIMESTAMP after its own fields. const IDENTITY = [ fieldReply({ NAME: 'RECORD_ID', TYPE: 'LONG', NULLABLE: false }), fieldReply({ NAME: 'TIMESTAMP', TYPE: 'NANO_TIMESTAMP', NULLABLE: false }), ]; // A table as GSF's dictionary has it: a platform one as its product declares // it (the preset leaves USER's PASSWORD and KEY_VALUE's columns out of its // own), else the config's. const tableDefOf = (config: MockServerConfig, name: string): TableDef | undefined => isPlatformTable(name) ? platformTable(name) : config.tables?.[name]; // A table field's TYPE, VALUES and DEFAULT_VALUE: an ALIASED_ENUM by its keys, // an ENUM with no default defaulting to its first value (ParsedTable.kt:113). function enumOf(table: string, field: FieldDef): Pick { if (field.type !== 'ENUM') return { VALUES: null, DEFAULT_VALUE: field.default ?? null }; const values = field.values ?? []; const keys = aliasedEnumKeys(table, field.name); const keyOf = (value: unknown) => (keys ? keys[values.indexOf(String(value))] : value); const fallback = field.default ?? values[0]; return { VALUES: (keys ?? values).join(' ') || null, DEFAULT_VALUE: fallback === undefined ? null : keyOf(fallback), }; } // A table's own fields (Helper.kt:73-88): not nullable when the dictionary // says so, when the field is in any index (the key's included), generated, // or has a default. function tableFieldReplies(table: string, def: TableDef): Row[] { const required = new Set([ ...pkFieldsOf(def), ...(def.indexes ?? []).flatMap((index) => index.fields), ...(def.generated ?? []).map(({ field }) => field), ]); return (def.fields ?? []).map((field) => { const typed = enumOf(table, field); return fieldReply({ NAME: field.name, TYPE: field.type, ...typed, FORMAT: field.type === 'RAW' ? (fieldFormat(field.name) ?? null) : null, NULLABLE: field.nullable !== false && !required.has(field.name) && typed.DEFAULT_VALUE === null, }); }); } // An audit table's: its table's, then its own id and the four audit fields // (Audit.kt:76-80). function auditFieldReplies(table: string, def: TableDef): Row[] { const audit = auditTableName(table); return [ ...tableFieldReplies(table, def), ...IDENTITY, fieldReply({ NAME: `${audit}_ID`, TYPE: 'STRING', NULLABLE: false }), fieldReply({ NAME: 'AUDIT_EVENT_TYPE', TYPE: 'STRING' }), fieldReply({ NAME: 'AUDIT_EVENT_DATETIME', TYPE: 'DATETIME' }), fieldReply({ NAME: 'AUDIT_EVENT_TEXT', TYPE: 'STRING' }), fieldReply({ NAME: 'AUDIT_EVENT_USER', TYPE: 'STRING' }), ]; } // A view's fields (ViewConfigClassGenerator.kt:176-251), by their names in // the view: a base table column as nullable as in its table, a joined one // always nullable, a derived one too and with only its type; none with a // default or format. Then RECORD_ID and TIMESTAMP. function viewFieldReplies(config: MockServerConfig, view: ViewDef): Row[] { const columnOf = (table: string, column: string) => { const def = tableDefOf(config, table); const field = def?.fields?.find(({ name }) => name === column); if (!def || !field) return undefined; return tableFieldReplies(table, { ...def, fields: [field] })[0]; }; const baseDef = tableDefOf(config, view.base); const names = view.fields ?? [ ...(baseDef?.fields ?? []).map(({ name }) => name), ...Object.keys(view.select ?? {}), ...(view.joins ?? []).flatMap((join) => Object.keys(join.select ?? {})), ...Object.keys(view.derivedTypes ?? {}), ]; const replies = names .filter((name) => name !== 'RECORD_ID' && name !== 'TIMESTAMP') .map((name) => { const derived = view.derivedTypes?.[name]; if (derived) { const type = typeof derived === 'string' ? derived : derived.type; const values = typeof derived === 'string' ? undefined : derived.values; return fieldReply({ NAME: name, TYPE: type, VALUES: values?.join(' ') ?? null }); } const join = (view.joins ?? []).find((candidate) => candidate.select?.[name]); const source = join ? columnOf(join.table, join.select![name]) : columnOf(view.base, view.select?.[name] ?? name); return fieldReply({ NAME: name, TYPE: source?.TYPE ?? 'STRING', VALUES: source?.VALUES ?? null, NULLABLE: join ? true : (source?.NULLABLE ?? true), }); }); return [...replies, ...IDENTITY]; } function entityFieldReplies(config: MockServerConfig, entity: string): Row[] { const view = config.views?.[entity]; if (view) return viewFieldReplies(config, view); const audited = auditedTableOf(config, entity); if (audited) return auditFieldReplies(audited, tableDefOf(config, audited)!); return [...tableFieldReplies(entity, tableDefOf(config, entity) ?? { pkField: [] }), ...IDENTITY]; } /** system-info-reqrep.kts's two request servers. */ export function systemInfoRequestReplies(): Record { return { // :11 the entities whose name the pattern matches whole, `*` any run. SYSTEM_ENTITY: { typed: SYSTEM_ENTITY_TYPES, requestFields: ['ENTITY_NAME'], resolver: ({ request, config }) => { const pattern = javaPattern(entityNameOf(request).replaceAll('*', '.*')); return matchingNames(pattern, entityNames(config)).map((ENTITY_NAME) => ({ ENTITY_NAME, })); }, }, // :25 the first entity the pattern (as given: `*` alone is no pattern) // matches, and its fields. SYSTEM_ENTITY_FIELDS: { typed: SYSTEM_FIELD_TYPES, requestFields: ['ENTITY_NAME'], resolver: ({ request, config }) => { const pattern = javaPattern(entityNameOf(request)); const [entity] = matchingNames(pattern, entityNames(config)); if (!entity) throw new Error('Collection contains no element matching the predicate.'); return entityFieldReplies(config, entity); }, }, }; } // --- the status dataservers ---------------------------------------------------------- // StatusDataserver.java:113-145: SYSTEM_STATUS with its memory columns // renamed. export const SYSTEM_STATUS_BYTES: ViewDef = { base: 'SYSTEM_STATUS', select: { TOTAL_PHYSICAL_MEMORY_SIZE_BYTES: 'TOTAL_PHYSICAL_MEMORY_SIZE', FREE_PHYSICAL_MEMORY_SIZE_BYTES: 'FREE_PHYSICAL_MEMORY_SIZE', }, fields: [ 'SYSTEM_CPU_LOAD', 'TOTAL_PHYSICAL_MEMORY_SIZE_BYTES', 'HOST_NAME', 'FREE_PHYSICAL_MEMORY_SIZE_BYTES', ], }; /** * StatusDataserver.java's two dataservers, on GSF's older engine (no INDEXES * in their META), their fields in the order its HashSet of fields gives * them. Only GSF's Resource Daemon fills GENESIS_PROCESS and SYSTEM_STATUS, * and Genesis Start and Create's preview don't run it, so both stream no * rows, as there. */ export function statusQueries(): Record { return { ALL_PROCESSES_STATUS: { source: 'GENESIS_PROCESS', legacyEngine: true, fields: [ 'PROCESS_PORT', 'START_TIME', 'PROCESS_STATUS', 'PROCESS_ENABLED', 'PROCESS_SECURE', 'PROCESS_STATE_TEXT', 'DATADUMP', 'PROCESS_RESOURCES', 'PROCESS_MEM_USAGE', 'PROCESS_CPU_USAGE', 'PROCESS_NAME', 'PROCESS_STATUS_MESSAGE', 'LOG_LEVEL', 'PROCESS_HOSTNAME', 'RESOURCE_TYPES', ], // A STRING in its metadata, an ENUM in its table. fieldTypes: { PROCESS_STATE_TEXT: 'STRING' }, }, ALL_SYSTEM_STATUS: { source: 'SYSTEM_STATUS_BYTES', legacyEngine: true }, }; }