// 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 },
};
}