querysub 0.518.0 → 0.520.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  import { SocketFunction } from "socket-function/SocketFunction";
2
2
  import { qreact } from "../../4-dom/qreact";
3
- import { DEFAULT_OVERLAP_TIME, MACHINE_RESYNC_INTERVAL, MachineServiceController, ServiceConfig, ServiceParameters, getLiveServiceParameters, stripReleaseState } from "../machineSchema";
3
+ import { DEFAULT_OVERLAP_TIME, MACHINE_RESYNC_INTERVAL, MachineServiceController, ServiceConfig, ServiceParameters, applyCommandTemplate, getCommandTemplateVariables, getLiveServiceParameters, getMachineTargets, setMachineTargets, stripReleaseState } from "../machineSchema";
4
4
  import { css } from "typesafecss";
5
5
  import { t } from "../../2-proxy/schema2";
6
6
  import { Querysub } from "../../4-querysub/Querysub";
@@ -165,6 +165,47 @@ export class ServiceDetailPage extends qreact.Component {
165
165
  });
166
166
  }
167
167
 
168
+ // Shows the `${name}` variables found in the command, and lets each picked machine entry set its own values for them (which converts machineIds to the { machineId, variables } form).
169
+ private renderTemplateVariables(config: ServiceConfig) {
170
+ let templateVariables = getCommandTemplateVariables(config.parameters.command);
171
+ let targets = getMachineTargets(config.parameters);
172
+ let anyVariablesSet = targets.some(target => Object.keys(target.variables).length > 0);
173
+ if (templateVariables.length === 0 && !anyVariablesSet) return undefined;
174
+
175
+ const setVariable = (entryIndex: number, name: string, value: string) => {
176
+ let updated = deepCloneJSON(config);
177
+ let updatedTargets = getMachineTargets(updated.parameters);
178
+ if (value) {
179
+ updatedTargets[entryIndex].variables[name] = value;
180
+ } else {
181
+ delete updatedTargets[entryIndex].variables[name];
182
+ }
183
+ setMachineTargets(updated.parameters, updatedTargets);
184
+ this.updateEditorState(updated);
185
+ };
186
+
187
+ let dupIndexes = new Map<string, number>();
188
+ return <div className={css.vbox(10).fillWidth.pad2(12).bord2(0, 0, 20)}>
189
+ {targets.map((target, entryIndex) => {
190
+ let index = dupIndexes.get(target.machineId) || 0;
191
+ dupIndexes.set(target.machineId, index + 1);
192
+ // Variables set on the entry but no longer in the command are still shown (flagged), so stale values can be cleared.
193
+ let variableNames = [...new Set([...templateVariables, ...Object.keys(target.variables)])];
194
+ return <div className={css.hbox(12).wrap.alignItems("center").fillWidth}>
195
+ <div className={css.boldStyle}>{getScreenName({ serviceKey: config.parameters.key, index })} ({target.machineId})</div>
196
+ {variableNames.map(name => {
197
+ let inCommand = templateVariables.includes(name);
198
+ return <InputLabel
199
+ label={inCommand && name || `${name} (not in command)`}
200
+ value={target.variables[name] || ""}
201
+ onChangeValue={value => setVariable(entryIndex, name, value)}
202
+ />;
203
+ })}
204
+ </div>;
205
+ })}
206
+ </div>;
207
+ }
208
+
168
209
  render() {
169
210
  const selectedServiceId = selectedServiceIdParam.value;
170
211
  if (!selectedServiceId) return <div>No service selected</div>;
@@ -199,8 +240,9 @@ export class ServiceDetailPage extends qreact.Component {
199
240
 
200
241
  // Sort machines by status and heartbeat
201
242
  let nextIndexes = new Map<string, number>();
202
- let machineIds = config.parameters.machineIds || [];
203
- let machineStatuses = machineIds.map(machineId => {
243
+ let machineTargets = getMachineTargets(config.parameters);
244
+ let machineIds = machineTargets.map(x => x.machineId);
245
+ let machineStatuses = machineTargets.map(({ machineId, variables }) => {
204
246
  let index = nextIndexes.get(machineId) || 0;
205
247
  nextIndexes.set(machineId, index + 1);
206
248
  let machineInfo = controller.getMachineInfo(machineId);
@@ -212,6 +254,7 @@ export class ServiceDetailPage extends qreact.Component {
212
254
 
213
255
  return {
214
256
  machineId,
257
+ variables,
215
258
  machineInfo,
216
259
  serviceInfo,
217
260
  isMachineDead,
@@ -297,17 +340,17 @@ export class ServiceDetailPage extends qreact.Component {
297
340
  picked={machineIds}
298
341
  addPicked={machineId => {
299
342
  let updated = deepCloneJSON(config);
300
- updated.parameters.machineIds = [...(updated.parameters.machineIds || []), machineId];
343
+ setMachineTargets(updated.parameters, [...getMachineTargets(updated.parameters), { machineId, variables: {} }]);
301
344
  this.updateEditorState(updated);
302
345
  }}
303
346
  removePicked={machineId => {
304
347
  let updated = deepCloneJSON(config);
305
- let updatedMachineIds = updated.parameters.machineIds || [];
306
- let index = updatedMachineIds.indexOf(machineId);
348
+ let targets = getMachineTargets(updated.parameters);
349
+ let index = targets.findIndex(x => x.machineId === machineId);
307
350
  if (index !== -1) {
308
- updatedMachineIds.splice(index, 1);
351
+ targets.splice(index, 1);
309
352
  }
310
- updated.parameters.machineIds = updatedMachineIds;
353
+ setMachineTargets(updated.parameters, targets);
311
354
  this.updateEditorState(updated);
312
355
  }}
313
356
  />
@@ -325,7 +368,7 @@ export class ServiceDetailPage extends qreact.Component {
325
368
  {config.parameters.deploy && <div className={css.vbox(8).fillWidth}>
326
369
  <h3>Deployed Machines ({machineIds.length})</h3>
327
370
  <div className={css.vbox(4).fillWidth}>
328
- {machineStatuses.map(({ machineId, machineInfo, serviceInfo, isMachineDead, hasError, isDisabled, index }) => {
371
+ {machineStatuses.map(({ machineId, variables, machineInfo, serviceInfo, isMachineDead, hasError, isDisabled, index }) => {
329
372
  if (!machineInfo) return <div key={machineId}>Loading {machineId}...</div>;
330
373
 
331
374
  let backgroundColor = css.hsl(0, 0, 100); // Default: white
@@ -358,6 +401,12 @@ export class ServiceDetailPage extends qreact.Component {
358
401
  <div>
359
402
  {serviceInfo?.nodeId || machineId} ({machineInfo.info["getExternalIP"]})
360
403
  </div>
404
+ {Object.keys(variables).length > 0 && <div
405
+ className={css.fontFamily("monospace").colorhsl(210, 60, 35)}
406
+ title={`Runs: ${applyCommandTemplate(config.parameters.command, variables)}`}
407
+ >
408
+ {Object.entries(variables).map(([name, value]) => `${name}=${value}`).join(" ")}
409
+ </div>}
361
410
  </div>
362
411
  {isDisabled && (
363
412
  <div className={css.colorhsl(0, 0, 30)}>
@@ -708,6 +757,8 @@ export class ServiceDetailPage extends qreact.Component {
708
757
  </div>;
709
758
  })()}
710
759
 
760
+ {this.renderTemplateVariables(config)}
761
+
711
762
  {this.state.saveError && (
712
763
  <div className={css.pad2(12).bord2(0, 80, 50).hsl(0, 80, 95).colorhsl(0, 80, 30)}>
713
764
  ⚠️ Error: {this.state.saveError}
@@ -1,6 +1,6 @@
1
1
  import { SocketFunction } from "socket-function/SocketFunction";
2
2
  import { qreact } from "../../4-dom/qreact";
3
- import { DEFAULT_OVERLAP_TIME, MachineServiceController, ServiceConfig, getLiveServiceParameters } from "../machineSchema";
3
+ import { DEFAULT_OVERLAP_TIME, MachineServiceController, ServiceConfig, getLiveServiceParameters, getMachineIdList } from "../machineSchema";
4
4
  import { css } from "typesafecss";
5
5
  import { t } from "../../2-proxy/schema2";
6
6
  import { Querysub } from "../../4-querysub/Querysub";
@@ -82,7 +82,7 @@ export class ServicesListPage extends qreact.Component {
82
82
  {services.map(([serviceId, config]) => {
83
83
  if (!config) return <div key={serviceId}>Config is broken? Missing value for service? Is the file corrupted?</div>;
84
84
 
85
- let machineIds = config.parameters.machineIds || [];
85
+ let machineIds = getMachineIdList(config.parameters);
86
86
  let disabledMachines = machineIds.filter(machineId => {
87
87
  let machineConfig = getMachineConfig(machineId);
88
88
  return machineConfig?.disabled;
@@ -4,7 +4,7 @@ import { qreact } from "../../4-dom/qreact";
4
4
  import { css } from "typesafecss";
5
5
  import { t } from "../../2-proxy/schema2";
6
6
  import { Querysub } from "../../4-querysub/Querysub";
7
- import { MachineServiceController, ServiceConfig } from "../machineSchema";
7
+ import { MachineServiceController, ServiceConfig, getMachineTargets, setMachineTargets } from "../machineSchema";
8
8
  import { MachinePicker } from "./MachinePicker";
9
9
  import { isDefined } from "../../misc";
10
10
 
@@ -125,15 +125,11 @@ class RenameMachine extends qreact.Component {
125
125
  );
126
126
  let toUpdate: ServiceConfig[] = [];
127
127
  for (let config of configs.filter(isDefined)) {
128
- let machineIds = config.parameters.machineIds || [];
129
- if (!machineIds.includes(from)) continue;
130
- toUpdate.push({
131
- ...config,
132
- parameters: {
133
- ...config.parameters,
134
- machineIds: machineIds.map(m => m === from ? to : m),
135
- },
136
- });
128
+ let targets = getMachineTargets(config.parameters);
129
+ if (!targets.some(target => target.machineId === from)) continue;
130
+ let updated = { ...config, parameters: { ...config.parameters } };
131
+ setMachineTargets(updated.parameters, targets.map(target => target.machineId === from ? { ...target, machineId: to } : target));
132
+ toUpdate.push(updated);
137
133
  }
138
134
  if (toUpdate.length === 0) {
139
135
  Querysub.commit(() => {
@@ -4,7 +4,7 @@ import { measureWrap } from "socket-function/src/profiling/measure";
4
4
  import { getOwnMachineId } from "sliftutils/misc/https/certs";
5
5
  import { forceRemoveNode, getOurNodeId, getOurNodeIdAssert } from "../-f-node-discovery/NodeDiscovery";
6
6
  import { Querysub } from "../4-querysub/Querysub";
7
- import { MACHINE_RESYNC_INTERVAL, MachineServiceControllerBase, MachineInfo, ServiceConfig, ServiceParameters, SERVICE_FOLDER, machineInfos, SERVICE_NODE_FILE_NAME, getEffectiveServiceConfigs, getLiveServiceParameters, restartParametersKey, recordLaunch, DEFAULT_OVERLAP_TIME } from "./machineSchema";
7
+ import { MACHINE_RESYNC_INTERVAL, MachineServiceControllerBase, MachineInfo, ServiceConfig, ServiceParameters, SERVICE_FOLDER, machineInfos, SERVICE_NODE_FILE_NAME, getEffectiveServiceConfigs, getLiveServiceParameters, restartParametersKey, recordLaunch, DEFAULT_OVERLAP_TIME, getMachineTargets, getMachineIdList, applyCommandTemplate } from "./machineSchema";
8
8
  import { runPromise } from "../functional/runCommand";
9
9
  import { getExternalIP } from "socket-function/src/networking";
10
10
  import { errorToUndefined, errorToUndefinedSilent } from "../errors";
@@ -656,7 +656,7 @@ const resyncServicesBase = runInSerial(measureWrap(async function resyncServices
656
656
  let machineId = getOwnMachineId(getDomain());
657
657
  let allConfigs = await getEffectiveServiceConfigs();
658
658
  let relevantConfigs = allConfigs
659
- .filter(config => (getLiveServiceParameters(config).machineIds || []).includes(machineId) || (config.parameters.machineIds || []).includes(machineId))
659
+ .filter(config => getMachineIdList(getLiveServiceParameters(config)).includes(machineId) || getMachineIdList(config.parameters).includes(machineId))
660
660
  .filter(x => getLiveServiceParameters(x).deploy || x.parameters.deploy);
661
661
 
662
662
  let machineInfo = await getLiveMachineInfo();
@@ -676,11 +676,11 @@ const resyncServicesBase = runInSerial(measureWrap(async function resyncServices
676
676
  console.log(magenta(`Release in flight for ${record.serviceId} (${record.parameters.key}): new instances start at ${new Date(releaseTime - overlapTime).toLocaleString()}${Date.now() >= releaseTime - overlapTime && " (overlap running)" || ""}, old instances shut down at ${new Date(releaseTime).toLocaleString()}`));
677
677
  // The release-overlap instances follow the NEW parameters' machineIds, so machines that are only in the new parameters still start their instances at overlap time (their canonical instances start once the release passes)
678
678
  if (Date.now() >= releaseTime - overlapTime && record.parameters.deploy) {
679
- let nextMatchedCount = (record.parameters.machineIds || []).filter(id => id === machineId).length;
680
- for (let i = 0; i < nextMatchedCount; i++) {
679
+ let nextTargets = getMachineTargets(record.parameters).filter(target => target.machineId === machineId);
680
+ for (let i = 0; i < nextTargets.length; i++) {
681
681
  try {
682
682
  await ensureNextInstance({
683
- next: record.parameters,
683
+ next: { ...record.parameters, command: applyCommandTemplate(record.parameters.command, nextTargets[i].variables) },
684
684
  index: i,
685
685
  screenNamesUsed,
686
686
  screenStateMap,
@@ -692,8 +692,10 @@ const resyncServicesBase = runInSerial(measureWrap(async function resyncServices
692
692
  }
693
693
  }
694
694
  if (!config.parameters.deploy) continue;
695
- let matchedCount = (config.parameters.machineIds || []).filter(id => id === machineId).length;
696
- for (let i = 0; i < matchedCount; i++) {
695
+ let matchedTargets = getMachineTargets(config.parameters).filter(target => target.machineId === machineId);
696
+ for (let i = 0; i < matchedTargets.length; i++) {
697
+ // Each instance runs the command with its own entry's template variables resolved, so the per-instance parameters (written to parameters.json and compared for restarts) carry the resolved command.
698
+ let instanceParameters = { ...config.parameters, command: applyCommandTemplate(config.parameters.command, matchedTargets[i].variables) };
697
699
  let screenName = getScreenName({
698
700
  serviceKey: config.parameters.key,
699
701
  index: i,
@@ -741,9 +743,9 @@ const resyncServicesBase = runInSerial(measureWrap(async function resyncServices
741
743
  if (await fsExistsAsync(parameterPath)) {
742
744
  prevParameters = await fs.promises.readFile(parameterPath, "utf8");
743
745
  }
744
- let newParametersString = JSON.stringify(config.parameters);
746
+ let newParametersString = JSON.stringify(instanceParameters);
745
747
 
746
- let sameParameters = sameRestartParameters(prevParameters, config.parameters);
748
+ let sameParameters = sameRestartParameters(prevParameters, instanceParameters);
747
749
  let screenIsRunning = screenStateMap.get(screenName)?.isProcessRunning;
748
750
 
749
751
  let nodePathId = folder + SERVICE_NODE_FILE_NAME;
@@ -760,7 +762,7 @@ const resyncServicesBase = runInSerial(measureWrap(async function resyncServices
760
762
  continue;
761
763
  }
762
764
 
763
- console.log(`Resyncing service ${magenta(screenName)}, with ${JSON.stringify(config.parameters)}, isRunning = ${screenIsRunning}, sameParameters = ${sameParameters}`);
765
+ console.log(`Resyncing service ${magenta(screenName)}, with ${newParametersString}, isRunning = ${screenIsRunning}, sameParameters = ${sameParameters}`);
764
766
 
765
767
  await fs.promises.writeFile(parameterPath, newParametersString);
766
768
 
@@ -781,7 +783,7 @@ const resyncServicesBase = runInSerial(measureWrap(async function resyncServices
781
783
 
782
784
  await runScreenCommand({
783
785
  screenName,
784
- command: config.parameters.command,
786
+ command: instanceParameters.command,
785
787
  });
786
788
  await delay(2000);
787
789
  let newScreens = await getScreenState(false);
@@ -66,15 +66,22 @@ export type MachineInfo = {
66
66
  };
67
67
 
68
68
 
69
+ export type MachineTarget = {
70
+ machineId: string;
71
+ /** Substituted into the command's `${name}` template placeholders for this machine entry (see applyCommandTemplate). */
72
+ variables: Record<string, string>;
73
+ };
74
+
69
75
  export type ServiceParameters = {
70
76
  /** MUST be unique, and clean enough to be used as the screen/tmux name, and folder name */
71
77
  key: string;
72
78
 
73
- /** There can be duplicate machine IDs which just means we apply multiple times on that machine, as `${key}-${index}-dply`, each with their own screen and folder. Part of the parameters so machine changes are released on the same schedule as everything else (but they never restart already-running instances, see restartParametersKey). Optional, as already deployed configs predate the field (treat missing as []). */
74
- machineIds?: string[];
79
+ /** There can be duplicate machine IDs which just means we apply multiple times on that machine, as `${key}-${index}-dply`, each with their own screen and folder. Part of the parameters so machine changes are released on the same schedule as everything else (but they never restart already-running instances, see restartParametersKey). Optional, as already deployed configs predate the field (treat missing as []). Entries are either bare machine ids, or objects carrying per-entry template variables for the command. Use getMachineTargets / setMachineTargets instead of touching this directly. */
80
+ machineIds?: string[] | MachineTarget[];
75
81
 
76
82
  repoUrl: string;
77
83
  gitRef: string;
84
+ /** May contain `${name}` template placeholders, filled per machine entry from its variables. Placeholders with no matching variable are left untouched, so bash's own `${ENV}` references keep working. */
78
85
  command: string;
79
86
  /** Allows forcing an update */
80
87
  poke?: number;
@@ -106,6 +113,40 @@ export type ServiceConfig = {
106
113
 
107
114
  export const DEFAULT_OVERLAP_TIME = timeInMinute * 10;
108
115
 
116
+ const TEMPLATE_VARIABLE_REGEX = /\$\{([a-zA-Z0-9_-]+)\}/g;
117
+ /** The `${name}` placeholder names in a command template, in order of first appearance. */
118
+ export function getCommandTemplateVariables(command: string): string[] {
119
+ let names = new Set<string>();
120
+ for (let match of command.matchAll(TEMPLATE_VARIABLE_REGEX)) {
121
+ names.add(match[1]);
122
+ }
123
+ return [...names];
124
+ }
125
+ export function applyCommandTemplate(command: string, variables: Record<string, string>): string {
126
+ return command.replace(TEMPLATE_VARIABLE_REGEX, (placeholder, name) => name in variables ? variables[name] : placeholder);
127
+ }
128
+
129
+ /** The machineIds entries, normalized to the object form. */
130
+ export function getMachineTargets(parameters: ServiceParameters): MachineTarget[] {
131
+ return ((parameters.machineIds || []) as (string | MachineTarget)[]).map(entry =>
132
+ typeof entry === "string"
133
+ ? { machineId: entry, variables: {} }
134
+ // Hand-edited configs (ex, via the raw config editor) may omit variables
135
+ : { machineId: entry.machineId, variables: entry.variables || {} }
136
+ );
137
+ }
138
+ export function getMachineIdList(parameters: ServiceParameters): string[] {
139
+ return getMachineTargets(parameters).map(x => x.machineId);
140
+ }
141
+ /** Stores targets back onto parameters, collapsing to the plain string[] form when no entry has variables (keeps configs without templates identical to how they always looked). */
142
+ export function setMachineTargets(parameters: ServiceParameters, targets: MachineTarget[]): void {
143
+ if (targets.every(target => Object.keys(target.variables).length === 0)) {
144
+ parameters.machineIds = targets.map(x => x.machineId);
145
+ } else {
146
+ parameters.machineIds = targets;
147
+ }
148
+ }
149
+
109
150
  /** The parameters machines should actually be running right now. */
110
151
  export function getLiveServiceParameters(config: ServiceConfig): ServiceParameters {
111
152
  let releaseTime = config.parameters.releaseTime;
@@ -121,12 +162,15 @@ export function getLiveServiceConfig(config: ServiceConfig): ServiceConfig {
121
162
  export function stripReleaseState(config: ServiceConfig): ServiceConfig {
122
163
  return { ...config, oldParameters: undefined, parameters: { ...config.parameters, releaseTime: undefined } };
123
164
  }
124
- /** The parts of the parameters that a running instance actually depends on. machineIds / releaseTime / overlapTime don't change what an instance runs, so changes to only them must never restart already-running instances (ex, adding a machine must not restart the existing machines). */
165
+ /** The parts of the parameters that a running instance actually depends on. machineIds / releaseTime / overlapTime don't change what an instance runs, so changes to only them must never restart already-running instances (ex, adding a machine must not restart the existing machines). Per-entry template variables DO change what an instance runs, but that is captured by the resolved command in the per-instance parameters the apply code compares (so a variable change restarts only that machine's instance). */
125
166
  export function restartParametersKey(parameters: ServiceParameters): string {
126
167
  return JSON.stringify({ ...parameters, machineIds: undefined, releaseTime: undefined, overlapTime: undefined });
127
168
  }
128
169
  function getConfigMachineIds(config: ServiceConfig): string[] {
129
- return [...new Set([...(config.parameters.machineIds || []), ...(config.oldParameters?.machineIds || [])])];
170
+ return [...new Set([
171
+ ...getMachineIdList(config.parameters),
172
+ ...(config.oldParameters ? getMachineIdList(config.oldParameters) : []),
173
+ ])];
130
174
  }
131
175
  // Migrates records that stored machineIds at the top level of ServiceConfig (it now lives in parameters)
132
176
  function normalizeServiceConfig(config: ServiceConfig): ServiceConfig {
@@ -562,9 +606,9 @@ export async function getEffectiveServiceConfigs(): Promise<ServiceConfig[]> {
562
606
 
563
607
  return configs.map(config => {
564
608
  normalizeServiceConfig(config);
565
- config.parameters.machineIds = (config.parameters.machineIds || []).filter(machineId => !disabledMachineIds.has(machineId));
609
+ setMachineTargets(config.parameters, getMachineTargets(config.parameters).filter(target => !disabledMachineIds.has(target.machineId)));
566
610
  if (config.oldParameters) {
567
- config.oldParameters.machineIds = (config.oldParameters.machineIds || []).filter(machineId => !disabledMachineIds.has(machineId));
611
+ setMachineTargets(config.oldParameters, getMachineTargets(config.oldParameters).filter(target => !disabledMachineIds.has(target.machineId)));
568
612
  }
569
613
  return config;
570
614
  });
@@ -82,9 +82,9 @@ export async function registerManagementPages2(config: {
82
82
 
83
83
  inputPages.push({
84
84
  title: "Routing Table",
85
- componentName: "AuthoritySpecPage",
86
- controllerName: "AuthoritySpecPageController",
87
- getModule: () => import("./misc-pages/AuthoritySpecPage"),
85
+ componentName: "RoutingTablePage",
86
+ controllerName: "RoutingTablePageController",
87
+ getModule: () => import("./misc-pages/RoutingTablePage"),
88
88
  });
89
89
  inputPages.push({
90
90
  title: "LOG VIEWER",