@guanjinke/artifact-client 1.7.2 → 1.9.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.
- package/README.md +3 -6
- package/dist/cli/environment.d.ts +1 -10
- package/dist/cli/environment.d.ts.map +1 -1
- package/dist/cli/environment.js +0 -24
- package/dist/cli/environment.js.map +1 -1
- package/dist/cli/setup.js +5 -8
- package/dist/cli/setup.js.map +1 -1
- package/dist/environment/builder.d.ts +4 -0
- package/dist/environment/builder.d.ts.map +1 -1
- package/dist/environment/builder.js +31 -20
- package/dist/environment/builder.js.map +1 -1
- package/dist/environment/catalog.d.ts +8 -0
- package/dist/environment/catalog.d.ts.map +1 -1
- package/dist/environment/catalog.js +25 -0
- package/dist/environment/catalog.js.map +1 -1
- package/dist/environment/package-resource.d.ts +7 -1
- package/dist/environment/package-resource.d.ts.map +1 -1
- package/dist/environment/package-resource.js +8 -2
- package/dist/environment/package-resource.js.map +1 -1
- package/dist/environment/plan.d.ts +26 -5
- package/dist/environment/plan.d.ts.map +1 -1
- package/dist/environment/plan.js +63 -11
- package/dist/environment/plan.js.map +1 -1
- package/dist/environment/types.d.ts +17 -9
- package/dist/environment/types.d.ts.map +1 -1
- package/dist/environment/update.d.ts +9 -29
- package/dist/environment/update.d.ts.map +1 -1
- package/dist/environment/update.js +375 -95
- package/dist/environment/update.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/web/static/app.js +1 -1
- package/package.json +2 -3
- package/resources/3DScene/capeHill.hdr +0 -0
- package/resources/3DScene/defaultGround.jpg +0 -0
- package/resources/3DScene/environmentSpecular.env +0 -0
- package/resources/3DScene/flare.png +0 -0
- package/resources/3DScene/geometry1.png +0 -0
- package/resources/3DScene/geometry10.png +0 -0
- package/resources/3DScene/geometry11.png +0 -0
- package/resources/3DScene/geometry12.png +0 -0
- package/resources/3DScene/geometry13.png +0 -0
- package/resources/3DScene/geometry2.png +0 -0
- package/resources/3DScene/geometry3.png +0 -0
- package/resources/3DScene/geometry4.png +0 -0
- package/resources/3DScene/geometry5.png +0 -0
- package/resources/3DScene/geometry6.png +0 -0
- package/resources/3DScene/geometry7.png +0 -0
- package/resources/3DScene/geometry8.png +0 -0
- package/resources/3DScene/geometry9.png +0 -0
- package/resources/3DScene/lilienstein.hdr +0 -0
- package/resources/3DScene/sky.hdr +0 -0
- package/resources/3DScene/studio.env +0 -0
- package/resources/3DScene/wastelandCloudsPureSky.hdr +0 -0
|
@@ -42,11 +42,12 @@ const fast_xml_parser_1 = require("fast-xml-parser");
|
|
|
42
42
|
const catalog_1 = require("./catalog");
|
|
43
43
|
const archive_1 = require("./archive");
|
|
44
44
|
const builder_1 = require("./builder");
|
|
45
|
+
const config_1 = require("./config");
|
|
46
|
+
const plan_1 = require("./plan");
|
|
45
47
|
const registry_1 = require("./registry");
|
|
46
48
|
const validate_1 = require("./validate");
|
|
47
49
|
const process_1 = require("./process");
|
|
48
50
|
const posix_process_1 = require("./posix-process");
|
|
49
|
-
const package_resource_1 = require("./package-resource");
|
|
50
51
|
const atomic_1 = require("../utils/atomic");
|
|
51
52
|
const local_path_1 = require("../utils/local-path");
|
|
52
53
|
/** Same parser options as the creation path, so a rewrite round-trips identically. */
|
|
@@ -55,19 +56,54 @@ const builderOptions = { ignoreAttributes: false, format: true, suppressEmptyNod
|
|
|
55
56
|
const windowsPath = (value, platform) => (0, local_path_1.nativeSeparators)(value, platform);
|
|
56
57
|
/** The separator the recorded entries use on the target platform. */
|
|
57
58
|
const separatorOf = (platform) => (platform === 'win32' ? '\\' : '/');
|
|
58
|
-
/**
|
|
59
|
-
exports.updatableModules =
|
|
59
|
+
/** Every catalog module can be updated remotely; local overrides are excluded per environment. */
|
|
60
|
+
exports.updatableModules = Object.keys(catalog_1.catalog).sort();
|
|
61
|
+
/**
|
|
62
|
+
* Server-root settings the environment owns.
|
|
63
|
+
*
|
|
64
|
+
* `create-env` never copies them out of the archive, because bundled startup
|
|
65
|
+
* overrides must not decide how a generated environment runs. An update replaces
|
|
66
|
+
* the whole `Server` directory, so it carries the deployed files across the
|
|
67
|
+
* replacement instead of deleting them with the old version.
|
|
68
|
+
*/
|
|
69
|
+
const serverOwnedFiles = ['appsettings.json', 'appsettings.development.json'];
|
|
70
|
+
/**
|
|
71
|
+
* Package files a staged artifact must provide before it may replace a deployed
|
|
72
|
+
* module. `create-env` checks the same ones while it assembles an environment, so
|
|
73
|
+
* an update cannot install something a fresh environment would have been refused.
|
|
74
|
+
*/
|
|
75
|
+
const requiredPackageFiles = {
|
|
76
|
+
server: ['Wyn.Identity.Service.dll'],
|
|
77
|
+
monitoring: ['Wyn.Configuration.dll', 'appsettings.json'],
|
|
78
|
+
};
|
|
79
|
+
/**
|
|
80
|
+
* Package entries an update must not copy over a deployed directory.
|
|
81
|
+
*
|
|
82
|
+
* Besides the environment-owned Server settings, monitoring's own `conf`
|
|
83
|
+
* directory is excluded: its assets are merged into the canonical `conf/` and the
|
|
84
|
+
* directory itself has to become a junction to `conf/` again, not a second copy
|
|
85
|
+
* of the template the Monitor would then resolve configuration from.
|
|
86
|
+
*/
|
|
87
|
+
const excludedCopyNames = (mapping) => mapping.role === 'server' ? [...serverOwnedFiles] : mapping.role === 'monitor' ? ['conf'] : [];
|
|
88
|
+
/**
|
|
89
|
+
* The modules a selection may name, from the same table `create-env` and the web
|
|
90
|
+
* interface read.
|
|
91
|
+
*
|
|
92
|
+
* Reading it through the call keeps this module loadable on its own: the policy
|
|
93
|
+
* is only needed while an update records a module it installs for the first time.
|
|
94
|
+
*/
|
|
95
|
+
const selectableModules = () => new Set([...new Set(Object.values(plan_1.optionalModulesByEdition).flat())]);
|
|
60
96
|
const installationDirectory = (root, destination) => path.join(root, ...destination.split('/'));
|
|
97
|
+
/**
|
|
98
|
+
* The edition an environment deploys. The registry records a free string and
|
|
99
|
+
* only `en` is special everywhere else in this module, so anything else counts
|
|
100
|
+
* as `cn`, exactly like the edition gate below does.
|
|
101
|
+
*/
|
|
102
|
+
const editionOf = (entry) => entry.edition === 'en' ? 'en' : 'cn';
|
|
61
103
|
function validateUpdatableModule(module) {
|
|
62
|
-
if (
|
|
104
|
+
if (Object.prototype.hasOwnProperty.call(catalog_1.catalog, module))
|
|
63
105
|
return;
|
|
64
|
-
|
|
65
|
-
throw new Error('Module server cannot be updated: it defines the environment runtime and configuration schema. Recreate the environment with create-env instead.');
|
|
66
|
-
if (module === 'monitoring')
|
|
67
|
-
throw new Error('Module monitoring cannot be updated: it defines the configuration schema and monitor runtime. Recreate the environment with create-env instead.');
|
|
68
|
-
if (module === 'collaboration')
|
|
69
|
-
throw new Error('Module collaboration is deployed together with documents by create-env and the Server loads it from its own configuration entry; it is not updated on its own. Update `documents`, or recreate the environment.');
|
|
70
|
-
throw new Error(`Unsupported module "${module}": updateable modules are dashboard, reporting, sheets, documents, analysis and common.`);
|
|
106
|
+
throw new Error(`Unsupported module "${module}": supported modules are ${Object.keys(catalog_1.catalog).sort().join(', ')}.`);
|
|
71
107
|
}
|
|
72
108
|
/** `all` is the only allowed pseudo-module: every configured module from remote. */
|
|
73
109
|
const allModules = 'all';
|
|
@@ -214,6 +250,12 @@ function stringList(raw) {
|
|
|
214
250
|
throw new Error('Invalid environment configuration.');
|
|
215
251
|
return list;
|
|
216
252
|
}
|
|
253
|
+
/**
|
|
254
|
+
* One recorded module list of the manifest, or undefined when the environment
|
|
255
|
+
* records none. A field of the wrong shape is treated as absent rather than as an
|
|
256
|
+
* error: the update only ever adds to these lists.
|
|
257
|
+
*/
|
|
258
|
+
const stringListOrUndefined = (raw) => Array.isArray(raw) && raw.every(item => typeof item === 'string') ? [...raw] : undefined;
|
|
217
259
|
/** Replace or append one `<sys:string>` path list, matching by destination or assembly name. */
|
|
218
260
|
function rewritePathList(values, entries, platform) {
|
|
219
261
|
const strings = values.map(value => value.trim());
|
|
@@ -247,7 +289,10 @@ function rewritePathList(values, entries, platform) {
|
|
|
247
289
|
* is returned. Dashboard stays the first `Plugins` entry; every other entry keeps
|
|
248
290
|
* its order. A worker service keeps all of its other fields, including
|
|
249
291
|
* `Disabled`; a configuration without that node is updated through
|
|
250
|
-
* `<PluginWorkers>` alone.
|
|
292
|
+
* `<PluginWorkers>` alone. The Server service is kept in step the same way a
|
|
293
|
+
* worker is, and a native service the environment does not declare yet gets the
|
|
294
|
+
* empty node `create-env` writes, so a freshly installed collaboration service is
|
|
295
|
+
* actually loaded.
|
|
251
296
|
*/
|
|
252
297
|
function patchEnvironmentConfiguration(xml, entries, platform = process.platform) {
|
|
253
298
|
if (typeof xml !== 'string' || /<!DOCTYPE|<!ENTITY/i.test(xml) || fast_xml_parser_1.XMLValidator.validate(xml) !== true)
|
|
@@ -256,7 +301,7 @@ function patchEnvironmentConfiguration(xml, entries, platform = process.platform
|
|
|
256
301
|
const server = document?.SystemConfig?.Services?.Server;
|
|
257
302
|
if (!server || typeof server !== 'object' || Array.isArray(server))
|
|
258
303
|
throw new Error('Environment configuration has no Server service.');
|
|
259
|
-
const pluginEntries = entries.filter(entry => entry.role
|
|
304
|
+
const pluginEntries = entries.filter(entry => entry.role === undefined || entry.role === 'plugin');
|
|
260
305
|
if (server.Plugins !== undefined || pluginEntries.length) {
|
|
261
306
|
const plugins = server.Plugins === undefined || server.Plugins === '' ? {} : server.Plugins;
|
|
262
307
|
if (typeof plugins !== 'object' || Array.isArray(plugins))
|
|
@@ -279,18 +324,29 @@ function patchEnvironmentConfiguration(xml, entries, platform = process.platform
|
|
|
279
324
|
throw new Error('Invalid environment configuration.');
|
|
280
325
|
pluginWorkers['sys:string'] = rewritePathList(stringList(pluginWorkers['sys:string']), workers, platform);
|
|
281
326
|
server.PluginWorkers = pluginWorkers;
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
327
|
+
}
|
|
328
|
+
// Every entry a service starts from an assembly: the embedded workers and the
|
|
329
|
+
// Server itself. Each service keeps all of its other fields, including
|
|
330
|
+
// `Disabled` and the Server's `Urls` and storage settings.
|
|
331
|
+
const hosted = entries.filter(entry => (entry.role === 'worker' || entry.role === 'server') && entry.service);
|
|
332
|
+
// A native out-of-process service carries no assembly and is declared empty,
|
|
333
|
+
// exactly like the product's own `conf/Wyn.conf`: the Server loads it itself.
|
|
334
|
+
const declared = entries.filter(entry => entry.role === 'service' && entry.service);
|
|
335
|
+
const services = document?.SystemConfig?.Services;
|
|
336
|
+
if ((hosted.length || declared.length) && services && typeof services === 'object' && !Array.isArray(services)) {
|
|
337
|
+
const nodes = services;
|
|
338
|
+
for (const entry of declared)
|
|
339
|
+
if (nodes[entry.service] === undefined)
|
|
340
|
+
nodes[entry.service] = {};
|
|
341
|
+
for (const entry of hosted) {
|
|
342
|
+
const node = nodes[entry.service];
|
|
343
|
+
if (!node || typeof node !== 'object' || Array.isArray(node))
|
|
288
344
|
continue;
|
|
289
|
-
const
|
|
290
|
-
// The
|
|
345
|
+
const fields = node;
|
|
346
|
+
// The service is loaded from the path list above and uses its own directory
|
|
291
347
|
// as its base directory: keep the service in step without touching Disabled.
|
|
292
|
-
|
|
293
|
-
|
|
348
|
+
fields.WorkingDirectory = windowsPath((0, local_path_1.directoryOf)(entry.path, platform), platform);
|
|
349
|
+
fields.Assembly = (0, local_path_1.baseNameOf)(entry.path);
|
|
294
350
|
}
|
|
295
351
|
}
|
|
296
352
|
const output = new fast_xml_parser_1.XMLBuilder(builderOptions).build(document);
|
|
@@ -331,10 +387,9 @@ async function readManifest(root) {
|
|
|
331
387
|
}
|
|
332
388
|
if (manifest.selectedModules !== undefined && (!Array.isArray(manifest.selectedModules) || manifest.selectedModules.some(module => typeof module !== 'string')))
|
|
333
389
|
throw new Error('Environment manifest is unreadable.');
|
|
334
|
-
//
|
|
335
|
-
//
|
|
336
|
-
|
|
337
|
-
throw new Error('Environment manifest is unreadable.');
|
|
390
|
+
// A stale `resources` key written by an older version is ignored here and
|
|
391
|
+
// dropped when the manifest is rewritten: the key belongs to the removed
|
|
392
|
+
// packaged-resource feature, not to the environment's configuration.
|
|
338
393
|
if (manifest.moduleSources !== undefined && (!manifest.moduleSources || typeof manifest.moduleSources !== 'object' || Array.isArray(manifest.moduleSources)))
|
|
339
394
|
throw new Error('Environment manifest is unreadable.');
|
|
340
395
|
for (const source of Object.values(manifest.moduleSources ?? {})) {
|
|
@@ -403,7 +458,7 @@ async function resolveRemoteArtifact(client, entry, spec) {
|
|
|
403
458
|
return { module, plugin: source.plugin, version, name, path: remotePath, mappings: source.mappings.map(mapping => ({ ...mapping, assemblies: [...mapping.assemblies] })) };
|
|
404
459
|
}
|
|
405
460
|
/** Download, hash, extract and verify one resolved artifact inside the staging workspace. */
|
|
406
|
-
async function stageRemoteArtifact(client, artifact, workspace, onProgress) {
|
|
461
|
+
async function stageRemoteArtifact(client, artifact, edition, workspace, onProgress) {
|
|
407
462
|
const label = `[${artifact.module}] ${artifact.plugin}/${artifact.version}/${artifact.name}`;
|
|
408
463
|
const download = path.join(workspace, 'download', artifact.module);
|
|
409
464
|
const expanded = path.join(workspace, `expanded-${artifact.module}`);
|
|
@@ -418,10 +473,18 @@ async function stageRemoteArtifact(client, artifact, workspace, onProgress) {
|
|
|
418
473
|
onProgress?.(`${label}: Extracting and validating ZIP`);
|
|
419
474
|
await (0, archive_1.extractArchive)(archive, expanded);
|
|
420
475
|
const assemblies = new Map();
|
|
421
|
-
|
|
476
|
+
// A mapping of the other edition is never required: its source directory
|
|
477
|
+
// does not even have to be in the archive.
|
|
478
|
+
for (const mapping of (0, catalog_1.editionMappings)(artifact.mappings, edition)) {
|
|
422
479
|
const source = path.join(expanded, mapping.source);
|
|
423
480
|
if (!(await fs.lstat(source)).isDirectory())
|
|
424
481
|
throw new Error('Required source directory missing.');
|
|
482
|
+
// A `resource` mapping is a static tree with no assembly to verify and no
|
|
483
|
+
// runtimeconfig to read; the Server only serves it.
|
|
484
|
+
if (mapping.role === 'resource')
|
|
485
|
+
continue;
|
|
486
|
+
if (mapping.role === 'service' && mapping.assemblies.length === 0)
|
|
487
|
+
continue;
|
|
425
488
|
const present = [];
|
|
426
489
|
for (const name of mapping.assemblies) {
|
|
427
490
|
try {
|
|
@@ -436,6 +499,12 @@ async function stageRemoteArtifact(client, artifact, workspace, onProgress) {
|
|
|
436
499
|
if (present.length !== 1)
|
|
437
500
|
throw new Error(`Expected one runtime assembly in ${mapping.destination || 'root'}.`);
|
|
438
501
|
assemblies.set(mapping.destination, present[0]);
|
|
502
|
+
// A plugin is loaded into the Server's own runtime; every other managed
|
|
503
|
+
// entry point is started by the Monitor and needs its own runtimeconfig for
|
|
504
|
+
// the .NET host to resolve the framework at all — exactly what create-env
|
|
505
|
+
// requires before it deploys one.
|
|
506
|
+
if (mapping.role !== 'plugin')
|
|
507
|
+
await (0, builder_1.regularFile)(path.join(source, present[0].replace(/\.dll$/, '.runtimeconfig.json')));
|
|
439
508
|
}
|
|
440
509
|
onProgress?.(`${label}: Archive and layout checks complete`);
|
|
441
510
|
return { ...artifact, sha256, expanded, assemblies };
|
|
@@ -444,6 +513,96 @@ async function stageRemoteArtifact(client, artifact, workspace, onProgress) {
|
|
|
444
513
|
throw new Error(`Module update failed for ${artifact.plugin}/${artifact.version}/${artifact.name} (download, archive or layout): ${error.message}`);
|
|
445
514
|
}
|
|
446
515
|
}
|
|
516
|
+
/**
|
|
517
|
+
* The configuration schema this environment already runs.
|
|
518
|
+
*
|
|
519
|
+
* `create-env` recorded the schema it detected from the Monitor template in the
|
|
520
|
+
* manifest; an environment whose manifest predates the field falls back to the
|
|
521
|
+
* namespace its live `conf/Wyn.conf` declares. A Server or Monitor update needs
|
|
522
|
+
* this to check that the new package ships the matching XSD assets.
|
|
523
|
+
*/
|
|
524
|
+
function environmentSchema(manifest, configuration) {
|
|
525
|
+
if (manifest.schema === 'V91' || manifest.schema === 'V100')
|
|
526
|
+
return manifest.schema;
|
|
527
|
+
try {
|
|
528
|
+
return (0, config_1.detectConfigurationSchema)(configuration);
|
|
529
|
+
}
|
|
530
|
+
catch {
|
|
531
|
+
return undefined;
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
/**
|
|
535
|
+
* Check a staged Server or Monitor package against the deployed environment.
|
|
536
|
+
*
|
|
537
|
+
* `create-env` verifies the same things while it assembles: the Server's
|
|
538
|
+
* identity service, the Monitor's configuration assembly and settings, and the
|
|
539
|
+
* `xsd/<schema>/Wyn.conf.xsd` both packages ship for the configuration schema the
|
|
540
|
+
* environment declares. A Server package may legitimately omit its XSD — Wyn's
|
|
541
|
+
* configuration reader falls back to ordinary XML deserialization — which
|
|
542
|
+
* `create-env` records as a warning rather than rejecting, so the update keeps
|
|
543
|
+
* that policy. Returns the warning text, or undefined when the package is
|
|
544
|
+
* complete.
|
|
545
|
+
*/
|
|
546
|
+
async function verifyStagedPackage(module, staged, schema) {
|
|
547
|
+
const label = `${staged.plugin}/${staged.version}/${staged.name}`;
|
|
548
|
+
for (const name of requiredPackageFiles[module] ?? []) {
|
|
549
|
+
try {
|
|
550
|
+
await (0, builder_1.regularFile)(path.join(staged.expanded, name));
|
|
551
|
+
}
|
|
552
|
+
catch {
|
|
553
|
+
throw new Error(`Module update failed for ${label}: the package is missing ${name}.`);
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
const relative = `xsd/${schema}/Wyn.conf.xsd`;
|
|
557
|
+
try {
|
|
558
|
+
await (0, builder_1.regularFile)(path.join(staged.expanded, relative));
|
|
559
|
+
return undefined;
|
|
560
|
+
}
|
|
561
|
+
catch (error) {
|
|
562
|
+
if (module === 'server' && error.code === 'ENOENT') {
|
|
563
|
+
return `Server package ${label} is missing ${relative}; Server will skip XSD validation. XML reader compatibility and runtime startup have not been verified.`;
|
|
564
|
+
}
|
|
565
|
+
throw new Error(`Configuration schema validation failed for ${label}: ${module === 'server' ? 'Server' : 'Monitor'} package is missing ${relative}.`);
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
/** Copy a tree over an existing one, replacing files instead of refusing a collision. */
|
|
569
|
+
async function overlayTree(source, destination) {
|
|
570
|
+
await fs.mkdir(destination, { recursive: true });
|
|
571
|
+
for (const entry of await fs.readdir(source, { withFileTypes: true })) {
|
|
572
|
+
const from = path.join(source, entry.name);
|
|
573
|
+
const to = path.join(destination, entry.name);
|
|
574
|
+
if (entry.isDirectory())
|
|
575
|
+
await overlayTree(from, to);
|
|
576
|
+
else if (entry.isFile())
|
|
577
|
+
await fs.copyFile(from, to);
|
|
578
|
+
else
|
|
579
|
+
throw new Error('Unexpected link in extracted archive.');
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
/**
|
|
583
|
+
* Merge a monitoring package's `conf` assets into the canonical configuration
|
|
584
|
+
* directory.
|
|
585
|
+
*
|
|
586
|
+
* `create-env` moves everything but the `Wyn.conf` template out of the Monitor
|
|
587
|
+
* package and leaves the environment's own `conf/Wyn.conf` as the single editable
|
|
588
|
+
* configuration, so the update refreshes the package's assets in place and never
|
|
589
|
+
* touches that file. An asset the new package no longer ships stays where it is:
|
|
590
|
+
* the directory also holds whatever the environment itself added.
|
|
591
|
+
*/
|
|
592
|
+
async function mergeConfigurationAssets(source, destination) {
|
|
593
|
+
for (const entry of await fs.readdir(source, { withFileTypes: true })) {
|
|
594
|
+
if (entry.name.toLowerCase() === 'wyn.conf')
|
|
595
|
+
continue;
|
|
596
|
+
const from = path.join(source, entry.name);
|
|
597
|
+
const to = path.join(destination, entry.name);
|
|
598
|
+
if (entry.isDirectory())
|
|
599
|
+
await overlayTree(from, to);
|
|
600
|
+
else if (entry.isFile())
|
|
601
|
+
await fs.copyFile(from, to);
|
|
602
|
+
else
|
|
603
|
+
throw new Error('Unexpected link in extracted archive.');
|
|
604
|
+
}
|
|
605
|
+
}
|
|
447
606
|
/** Mappings of one module that a local source may override. */
|
|
448
607
|
const localMappings = (module) => catalog_1.catalog[module].mappings.filter(mapping => mapping.role === 'plugin' || mapping.role === 'worker');
|
|
449
608
|
const expectedAssemblies = (module) => [...new Set(localMappings(module).flatMap(mapping => mapping.assemblies))];
|
|
@@ -467,7 +626,18 @@ function mappingForAssembly(module, assembly) {
|
|
|
467
626
|
* explicitly, for a build whose file name matches no assembly at all.
|
|
468
627
|
*/
|
|
469
628
|
async function resolveLocalSource(entry, module, assembly, input) {
|
|
629
|
+
// The built-in resource module is never a developer-build target: it ships a
|
|
630
|
+
// static asset tree that no local DLL could stand in for, so a local source is
|
|
631
|
+
// refused on its own terms before any path is even looked at. Remote specs
|
|
632
|
+
// (latest, pinned and `all`) keep working.
|
|
633
|
+
if (plan_1.trailingModules.includes(module))
|
|
634
|
+
throw new Error(`Module ${module} is built in and has no local source: it ships the 3D scene and map assets and is always deployed from its published artifact. Refresh it from the cloud (\`${module}\`, \`${module}:<version>\` or \`all\`).`);
|
|
470
635
|
const mappings = localMappings(module);
|
|
636
|
+
// The Server, Monitor and collaboration deployments are whole services, not a
|
|
637
|
+
// plugin the Server loads: a developer build has no place to point the
|
|
638
|
+
// configuration at, so they are refreshed from their artifact only.
|
|
639
|
+
if (!mappings.length)
|
|
640
|
+
throw new Error(`Module ${module} is deployed from its artifact only: it has no plugin or worker assembly a local build could replace. Update it from a remote artifact, or recreate the environment with create-env.`);
|
|
471
641
|
const source = path.resolve(input);
|
|
472
642
|
const stat = await lstatOrUndefined(source);
|
|
473
643
|
if (!stat)
|
|
@@ -551,14 +721,12 @@ class EnvironmentUpdater {
|
|
|
551
721
|
registry;
|
|
552
722
|
processes;
|
|
553
723
|
platform;
|
|
554
|
-
bundledResources;
|
|
555
724
|
onProgress;
|
|
556
725
|
constructor(dependencies = {}) {
|
|
557
726
|
this.dependencies = dependencies;
|
|
558
727
|
this.registry = dependencies.registry ?? new registry_1.EnvironmentRegistry();
|
|
559
728
|
this.processes = dependencies.processes ?? (process.platform === 'win32' ? new process_1.WindowsProcessBackend() : new posix_process_1.PosixProcessBackend());
|
|
560
729
|
this.platform = dependencies.platform ?? process.platform;
|
|
561
|
-
this.bundledResources = dependencies.bundledResourcesDirectory ?? (0, package_resource_1.bundledResourcesDirectory)();
|
|
562
730
|
this.onProgress = dependencies.onProgress;
|
|
563
731
|
}
|
|
564
732
|
async update(name, specs, clientFactory, options = {}) {
|
|
@@ -566,9 +734,8 @@ class EnvironmentUpdater {
|
|
|
566
734
|
const onProgress = options.onProgress ?? this.onProgress;
|
|
567
735
|
onProgress?.('Validating module specs');
|
|
568
736
|
let parsed = normalizeModuleSpecs(specs);
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
// them without touching a single module.
|
|
737
|
+
if (!parsed.length)
|
|
738
|
+
throw new Error('At least one module spec is required: pass `all` or one or more module names (module, module:<version> or module=<local path>).');
|
|
572
739
|
const entry = await this.registry.get(name);
|
|
573
740
|
onProgress?.(`Validating environment ${entry.name} at ${entry.root}`);
|
|
574
741
|
await (0, validate_1.validateEnvironmentRoot)(entry, this.registry.directory);
|
|
@@ -588,7 +755,7 @@ class EnvironmentUpdater {
|
|
|
588
755
|
skipped.push({ module, reason: 'local' });
|
|
589
756
|
const selected = configured.filter(module => !localModules.has(module));
|
|
590
757
|
if (!selected.length)
|
|
591
|
-
throw new Error(`Module spec all matched no updatable modules: ${configured.length ? configured.map(module => `${module} has a local source`).join('; ') : '
|
|
758
|
+
throw new Error(`Module spec all matched no updatable modules: ${configured.length ? configured.map(module => `${module} has a local source`).join('; ') : 'this environment has no installed module to refresh'}.`);
|
|
592
759
|
parsed = selected.map(module => ({ module }));
|
|
593
760
|
onProgress?.(`Updating all configured modules from remote: ${selected.join(', ')}${skipped.length ? ` (skipping local: ${skipped.map(item => item.module).join(', ')})` : ''}`);
|
|
594
761
|
}
|
|
@@ -612,6 +779,22 @@ class EnvironmentUpdater {
|
|
|
612
779
|
localOverrides.set(spec.module, targets);
|
|
613
780
|
}
|
|
614
781
|
const remote = parsed.filter(spec => spec.localPath === undefined);
|
|
782
|
+
// The built-in resource modules deploy their trees *inside* the Server
|
|
783
|
+
// directory, and a Server refresh replaces that whole directory. An
|
|
784
|
+
// environment that has such a module therefore re-deploys it from its own
|
|
785
|
+
// artifact whenever the Server is refreshed in the same call: a remote-only
|
|
786
|
+
// module has no local copy to restore the trees from.
|
|
787
|
+
const autoRefreshed = [];
|
|
788
|
+
if (remote.some(spec => spec.module === 'server')) {
|
|
789
|
+
for (const module of plan_1.trailingModules) {
|
|
790
|
+
if (!installed.has(module) || parsed.some(spec => spec.module === module))
|
|
791
|
+
continue;
|
|
792
|
+
parsed.push({ module });
|
|
793
|
+
remote.push({ module });
|
|
794
|
+
autoRefreshed.push(module);
|
|
795
|
+
onProgress?.(`${module} is refreshed with the Server package: its trees deploy inside the Server directory`);
|
|
796
|
+
}
|
|
797
|
+
}
|
|
615
798
|
// Smart documents cannot run without the collaboration service, which
|
|
616
799
|
// create-env deploys and declares in the Server configuration alongside
|
|
617
800
|
// documents. Installing documents into an environment with no collaboration
|
|
@@ -625,24 +808,19 @@ class EnvironmentUpdater {
|
|
|
625
808
|
throw new Error(`Cannot update modules from a remote source while ${entry.name} is ${state}: deployed files may be in use. Run \`afm env stop ${entry.name}\` first.`);
|
|
626
809
|
if (!remote.length && state !== 'ready' && state !== 'stopped' && state !== 'running')
|
|
627
810
|
throw new Error(`Cannot update modules while ${entry.name} is ${state}: the runtime identity must be verified before its files are changed. Run \`afm env stop ${entry.name}\` first.`);
|
|
628
|
-
// The packaged 3D scene assets belong to every environment, not only to one
|
|
629
|
-
// that create-env assembled: the plan below finds the files an environment is
|
|
630
|
-
// still missing (or that differ) and the apply writes them with the rest of
|
|
631
|
-
// the update. Planning touches nothing and runs before any download or write,
|
|
632
|
-
// so an incomplete installation fails loudly with the path it expected and
|
|
633
|
-
// leaves the environment exactly as it was.
|
|
634
|
-
const resourceSource = (0, package_resource_1.bundledResourceDirectory)(package_resource_1.sceneResourceName, this.bundledResources);
|
|
635
|
-
const resourceDestination = path.join(entry.root, ...package_resource_1.sceneResourceDestination.split('/'));
|
|
636
|
-
onProgress?.(`Checking bundled 3D scene resources in ${package_resource_1.sceneResourceDestination}`);
|
|
637
|
-
const resourcePlan = await (0, package_resource_1.planBundledResource)(resourceSource, resourceDestination);
|
|
638
|
-
const resourceWrites = resourcePlan.added + resourcePlan.updated;
|
|
639
|
-
const resource = { name: package_resource_1.sceneResourceName, source: resourceSource, destination: package_resource_1.sceneResourceDestination, files: resourcePlan.files.length, added: resourcePlan.added, updated: resourcePlan.updated };
|
|
640
|
-
const resourceSummary = `${resourcePlan.files.length - resourceWrites} identical, ${resourcePlan.added} missing, ${resourcePlan.updated} different`;
|
|
641
|
-
onProgress?.(`Bundled 3D scene resources in ${package_resource_1.sceneResourceDestination}: ${dryRun ? 'dry run; ' : ''}${resourceSummary}`);
|
|
642
811
|
const downloads = [...(manifest.downloads ?? [])];
|
|
643
|
-
const selectedModules = manifest.selectedModules
|
|
812
|
+
const selectedModules = stringListOrUndefined(manifest.selectedModules);
|
|
813
|
+
const autoAddedModules = stringListOrUndefined(manifest.autoAddedModules);
|
|
644
814
|
const moduleSources = { ...(manifest.moduleSources ?? {}) };
|
|
645
|
-
|
|
815
|
+
// What a fresh environment would have selected for itself. An update records
|
|
816
|
+
// a first install the way `create-env` does: a selectable module joins the
|
|
817
|
+
// selection, a core component or an automatic dependency is added instead.
|
|
818
|
+
const selectable = selectableModules();
|
|
819
|
+
// The manifest records the .NET requirements read from each artifact's
|
|
820
|
+
// runtimeconfig files, so an update replaces the entries of the modules it
|
|
821
|
+
// refreshes and leaves every other module's alone.
|
|
822
|
+
let requirements = Array.isArray(manifest.requirements) ? manifest.requirements.filter((line) => typeof line === 'string') : [];
|
|
823
|
+
const workspace = !dryRun && remote.length > 0 ? await fs.mkdtemp(path.join(path.dirname(entry.root), '.afm-env-')) : undefined;
|
|
646
824
|
const warnings = [];
|
|
647
825
|
try {
|
|
648
826
|
const resolved = new Map();
|
|
@@ -655,12 +833,34 @@ class EnvironmentUpdater {
|
|
|
655
833
|
}
|
|
656
834
|
if (!dryRun)
|
|
657
835
|
for (const spec of remote)
|
|
658
|
-
staged.set(spec.module, await stageRemoteArtifact(client, resolved.get(spec.module), workspace, onProgress));
|
|
836
|
+
staged.set(spec.module, await stageRemoteArtifact(client, resolved.get(spec.module), editionOf(entry), workspace, onProgress));
|
|
659
837
|
}
|
|
660
838
|
const configured = await fs.readFile(path.join(entry.root, 'conf', 'Wyn.conf'), 'utf8');
|
|
839
|
+
// A Server or Monitor package is only usable when it still serves the
|
|
840
|
+
// configuration schema this environment runs, so both are checked against
|
|
841
|
+
// the deployed schema before anything is planned around them.
|
|
842
|
+
const schema = remote.some(spec => spec.module === 'server' || spec.module === 'monitoring')
|
|
843
|
+
? environmentSchema(manifest, configured)
|
|
844
|
+
: undefined;
|
|
845
|
+
if (remote.some(spec => spec.module === 'server' || spec.module === 'monitoring') && !schema) {
|
|
846
|
+
throw new Error(`Module update for ${remote.filter(spec => spec.module === 'server' || spec.module === 'monitoring').map(spec => spec.module).join(' and ')} needs the configuration schema of ${entry.name}, which neither its manifest nor its conf/Wyn.conf records. Recreate the environment with create-env.`);
|
|
847
|
+
}
|
|
848
|
+
for (const spec of remote) {
|
|
849
|
+
const stagedArtifact = staged.get(spec.module);
|
|
850
|
+
if (!stagedArtifact || !schema || !(spec.module in requiredPackageFiles))
|
|
851
|
+
continue;
|
|
852
|
+
const warning = await verifyStagedPackage(spec.module, stagedArtifact, schema);
|
|
853
|
+
if (warning)
|
|
854
|
+
warnings.push(warning);
|
|
855
|
+
}
|
|
661
856
|
const plans = [];
|
|
662
857
|
// A module may carry several local specs: plan it once, for all its targets.
|
|
663
|
-
|
|
858
|
+
// The built-in trailing modules are planned last, in the same order
|
|
859
|
+
// `create-env` assembles: their trees live inside other modules'
|
|
860
|
+
// directories, so a Server replacement must not wipe them again.
|
|
861
|
+
const planOrder = [...new Set(parsed.map(spec => spec.module))]
|
|
862
|
+
.sort((left, right) => Number(plan_1.trailingModules.includes(left)) - Number(plan_1.trailingModules.includes(right)));
|
|
863
|
+
for (const module of planOrder) {
|
|
664
864
|
const artifact = artifacts.find(candidate => candidate.module === module);
|
|
665
865
|
const previous = moduleSources[module] ? { local: moduleSources[module].path } : { version: artifact?.version };
|
|
666
866
|
const override = localOverrides.get(module);
|
|
@@ -685,7 +885,7 @@ class EnvironmentUpdater {
|
|
|
685
885
|
});
|
|
686
886
|
plans.push({
|
|
687
887
|
module, previous, next: { local: override.map(target => target.path).join(', ') }, local: override,
|
|
688
|
-
entries, copies: [], removals, copiedDirectories: [], removedDirectories,
|
|
888
|
+
entries, copies: [], removals, preserved: [], copiedDirectories: [], removedDirectories,
|
|
689
889
|
});
|
|
690
890
|
continue;
|
|
691
891
|
}
|
|
@@ -693,30 +893,65 @@ class EnvironmentUpdater {
|
|
|
693
893
|
const stagedArtifact = staged.get(module);
|
|
694
894
|
const copies = [];
|
|
695
895
|
const removals = [];
|
|
896
|
+
const preserved = [];
|
|
696
897
|
const copiedDirectories = [];
|
|
697
898
|
const removedDirectories = [];
|
|
698
899
|
const entries = [];
|
|
699
|
-
|
|
900
|
+
let configurationAssets;
|
|
901
|
+
let configurationLink;
|
|
902
|
+
// Only the mappings this environment's edition deploys: the other
|
|
903
|
+
// edition's directory is never removed, copied or referenced.
|
|
904
|
+
for (const mapping of (0, catalog_1.editionMappings)(remoteArtifact.mappings, editionOf(entry))) {
|
|
700
905
|
const destination = installationDirectory(entry.root, mapping.destination);
|
|
906
|
+
// A `resource` directory the module already deployed is replaced, never
|
|
907
|
+
// merged, and it contributes no plugin, worker, service or runtime entry.
|
|
701
908
|
if (await exists(destination)) {
|
|
702
909
|
removals.push(destination);
|
|
703
910
|
removedDirectories.push(mapping.destination);
|
|
704
911
|
}
|
|
912
|
+
// The Server settings the environment owns are never taken from the
|
|
913
|
+
// archive and must survive the replacement, so carry each one that is
|
|
914
|
+
// actually deployed across the directory swap.
|
|
915
|
+
if (stagedArtifact && mapping.role === 'server' && workspace) {
|
|
916
|
+
for (const name of excludedCopyNames(mapping)) {
|
|
917
|
+
if (await exists(path.join(destination, name)))
|
|
918
|
+
preserved.push({ directory: destination, name, source: path.join(workspace, 'preserved', module, name) });
|
|
919
|
+
}
|
|
920
|
+
}
|
|
705
921
|
if (stagedArtifact)
|
|
706
|
-
copies.push({ source: path.join(stagedArtifact.expanded, mapping.source), destination });
|
|
922
|
+
copies.push({ source: path.join(stagedArtifact.expanded, mapping.source), destination, exclude: excludedCopyNames(mapping) });
|
|
707
923
|
copiedDirectories.push(mapping.destination);
|
|
924
|
+
if (mapping.role === 'resource')
|
|
925
|
+
continue;
|
|
926
|
+
// A native out-of-process service has no assembly and is declared as an
|
|
927
|
+
// empty node: the Server loads it from that entry and supplies the rest.
|
|
928
|
+
if (mapping.role === 'service') {
|
|
929
|
+
if (mapping.service)
|
|
930
|
+
entries.push({ destination: mapping.destination, assemblies: [], path: mapping.destination, role: 'service', service: mapping.service });
|
|
931
|
+
continue;
|
|
932
|
+
}
|
|
708
933
|
const assembly = stagedArtifact?.assemblies.get(mapping.destination) ?? knownAssembly(configured, mapping, this.platform);
|
|
709
934
|
// Plugins stay relative to the environment root; a worker entry does too,
|
|
710
935
|
// and its service keeps using the deployed destination as working directory.
|
|
711
|
-
|
|
936
|
+
// The Server service the Monitor starts is kept in step the same way.
|
|
937
|
+
if (mapping.role === 'plugin' || mapping.role === 'worker' || mapping.role === 'server') {
|
|
712
938
|
const entry = { destination: mapping.destination, assemblies: mapping.assemblies, path: path.posix.join(mapping.destination, assembly) };
|
|
713
|
-
if (mapping.role === 'worker') {
|
|
714
|
-
entry.role =
|
|
939
|
+
if (mapping.role === 'worker' || mapping.role === 'server') {
|
|
940
|
+
entry.role = mapping.role;
|
|
715
941
|
if (mapping.service)
|
|
716
942
|
entry.service = mapping.service;
|
|
717
943
|
}
|
|
718
944
|
entries.push(entry);
|
|
719
945
|
}
|
|
946
|
+
// The Monitor resolves its configuration from its own working directory,
|
|
947
|
+
// so its package assets belong in the canonical `conf` and the directory
|
|
948
|
+
// itself has to point back at it once the Monitor tree is replaced.
|
|
949
|
+
if (mapping.role === 'monitor' && stagedArtifact) {
|
|
950
|
+
const assets = path.join(stagedArtifact.expanded, 'conf');
|
|
951
|
+
if ((await fs.lstat(assets).catch(() => undefined))?.isDirectory())
|
|
952
|
+
configurationAssets = { source: assets, destination: path.join(entry.root, 'conf') };
|
|
953
|
+
configurationLink = path.join(destination, 'conf');
|
|
954
|
+
}
|
|
720
955
|
}
|
|
721
956
|
if (module === 'dashboard' && stagedArtifact) {
|
|
722
957
|
const contents = path.join(stagedArtifact.expanded, 'Web Contents');
|
|
@@ -726,13 +961,14 @@ class EnvironmentUpdater {
|
|
|
726
961
|
removals.push(destination);
|
|
727
962
|
removedDirectories.push('Web Contents');
|
|
728
963
|
}
|
|
729
|
-
copies.push({ source: contents, destination });
|
|
964
|
+
copies.push({ source: contents, destination, exclude: [] });
|
|
730
965
|
copiedDirectories.push('Web Contents');
|
|
731
966
|
}
|
|
732
967
|
}
|
|
733
968
|
plans.push({
|
|
734
969
|
module, previous, next: { version: remoteArtifact.version }, artifact: remoteArtifact, staged: stagedArtifact,
|
|
735
|
-
entries, copies, removals,
|
|
970
|
+
entries, copies, removals, preserved, ...(configurationAssets ? { configurationAssets } : {}), ...(configurationLink ? { configurationLink } : {}),
|
|
971
|
+
copiedDirectories, removedDirectories,
|
|
736
972
|
});
|
|
737
973
|
}
|
|
738
974
|
const changes = plans.map(plan => ({ module: plan.module, previous: plan.previous, next: plan.next, copiedDirectories: plan.copiedDirectories, removedDirectories: plan.removedDirectories }));
|
|
@@ -757,65 +993,109 @@ class EnvironmentUpdater {
|
|
|
757
993
|
nextDownloads.push(download);
|
|
758
994
|
else
|
|
759
995
|
nextDownloads[downloadIndex] = download;
|
|
996
|
+
// The requirements the old artifact needed are gone with it: re-read
|
|
997
|
+
// the ones the new package actually declares, per deployed mapping.
|
|
998
|
+
// A resource mapping is a static tree: it declares none.
|
|
999
|
+
requirements = requirements.filter(line => !line.endsWith(` (${plan.module})`));
|
|
1000
|
+
for (const mapping of (0, catalog_1.editionMappings)(plan.artifact.mappings, editionOf(entry))) {
|
|
1001
|
+
if (mapping.role === 'service' || mapping.role === 'resource')
|
|
1002
|
+
continue;
|
|
1003
|
+
for (const framework of await (0, builder_1.runtimeRequirements)(path.join(plan.staged.expanded, mapping.source))) {
|
|
1004
|
+
requirements.push(`${framework.name} ${framework.version} (${plan.module})`);
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
760
1007
|
}
|
|
761
1008
|
delete moduleSources[plan.module];
|
|
762
1009
|
}
|
|
763
1010
|
if (plan.local)
|
|
764
1011
|
moduleSources[plan.module] = manifestSource(plan.module, plan.local, moduleSources[plan.module]);
|
|
765
|
-
|
|
766
|
-
|
|
1012
|
+
// First installing a module records it the way `create-env` would: a
|
|
1013
|
+
// selectable one joins the selection, a core component or an automatic
|
|
1014
|
+
// dependency joins the automatically added list instead.
|
|
1015
|
+
if (!installed.has(plan.module)) {
|
|
1016
|
+
if (selectable.has(plan.module)) {
|
|
1017
|
+
if (selectedModules)
|
|
1018
|
+
selectedModules.push(plan.module);
|
|
1019
|
+
}
|
|
1020
|
+
else if (autoAddedModules)
|
|
1021
|
+
autoAddedModules.push(plan.module);
|
|
1022
|
+
}
|
|
767
1023
|
}
|
|
768
1024
|
if (selectedModules) {
|
|
769
1025
|
const unique = [...new Set(selectedModules)].sort();
|
|
770
1026
|
selectedModules.length = 0;
|
|
771
1027
|
selectedModules.push(...unique);
|
|
772
1028
|
}
|
|
773
|
-
|
|
1029
|
+
if (autoAddedModules) {
|
|
1030
|
+
const unique = [...new Set(autoAddedModules)].sort();
|
|
1031
|
+
autoAddedModules.length = 0;
|
|
1032
|
+
autoAddedModules.push(...unique);
|
|
1033
|
+
}
|
|
1034
|
+
const nextManifest = { ...manifest, artifacts: nextArtifacts, downloads: nextDownloads, requirements: [...new Set(requirements)] };
|
|
1035
|
+
if (autoAddedModules)
|
|
1036
|
+
nextManifest.autoAddedModules = autoAddedModules;
|
|
774
1037
|
if (Object.keys(moduleSources).length)
|
|
775
1038
|
nextManifest.moduleSources = moduleSources;
|
|
776
1039
|
else
|
|
777
1040
|
delete nextManifest.moduleSources;
|
|
778
1041
|
if (selectedModules)
|
|
779
1042
|
nextManifest.selectedModules = selectedModules;
|
|
780
|
-
//
|
|
781
|
-
//
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
const resources = [...(manifest.resources ?? [])];
|
|
785
|
-
const recorded = resources.findIndex(candidate => candidate.name === package_resource_1.sceneResourceName);
|
|
786
|
-
const record = { name: package_resource_1.sceneResourceName, source: resource.source, destination: resource.destination, files: resource.files };
|
|
787
|
-
if (recorded < 0)
|
|
788
|
-
resources.push(record);
|
|
789
|
-
else
|
|
790
|
-
resources[recorded] = { ...resources[recorded], ...record };
|
|
791
|
-
nextManifest.resources = resources;
|
|
792
|
-
const modules = [...new Set([...entry.modules, ...plans.filter(plan => !installed.has(plan.module)).map(plan => plan.module)])].sort();
|
|
1043
|
+
// A stale `resources` key from an older manifest is dropped here: the
|
|
1044
|
+
// packaged-resource feature is gone and nothing records it any more.
|
|
1045
|
+
delete nextManifest.resources;
|
|
1046
|
+
const modules = [...new Set([...entry.modules, ...plans.filter(plan => !installed.has(plan.module) && selectable.has(plan.module)).map(plan => plan.module)])].sort();
|
|
793
1047
|
const localModules = Object.keys(moduleSources).sort();
|
|
794
1048
|
if (state === 'running')
|
|
795
1049
|
warnings.push(`${entry.name} is running; restart it to load the updated modules.`);
|
|
796
1050
|
for (const item of skipped)
|
|
797
1051
|
warnings.push(`${item.module} has a local source and was not updated from artifacts.`);
|
|
1052
|
+
// The Server and Monitor packages each ship the XSD assets for a
|
|
1053
|
+
// configuration schema, so replacing one of them alone leaves the
|
|
1054
|
+
// environment mixing two package generations for the same configuration.
|
|
1055
|
+
const runtime = plans.filter(plan => plan.module === 'server' || plan.module === 'monitoring').map(plan => plan.module).sort();
|
|
1056
|
+
if (runtime.length === 1)
|
|
1057
|
+
warnings.push(`Only ${runtime[0]} was refreshed; refresh ${runtime[0] === 'server' ? 'monitoring' : 'server'} in the same call to keep both packages on one configuration schema.`);
|
|
1058
|
+
if (parsed.some(spec => spec.module === 'documents') && !parsed.some(spec => spec.module === 'collaboration')) {
|
|
1059
|
+
warnings.push('documents was refreshed without collaboration; the smart documents plugin may expect the deployed collaboration service version, so refresh collaboration too.');
|
|
1060
|
+
}
|
|
1061
|
+
if (autoRefreshed.length)
|
|
1062
|
+
warnings.push(`${autoRefreshed.join(', ')} was refreshed with the Server package: its trees live inside the Server directory and would otherwise be lost.`);
|
|
798
1063
|
if (!dryRun) {
|
|
799
1064
|
for (const plan of plans) {
|
|
1065
|
+
// Take the environment-owned settings aside first, and drop the Monitor
|
|
1066
|
+
// configuration junction before the directory holding it: a recursive
|
|
1067
|
+
// delete must never reach the canonical `conf` the junction points at.
|
|
1068
|
+
for (const file of plan.preserved) {
|
|
1069
|
+
await fs.mkdir(path.dirname(file.source), { recursive: true });
|
|
1070
|
+
await fs.copyFile(path.join(file.directory, file.name), file.source);
|
|
1071
|
+
}
|
|
1072
|
+
if (plan.configurationLink) {
|
|
1073
|
+
const existing = await fs.lstat(plan.configurationLink).catch(() => undefined);
|
|
1074
|
+
if (existing?.isSymbolicLink())
|
|
1075
|
+
await fs.unlink(plan.configurationLink);
|
|
1076
|
+
else
|
|
1077
|
+
await fs.rm(plan.configurationLink, { recursive: true, force: true });
|
|
1078
|
+
}
|
|
800
1079
|
for (const removal of plan.removals)
|
|
801
1080
|
await fs.rm(removal, { recursive: true, force: true });
|
|
802
1081
|
for (const copy of plan.copies)
|
|
803
|
-
await (0, builder_1.copyTree)(copy.source, copy.destination);
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
if (applied.added.length !== resourcePlan.added || applied.updated.length !== resourcePlan.updated)
|
|
813
|
-
throw new Error('Bundled resource layout did not match its plan.');
|
|
1082
|
+
await (0, builder_1.copyTree)(copy.source, copy.destination, copy.exclude);
|
|
1083
|
+
for (const file of plan.preserved)
|
|
1084
|
+
await fs.copyFile(file.source, path.join(file.directory, file.name));
|
|
1085
|
+
if (plan.configurationAssets) {
|
|
1086
|
+
onProgress?.(`Refreshing Monitor configuration assets in ${path.relative(entry.root, plan.configurationAssets.destination)}`);
|
|
1087
|
+
await mergeConfigurationAssets(plan.configurationAssets.source, plan.configurationAssets.destination);
|
|
1088
|
+
}
|
|
1089
|
+
if (plan.configurationLink)
|
|
1090
|
+
await fs.symlink(path.join(entry.root, 'conf'), plan.configurationLink, 'junction');
|
|
814
1091
|
}
|
|
815
|
-
//
|
|
816
|
-
//
|
|
817
|
-
|
|
818
|
-
|
|
1092
|
+
// Only a plan that really contributes something to the configuration
|
|
1093
|
+
// rewrites it: the built-in dashboard-resource module adds no plugin,
|
|
1094
|
+
// worker or service entry, so refreshing it alone leaves `conf/Wyn.conf`
|
|
1095
|
+
// exactly as it is instead of round-tripping it through the XML builder.
|
|
1096
|
+
const configurationChanged = plans.some(plan => plan.entries.length > 0 || plan.configurationAssets !== undefined || plan.configurationLink !== undefined);
|
|
1097
|
+
onProgress?.(configurationChanged ? 'Writing configuration, manifest and registry record' : 'Writing manifest and registry record');
|
|
1098
|
+
if (configurationChanged)
|
|
819
1099
|
await writeAtomic(path.join(entry.root, 'conf', 'Wyn.conf'), configuration);
|
|
820
1100
|
await writeJsonAtomic(path.join(entry.root, 'environment.lock.json'), nextManifest);
|
|
821
1101
|
await this.registry.update(entries => {
|
|
@@ -831,10 +1111,10 @@ class EnvironmentUpdater {
|
|
|
831
1111
|
delete entries[index].localModules;
|
|
832
1112
|
});
|
|
833
1113
|
}
|
|
834
|
-
const restartRequired = !dryRun &&
|
|
1114
|
+
const restartRequired = !dryRun && changes.length > 0;
|
|
835
1115
|
if (restartRequired)
|
|
836
|
-
onProgress?.(`${entry.name}:
|
|
837
|
-
return { name: entry.name, root: entry.root, dryRun, changes, skipped, restartRequired, warnings, localTargets: plans.flatMap(plan => plan.local ?? [])
|
|
1116
|
+
onProgress?.(`${entry.name}: modules updated; restart the environment to load the change (no service was started).`);
|
|
1117
|
+
return { name: entry.name, root: entry.root, dryRun, changes, skipped, restartRequired, warnings, localTargets: plans.flatMap(plan => plan.local ?? []) };
|
|
838
1118
|
}
|
|
839
1119
|
finally {
|
|
840
1120
|
if (workspace)
|