@guanjinke/artifact-client 1.7.1 → 1.8.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 +8 -1
- package/dist/cli/environment.d.ts +10 -1
- package/dist/cli/environment.d.ts.map +1 -1
- package/dist/cli/environment.js +24 -0
- package/dist/cli/environment.js.map +1 -1
- package/dist/cli/setup.js +9 -4
- 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 +16 -1
- package/dist/environment/builder.js.map +1 -1
- package/dist/environment/package-resource.d.ts +95 -0
- package/dist/environment/package-resource.d.ts.map +1 -0
- package/dist/environment/package-resource.js +323 -0
- package/dist/environment/package-resource.js.map +1 -0
- package/dist/environment/resource.d.ts +36 -0
- package/dist/environment/resource.d.ts.map +1 -0
- package/dist/environment/resource.js +101 -0
- package/dist/environment/resource.js.map +1 -0
- package/dist/environment/types.d.ts +7 -0
- package/dist/environment/types.d.ts.map +1 -1
- package/dist/environment/update.d.ts +34 -4
- package/dist/environment/update.d.ts.map +1 -1
- package/dist/environment/update.js +364 -44
- 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 +3 -2
- 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,10 +42,13 @@ 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");
|
|
51
|
+
const package_resource_1 = require("./package-resource");
|
|
49
52
|
const atomic_1 = require("../utils/atomic");
|
|
50
53
|
const local_path_1 = require("../utils/local-path");
|
|
51
54
|
/** Same parser options as the creation path, so a rewrite round-trips identically. */
|
|
@@ -54,19 +57,48 @@ const builderOptions = { ignoreAttributes: false, format: true, suppressEmptyNod
|
|
|
54
57
|
const windowsPath = (value, platform) => (0, local_path_1.nativeSeparators)(value, platform);
|
|
55
58
|
/** The separator the recorded entries use on the target platform. */
|
|
56
59
|
const separatorOf = (platform) => (platform === 'win32' ? '\\' : '/');
|
|
57
|
-
/**
|
|
58
|
-
exports.updatableModules =
|
|
60
|
+
/** Every catalog module can be updated remotely; local overrides are excluded per environment. */
|
|
61
|
+
exports.updatableModules = Object.keys(catalog_1.catalog).sort();
|
|
62
|
+
/**
|
|
63
|
+
* Server-root settings the environment owns.
|
|
64
|
+
*
|
|
65
|
+
* `create-env` never copies them out of the archive, because bundled startup
|
|
66
|
+
* overrides must not decide how a generated environment runs. An update replaces
|
|
67
|
+
* the whole `Server` directory, so it carries the deployed files across the
|
|
68
|
+
* replacement instead of deleting them with the old version.
|
|
69
|
+
*/
|
|
70
|
+
const serverOwnedFiles = ['appsettings.json', 'appsettings.development.json'];
|
|
71
|
+
/**
|
|
72
|
+
* Package files a staged artifact must provide before it may replace a deployed
|
|
73
|
+
* module. `create-env` checks the same ones while it assembles an environment, so
|
|
74
|
+
* an update cannot install something a fresh environment would have been refused.
|
|
75
|
+
*/
|
|
76
|
+
const requiredPackageFiles = {
|
|
77
|
+
server: ['Wyn.Identity.Service.dll'],
|
|
78
|
+
monitoring: ['Wyn.Configuration.dll', 'appsettings.json'],
|
|
79
|
+
};
|
|
80
|
+
/**
|
|
81
|
+
* Package entries an update must not copy over a deployed directory.
|
|
82
|
+
*
|
|
83
|
+
* Besides the environment-owned Server settings, monitoring's own `conf`
|
|
84
|
+
* directory is excluded: its assets are merged into the canonical `conf/` and the
|
|
85
|
+
* directory itself has to become a junction to `conf/` again, not a second copy
|
|
86
|
+
* of the template the Monitor would then resolve configuration from.
|
|
87
|
+
*/
|
|
88
|
+
const excludedCopyNames = (mapping) => mapping.role === 'server' ? [...serverOwnedFiles] : mapping.role === 'monitor' ? ['conf'] : [];
|
|
89
|
+
/**
|
|
90
|
+
* The modules a selection may name, from the same table `create-env` and the web
|
|
91
|
+
* interface read.
|
|
92
|
+
*
|
|
93
|
+
* Reading it through the call keeps this module loadable on its own: the policy
|
|
94
|
+
* is only needed while an update records a module it installs for the first time.
|
|
95
|
+
*/
|
|
96
|
+
const selectableModules = () => new Set([...new Set(Object.values(plan_1.optionalModulesByEdition).flat())]);
|
|
59
97
|
const installationDirectory = (root, destination) => path.join(root, ...destination.split('/'));
|
|
60
98
|
function validateUpdatableModule(module) {
|
|
61
|
-
if (
|
|
99
|
+
if (Object.prototype.hasOwnProperty.call(catalog_1.catalog, module))
|
|
62
100
|
return;
|
|
63
|
-
|
|
64
|
-
throw new Error('Module server cannot be updated: it defines the environment runtime and configuration schema. Recreate the environment with create-env instead.');
|
|
65
|
-
if (module === 'monitoring')
|
|
66
|
-
throw new Error('Module monitoring cannot be updated: it defines the configuration schema and monitor runtime. Recreate the environment with create-env instead.');
|
|
67
|
-
if (module === 'collaboration')
|
|
68
|
-
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.');
|
|
69
|
-
throw new Error(`Unsupported module "${module}": updateable modules are dashboard, reporting, sheets, documents, analysis and common.`);
|
|
101
|
+
throw new Error(`Unsupported module "${module}": supported modules are ${Object.keys(catalog_1.catalog).sort().join(', ')}.`);
|
|
70
102
|
}
|
|
71
103
|
/** `all` is the only allowed pseudo-module: every configured module from remote. */
|
|
72
104
|
const allModules = 'all';
|
|
@@ -213,6 +245,12 @@ function stringList(raw) {
|
|
|
213
245
|
throw new Error('Invalid environment configuration.');
|
|
214
246
|
return list;
|
|
215
247
|
}
|
|
248
|
+
/**
|
|
249
|
+
* One recorded module list of the manifest, or undefined when the environment
|
|
250
|
+
* records none. A field of the wrong shape is treated as absent rather than as an
|
|
251
|
+
* error: the update only ever adds to these lists.
|
|
252
|
+
*/
|
|
253
|
+
const stringListOrUndefined = (raw) => Array.isArray(raw) && raw.every(item => typeof item === 'string') ? [...raw] : undefined;
|
|
216
254
|
/** Replace or append one `<sys:string>` path list, matching by destination or assembly name. */
|
|
217
255
|
function rewritePathList(values, entries, platform) {
|
|
218
256
|
const strings = values.map(value => value.trim());
|
|
@@ -246,7 +284,10 @@ function rewritePathList(values, entries, platform) {
|
|
|
246
284
|
* is returned. Dashboard stays the first `Plugins` entry; every other entry keeps
|
|
247
285
|
* its order. A worker service keeps all of its other fields, including
|
|
248
286
|
* `Disabled`; a configuration without that node is updated through
|
|
249
|
-
* `<PluginWorkers>` alone.
|
|
287
|
+
* `<PluginWorkers>` alone. The Server service is kept in step the same way a
|
|
288
|
+
* worker is, and a native service the environment does not declare yet gets the
|
|
289
|
+
* empty node `create-env` writes, so a freshly installed collaboration service is
|
|
290
|
+
* actually loaded.
|
|
250
291
|
*/
|
|
251
292
|
function patchEnvironmentConfiguration(xml, entries, platform = process.platform) {
|
|
252
293
|
if (typeof xml !== 'string' || /<!DOCTYPE|<!ENTITY/i.test(xml) || fast_xml_parser_1.XMLValidator.validate(xml) !== true)
|
|
@@ -255,7 +296,7 @@ function patchEnvironmentConfiguration(xml, entries, platform = process.platform
|
|
|
255
296
|
const server = document?.SystemConfig?.Services?.Server;
|
|
256
297
|
if (!server || typeof server !== 'object' || Array.isArray(server))
|
|
257
298
|
throw new Error('Environment configuration has no Server service.');
|
|
258
|
-
const pluginEntries = entries.filter(entry => entry.role
|
|
299
|
+
const pluginEntries = entries.filter(entry => entry.role === undefined || entry.role === 'plugin');
|
|
259
300
|
if (server.Plugins !== undefined || pluginEntries.length) {
|
|
260
301
|
const plugins = server.Plugins === undefined || server.Plugins === '' ? {} : server.Plugins;
|
|
261
302
|
if (typeof plugins !== 'object' || Array.isArray(plugins))
|
|
@@ -278,18 +319,29 @@ function patchEnvironmentConfiguration(xml, entries, platform = process.platform
|
|
|
278
319
|
throw new Error('Invalid environment configuration.');
|
|
279
320
|
pluginWorkers['sys:string'] = rewritePathList(stringList(pluginWorkers['sys:string']), workers, platform);
|
|
280
321
|
server.PluginWorkers = pluginWorkers;
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
322
|
+
}
|
|
323
|
+
// Every entry a service starts from an assembly: the embedded workers and the
|
|
324
|
+
// Server itself. Each service keeps all of its other fields, including
|
|
325
|
+
// `Disabled` and the Server's `Urls` and storage settings.
|
|
326
|
+
const hosted = entries.filter(entry => (entry.role === 'worker' || entry.role === 'server') && entry.service);
|
|
327
|
+
// A native out-of-process service carries no assembly and is declared empty,
|
|
328
|
+
// exactly like the product's own `conf/Wyn.conf`: the Server loads it itself.
|
|
329
|
+
const declared = entries.filter(entry => entry.role === 'service' && entry.service);
|
|
330
|
+
const services = document?.SystemConfig?.Services;
|
|
331
|
+
if ((hosted.length || declared.length) && services && typeof services === 'object' && !Array.isArray(services)) {
|
|
332
|
+
const nodes = services;
|
|
333
|
+
for (const entry of declared)
|
|
334
|
+
if (nodes[entry.service] === undefined)
|
|
335
|
+
nodes[entry.service] = {};
|
|
336
|
+
for (const entry of hosted) {
|
|
337
|
+
const node = nodes[entry.service];
|
|
338
|
+
if (!node || typeof node !== 'object' || Array.isArray(node))
|
|
287
339
|
continue;
|
|
288
|
-
const
|
|
289
|
-
// The
|
|
340
|
+
const fields = node;
|
|
341
|
+
// The service is loaded from the path list above and uses its own directory
|
|
290
342
|
// as its base directory: keep the service in step without touching Disabled.
|
|
291
|
-
|
|
292
|
-
|
|
343
|
+
fields.WorkingDirectory = windowsPath((0, local_path_1.directoryOf)(entry.path, platform), platform);
|
|
344
|
+
fields.Assembly = (0, local_path_1.baseNameOf)(entry.path);
|
|
293
345
|
}
|
|
294
346
|
}
|
|
295
347
|
const output = new fast_xml_parser_1.XMLBuilder(builderOptions).build(document);
|
|
@@ -330,6 +382,10 @@ async function readManifest(root) {
|
|
|
330
382
|
}
|
|
331
383
|
if (manifest.selectedModules !== undefined && (!Array.isArray(manifest.selectedModules) || manifest.selectedModules.some(module => typeof module !== 'string')))
|
|
332
384
|
throw new Error('Environment manifest is unreadable.');
|
|
385
|
+
// Only the entry name is required: the resource list is written by this package
|
|
386
|
+
// for the package's own trees, and unknown extra fields are preserved as-is.
|
|
387
|
+
if (manifest.resources !== undefined && (!Array.isArray(manifest.resources) || manifest.resources.some(resource => !resource || typeof resource !== 'object' || Array.isArray(resource) || typeof resource.name !== 'string')))
|
|
388
|
+
throw new Error('Environment manifest is unreadable.');
|
|
333
389
|
if (manifest.moduleSources !== undefined && (!manifest.moduleSources || typeof manifest.moduleSources !== 'object' || Array.isArray(manifest.moduleSources)))
|
|
334
390
|
throw new Error('Environment manifest is unreadable.');
|
|
335
391
|
for (const source of Object.values(manifest.moduleSources ?? {})) {
|
|
@@ -417,6 +473,8 @@ async function stageRemoteArtifact(client, artifact, workspace, onProgress) {
|
|
|
417
473
|
const source = path.join(expanded, mapping.source);
|
|
418
474
|
if (!(await fs.lstat(source)).isDirectory())
|
|
419
475
|
throw new Error('Required source directory missing.');
|
|
476
|
+
if (mapping.role === 'service' && mapping.assemblies.length === 0)
|
|
477
|
+
continue;
|
|
420
478
|
const present = [];
|
|
421
479
|
for (const name of mapping.assemblies) {
|
|
422
480
|
try {
|
|
@@ -431,6 +489,12 @@ async function stageRemoteArtifact(client, artifact, workspace, onProgress) {
|
|
|
431
489
|
if (present.length !== 1)
|
|
432
490
|
throw new Error(`Expected one runtime assembly in ${mapping.destination || 'root'}.`);
|
|
433
491
|
assemblies.set(mapping.destination, present[0]);
|
|
492
|
+
// A plugin is loaded into the Server's own runtime; every other managed
|
|
493
|
+
// entry point is started by the Monitor and needs its own runtimeconfig for
|
|
494
|
+
// the .NET host to resolve the framework at all — exactly what create-env
|
|
495
|
+
// requires before it deploys one.
|
|
496
|
+
if (mapping.role !== 'plugin')
|
|
497
|
+
await (0, builder_1.regularFile)(path.join(source, present[0].replace(/\.dll$/, '.runtimeconfig.json')));
|
|
434
498
|
}
|
|
435
499
|
onProgress?.(`${label}: Archive and layout checks complete`);
|
|
436
500
|
return { ...artifact, sha256, expanded, assemblies };
|
|
@@ -439,6 +503,96 @@ async function stageRemoteArtifact(client, artifact, workspace, onProgress) {
|
|
|
439
503
|
throw new Error(`Module update failed for ${artifact.plugin}/${artifact.version}/${artifact.name} (download, archive or layout): ${error.message}`);
|
|
440
504
|
}
|
|
441
505
|
}
|
|
506
|
+
/**
|
|
507
|
+
* The configuration schema this environment already runs.
|
|
508
|
+
*
|
|
509
|
+
* `create-env` recorded the schema it detected from the Monitor template in the
|
|
510
|
+
* manifest; an environment whose manifest predates the field falls back to the
|
|
511
|
+
* namespace its live `conf/Wyn.conf` declares. A Server or Monitor update needs
|
|
512
|
+
* this to check that the new package ships the matching XSD assets.
|
|
513
|
+
*/
|
|
514
|
+
function environmentSchema(manifest, configuration) {
|
|
515
|
+
if (manifest.schema === 'V91' || manifest.schema === 'V100')
|
|
516
|
+
return manifest.schema;
|
|
517
|
+
try {
|
|
518
|
+
return (0, config_1.detectConfigurationSchema)(configuration);
|
|
519
|
+
}
|
|
520
|
+
catch {
|
|
521
|
+
return undefined;
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
/**
|
|
525
|
+
* Check a staged Server or Monitor package against the deployed environment.
|
|
526
|
+
*
|
|
527
|
+
* `create-env` verifies the same things while it assembles: the Server's
|
|
528
|
+
* identity service, the Monitor's configuration assembly and settings, and the
|
|
529
|
+
* `xsd/<schema>/Wyn.conf.xsd` both packages ship for the configuration schema the
|
|
530
|
+
* environment declares. A Server package may legitimately omit its XSD — Wyn's
|
|
531
|
+
* configuration reader falls back to ordinary XML deserialization — which
|
|
532
|
+
* `create-env` records as a warning rather than rejecting, so the update keeps
|
|
533
|
+
* that policy. Returns the warning text, or undefined when the package is
|
|
534
|
+
* complete.
|
|
535
|
+
*/
|
|
536
|
+
async function verifyStagedPackage(module, staged, schema) {
|
|
537
|
+
const label = `${staged.plugin}/${staged.version}/${staged.name}`;
|
|
538
|
+
for (const name of requiredPackageFiles[module] ?? []) {
|
|
539
|
+
try {
|
|
540
|
+
await (0, builder_1.regularFile)(path.join(staged.expanded, name));
|
|
541
|
+
}
|
|
542
|
+
catch {
|
|
543
|
+
throw new Error(`Module update failed for ${label}: the package is missing ${name}.`);
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
const relative = `xsd/${schema}/Wyn.conf.xsd`;
|
|
547
|
+
try {
|
|
548
|
+
await (0, builder_1.regularFile)(path.join(staged.expanded, relative));
|
|
549
|
+
return undefined;
|
|
550
|
+
}
|
|
551
|
+
catch (error) {
|
|
552
|
+
if (module === 'server' && error.code === 'ENOENT') {
|
|
553
|
+
return `Server package ${label} is missing ${relative}; Server will skip XSD validation. XML reader compatibility and runtime startup have not been verified.`;
|
|
554
|
+
}
|
|
555
|
+
throw new Error(`Configuration schema validation failed for ${label}: ${module === 'server' ? 'Server' : 'Monitor'} package is missing ${relative}.`);
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
/** Copy a tree over an existing one, replacing files instead of refusing a collision. */
|
|
559
|
+
async function overlayTree(source, destination) {
|
|
560
|
+
await fs.mkdir(destination, { recursive: true });
|
|
561
|
+
for (const entry of await fs.readdir(source, { withFileTypes: true })) {
|
|
562
|
+
const from = path.join(source, entry.name);
|
|
563
|
+
const to = path.join(destination, entry.name);
|
|
564
|
+
if (entry.isDirectory())
|
|
565
|
+
await overlayTree(from, to);
|
|
566
|
+
else if (entry.isFile())
|
|
567
|
+
await fs.copyFile(from, to);
|
|
568
|
+
else
|
|
569
|
+
throw new Error('Unexpected link in extracted archive.');
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
/**
|
|
573
|
+
* Merge a monitoring package's `conf` assets into the canonical configuration
|
|
574
|
+
* directory.
|
|
575
|
+
*
|
|
576
|
+
* `create-env` moves everything but the `Wyn.conf` template out of the Monitor
|
|
577
|
+
* package and leaves the environment's own `conf/Wyn.conf` as the single editable
|
|
578
|
+
* configuration, so the update refreshes the package's assets in place and never
|
|
579
|
+
* touches that file. An asset the new package no longer ships stays where it is:
|
|
580
|
+
* the directory also holds whatever the environment itself added.
|
|
581
|
+
*/
|
|
582
|
+
async function mergeConfigurationAssets(source, destination) {
|
|
583
|
+
for (const entry of await fs.readdir(source, { withFileTypes: true })) {
|
|
584
|
+
if (entry.name.toLowerCase() === 'wyn.conf')
|
|
585
|
+
continue;
|
|
586
|
+
const from = path.join(source, entry.name);
|
|
587
|
+
const to = path.join(destination, entry.name);
|
|
588
|
+
if (entry.isDirectory())
|
|
589
|
+
await overlayTree(from, to);
|
|
590
|
+
else if (entry.isFile())
|
|
591
|
+
await fs.copyFile(from, to);
|
|
592
|
+
else
|
|
593
|
+
throw new Error('Unexpected link in extracted archive.');
|
|
594
|
+
}
|
|
595
|
+
}
|
|
442
596
|
/** Mappings of one module that a local source may override. */
|
|
443
597
|
const localMappings = (module) => catalog_1.catalog[module].mappings.filter(mapping => mapping.role === 'plugin' || mapping.role === 'worker');
|
|
444
598
|
const expectedAssemblies = (module) => [...new Set(localMappings(module).flatMap(mapping => mapping.assemblies))];
|
|
@@ -463,6 +617,11 @@ function mappingForAssembly(module, assembly) {
|
|
|
463
617
|
*/
|
|
464
618
|
async function resolveLocalSource(entry, module, assembly, input) {
|
|
465
619
|
const mappings = localMappings(module);
|
|
620
|
+
// The Server, Monitor and collaboration deployments are whole services, not a
|
|
621
|
+
// plugin the Server loads: a developer build has no place to point the
|
|
622
|
+
// configuration at, so they are refreshed from their artifact only.
|
|
623
|
+
if (!mappings.length)
|
|
624
|
+
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.`);
|
|
466
625
|
const source = path.resolve(input);
|
|
467
626
|
const stat = await lstatOrUndefined(source);
|
|
468
627
|
if (!stat)
|
|
@@ -546,12 +705,14 @@ class EnvironmentUpdater {
|
|
|
546
705
|
registry;
|
|
547
706
|
processes;
|
|
548
707
|
platform;
|
|
708
|
+
bundledResources;
|
|
549
709
|
onProgress;
|
|
550
710
|
constructor(dependencies = {}) {
|
|
551
711
|
this.dependencies = dependencies;
|
|
552
712
|
this.registry = dependencies.registry ?? new registry_1.EnvironmentRegistry();
|
|
553
713
|
this.processes = dependencies.processes ?? (process.platform === 'win32' ? new process_1.WindowsProcessBackend() : new posix_process_1.PosixProcessBackend());
|
|
554
714
|
this.platform = dependencies.platform ?? process.platform;
|
|
715
|
+
this.bundledResources = dependencies.bundledResourcesDirectory ?? (0, package_resource_1.bundledResourcesDirectory)();
|
|
555
716
|
this.onProgress = dependencies.onProgress;
|
|
556
717
|
}
|
|
557
718
|
async update(name, specs, clientFactory, options = {}) {
|
|
@@ -559,8 +720,9 @@ class EnvironmentUpdater {
|
|
|
559
720
|
const onProgress = options.onProgress ?? this.onProgress;
|
|
560
721
|
onProgress?.('Validating module specs');
|
|
561
722
|
let parsed = normalizeModuleSpecs(specs);
|
|
562
|
-
|
|
563
|
-
|
|
723
|
+
// No module spec is a valid request: it (re)lays out the bundled resources
|
|
724
|
+
// alone, which is how an environment created before they were bundled gets
|
|
725
|
+
// them without touching a single module.
|
|
564
726
|
const entry = await this.registry.get(name);
|
|
565
727
|
onProgress?.(`Validating environment ${entry.name} at ${entry.root}`);
|
|
566
728
|
await (0, validate_1.validateEnvironmentRoot)(entry, this.registry.directory);
|
|
@@ -580,7 +742,7 @@ class EnvironmentUpdater {
|
|
|
580
742
|
skipped.push({ module, reason: 'local' });
|
|
581
743
|
const selected = configured.filter(module => !localModules.has(module));
|
|
582
744
|
if (!selected.length)
|
|
583
|
-
throw new Error(`Module spec all matched no updatable modules: ${configured.length ? configured.map(module => `${module} has a local source`).join('; ') : '
|
|
745
|
+
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'}.`);
|
|
584
746
|
parsed = selected.map(module => ({ module }));
|
|
585
747
|
onProgress?.(`Updating all configured modules from remote: ${selected.join(', ')}${skipped.length ? ` (skipping local: ${skipped.map(item => item.module).join(', ')})` : ''}`);
|
|
586
748
|
}
|
|
@@ -617,10 +779,39 @@ class EnvironmentUpdater {
|
|
|
617
779
|
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.`);
|
|
618
780
|
if (!remote.length && state !== 'ready' && state !== 'stopped' && state !== 'running')
|
|
619
781
|
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.`);
|
|
782
|
+
// The packaged 3D scene assets belong to every environment, not only to one
|
|
783
|
+
// that create-env assembled: the plan below finds the files an environment is
|
|
784
|
+
// still missing (or that differ) and the apply writes them with the rest of
|
|
785
|
+
// the update. Planning touches nothing and runs before any download or write,
|
|
786
|
+
// so an incomplete installation fails loudly with the path it expected and
|
|
787
|
+
// leaves the environment exactly as it was.
|
|
788
|
+
//
|
|
789
|
+
// The assets live at `Server/wwwroot/3DScene`, so refreshing the server
|
|
790
|
+
// module removes the whole tree they are in. Plan every packaged file as
|
|
791
|
+
// missing in that case: comparing against the content that is about to be
|
|
792
|
+
// deleted would report every file identical and lay nothing out at all.
|
|
793
|
+
const refreshesServer = remote.some(spec => spec.module === 'server');
|
|
794
|
+
const resourceSource = (0, package_resource_1.bundledResourceDirectory)(package_resource_1.sceneResourceName, this.bundledResources);
|
|
795
|
+
const resourceDestination = path.join(entry.root, ...package_resource_1.sceneResourceDestination.split('/'));
|
|
796
|
+
onProgress?.(`Checking bundled 3D scene resources in ${package_resource_1.sceneResourceDestination}`);
|
|
797
|
+
const resourcePlan = await (0, package_resource_1.planBundledResource)(resourceSource, resourceDestination, refreshesServer);
|
|
798
|
+
const resourceWrites = resourcePlan.added + resourcePlan.updated;
|
|
799
|
+
const resource = { name: package_resource_1.sceneResourceName, source: resourceSource, destination: package_resource_1.sceneResourceDestination, files: resourcePlan.files.length, added: resourcePlan.added, updated: resourcePlan.updated };
|
|
800
|
+
const resourceSummary = `${resourcePlan.files.length - resourceWrites} identical, ${resourcePlan.added} missing, ${resourcePlan.updated} different`;
|
|
801
|
+
onProgress?.(`Bundled 3D scene resources in ${package_resource_1.sceneResourceDestination}: ${dryRun ? 'dry run; ' : ''}${resourceSummary}`);
|
|
620
802
|
const downloads = [...(manifest.downloads ?? [])];
|
|
621
|
-
const selectedModules = manifest.selectedModules
|
|
803
|
+
const selectedModules = stringListOrUndefined(manifest.selectedModules);
|
|
804
|
+
const autoAddedModules = stringListOrUndefined(manifest.autoAddedModules);
|
|
622
805
|
const moduleSources = { ...(manifest.moduleSources ?? {}) };
|
|
623
|
-
|
|
806
|
+
// What a fresh environment would have selected for itself. An update records
|
|
807
|
+
// a first install the way `create-env` does: a selectable module joins the
|
|
808
|
+
// selection, a core component or an automatic dependency is added instead.
|
|
809
|
+
const selectable = selectableModules();
|
|
810
|
+
// The manifest records the .NET requirements read from each artifact's
|
|
811
|
+
// runtimeconfig files, so an update replaces the entries of the modules it
|
|
812
|
+
// refreshes and leaves every other module's alone.
|
|
813
|
+
let requirements = Array.isArray(manifest.requirements) ? manifest.requirements.filter((line) => typeof line === 'string') : [];
|
|
814
|
+
const workspace = !dryRun && (remote.length > 0 || resourceWrites > 0) ? await fs.mkdtemp(path.join(path.dirname(entry.root), '.afm-env-')) : undefined;
|
|
624
815
|
const warnings = [];
|
|
625
816
|
try {
|
|
626
817
|
const resolved = new Map();
|
|
@@ -636,6 +827,23 @@ class EnvironmentUpdater {
|
|
|
636
827
|
staged.set(spec.module, await stageRemoteArtifact(client, resolved.get(spec.module), workspace, onProgress));
|
|
637
828
|
}
|
|
638
829
|
const configured = await fs.readFile(path.join(entry.root, 'conf', 'Wyn.conf'), 'utf8');
|
|
830
|
+
// A Server or Monitor package is only usable when it still serves the
|
|
831
|
+
// configuration schema this environment runs, so both are checked against
|
|
832
|
+
// the deployed schema before anything is planned around them.
|
|
833
|
+
const schema = remote.some(spec => spec.module === 'server' || spec.module === 'monitoring')
|
|
834
|
+
? environmentSchema(manifest, configured)
|
|
835
|
+
: undefined;
|
|
836
|
+
if (remote.some(spec => spec.module === 'server' || spec.module === 'monitoring') && !schema) {
|
|
837
|
+
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.`);
|
|
838
|
+
}
|
|
839
|
+
for (const spec of remote) {
|
|
840
|
+
const stagedArtifact = staged.get(spec.module);
|
|
841
|
+
if (!stagedArtifact || !schema || !(spec.module in requiredPackageFiles))
|
|
842
|
+
continue;
|
|
843
|
+
const warning = await verifyStagedPackage(spec.module, stagedArtifact, schema);
|
|
844
|
+
if (warning)
|
|
845
|
+
warnings.push(warning);
|
|
846
|
+
}
|
|
639
847
|
const plans = [];
|
|
640
848
|
// A module may carry several local specs: plan it once, for all its targets.
|
|
641
849
|
for (const module of [...new Set(parsed.map(spec => spec.module))]) {
|
|
@@ -663,7 +871,7 @@ class EnvironmentUpdater {
|
|
|
663
871
|
});
|
|
664
872
|
plans.push({
|
|
665
873
|
module, previous, next: { local: override.map(target => target.path).join(', ') }, local: override,
|
|
666
|
-
entries, copies: [], removals, copiedDirectories: [], removedDirectories,
|
|
874
|
+
entries, copies: [], removals, preserved: [], copiedDirectories: [], removedDirectories,
|
|
667
875
|
});
|
|
668
876
|
continue;
|
|
669
877
|
}
|
|
@@ -671,30 +879,59 @@ class EnvironmentUpdater {
|
|
|
671
879
|
const stagedArtifact = staged.get(module);
|
|
672
880
|
const copies = [];
|
|
673
881
|
const removals = [];
|
|
882
|
+
const preserved = [];
|
|
674
883
|
const copiedDirectories = [];
|
|
675
884
|
const removedDirectories = [];
|
|
676
885
|
const entries = [];
|
|
886
|
+
let configurationAssets;
|
|
887
|
+
let configurationLink;
|
|
677
888
|
for (const mapping of remoteArtifact.mappings) {
|
|
678
889
|
const destination = installationDirectory(entry.root, mapping.destination);
|
|
679
890
|
if (await exists(destination)) {
|
|
680
891
|
removals.push(destination);
|
|
681
892
|
removedDirectories.push(mapping.destination);
|
|
682
893
|
}
|
|
894
|
+
// The Server settings the environment owns are never taken from the
|
|
895
|
+
// archive and must survive the replacement, so carry each one that is
|
|
896
|
+
// actually deployed across the directory swap.
|
|
897
|
+
if (stagedArtifact && mapping.role === 'server' && workspace) {
|
|
898
|
+
for (const name of excludedCopyNames(mapping)) {
|
|
899
|
+
if (await exists(path.join(destination, name)))
|
|
900
|
+
preserved.push({ directory: destination, name, source: path.join(workspace, 'preserved', module, name) });
|
|
901
|
+
}
|
|
902
|
+
}
|
|
683
903
|
if (stagedArtifact)
|
|
684
|
-
copies.push({ source: path.join(stagedArtifact.expanded, mapping.source), destination });
|
|
904
|
+
copies.push({ source: path.join(stagedArtifact.expanded, mapping.source), destination, exclude: excludedCopyNames(mapping) });
|
|
685
905
|
copiedDirectories.push(mapping.destination);
|
|
906
|
+
// A native out-of-process service has no assembly and is declared as an
|
|
907
|
+
// empty node: the Server loads it from that entry and supplies the rest.
|
|
908
|
+
if (mapping.role === 'service') {
|
|
909
|
+
if (mapping.service)
|
|
910
|
+
entries.push({ destination: mapping.destination, assemblies: [], path: mapping.destination, role: 'service', service: mapping.service });
|
|
911
|
+
continue;
|
|
912
|
+
}
|
|
686
913
|
const assembly = stagedArtifact?.assemblies.get(mapping.destination) ?? knownAssembly(configured, mapping, this.platform);
|
|
687
914
|
// Plugins stay relative to the environment root; a worker entry does too,
|
|
688
915
|
// and its service keeps using the deployed destination as working directory.
|
|
689
|
-
|
|
916
|
+
// The Server service the Monitor starts is kept in step the same way.
|
|
917
|
+
if (mapping.role === 'plugin' || mapping.role === 'worker' || mapping.role === 'server') {
|
|
690
918
|
const entry = { destination: mapping.destination, assemblies: mapping.assemblies, path: path.posix.join(mapping.destination, assembly) };
|
|
691
|
-
if (mapping.role === 'worker') {
|
|
692
|
-
entry.role =
|
|
919
|
+
if (mapping.role === 'worker' || mapping.role === 'server') {
|
|
920
|
+
entry.role = mapping.role;
|
|
693
921
|
if (mapping.service)
|
|
694
922
|
entry.service = mapping.service;
|
|
695
923
|
}
|
|
696
924
|
entries.push(entry);
|
|
697
925
|
}
|
|
926
|
+
// The Monitor resolves its configuration from its own working directory,
|
|
927
|
+
// so its package assets belong in the canonical `conf` and the directory
|
|
928
|
+
// itself has to point back at it once the Monitor tree is replaced.
|
|
929
|
+
if (mapping.role === 'monitor' && stagedArtifact) {
|
|
930
|
+
const assets = path.join(stagedArtifact.expanded, 'conf');
|
|
931
|
+
if ((await fs.lstat(assets).catch(() => undefined))?.isDirectory())
|
|
932
|
+
configurationAssets = { source: assets, destination: path.join(entry.root, 'conf') };
|
|
933
|
+
configurationLink = path.join(destination, 'conf');
|
|
934
|
+
}
|
|
698
935
|
}
|
|
699
936
|
if (module === 'dashboard' && stagedArtifact) {
|
|
700
937
|
const contents = path.join(stagedArtifact.expanded, 'Web Contents');
|
|
@@ -704,13 +941,14 @@ class EnvironmentUpdater {
|
|
|
704
941
|
removals.push(destination);
|
|
705
942
|
removedDirectories.push('Web Contents');
|
|
706
943
|
}
|
|
707
|
-
copies.push({ source: contents, destination });
|
|
944
|
+
copies.push({ source: contents, destination, exclude: [] });
|
|
708
945
|
copiedDirectories.push('Web Contents');
|
|
709
946
|
}
|
|
710
947
|
}
|
|
711
948
|
plans.push({
|
|
712
949
|
module, previous, next: { version: remoteArtifact.version }, artifact: remoteArtifact, staged: stagedArtifact,
|
|
713
|
-
entries, copies, removals,
|
|
950
|
+
entries, copies, removals, preserved, ...(configurationAssets ? { configurationAssets } : {}), ...(configurationLink ? { configurationLink } : {}),
|
|
951
|
+
copiedDirectories, removedDirectories,
|
|
714
952
|
});
|
|
715
953
|
}
|
|
716
954
|
const changes = plans.map(plan => ({ module: plan.module, previous: plan.previous, next: plan.next, copiedDirectories: plan.copiedDirectories, removedDirectories: plan.removedDirectories }));
|
|
@@ -735,41 +973,123 @@ class EnvironmentUpdater {
|
|
|
735
973
|
nextDownloads.push(download);
|
|
736
974
|
else
|
|
737
975
|
nextDownloads[downloadIndex] = download;
|
|
976
|
+
// The requirements the old artifact needed are gone with it: re-read
|
|
977
|
+
// the ones the new package actually declares, per deployed mapping.
|
|
978
|
+
requirements = requirements.filter(line => !line.endsWith(` (${plan.module})`));
|
|
979
|
+
for (const mapping of plan.artifact.mappings) {
|
|
980
|
+
if (mapping.role === 'service')
|
|
981
|
+
continue;
|
|
982
|
+
for (const framework of await (0, builder_1.runtimeRequirements)(path.join(plan.staged.expanded, mapping.source))) {
|
|
983
|
+
requirements.push(`${framework.name} ${framework.version} (${plan.module})`);
|
|
984
|
+
}
|
|
985
|
+
}
|
|
738
986
|
}
|
|
739
987
|
delete moduleSources[plan.module];
|
|
740
988
|
}
|
|
741
989
|
if (plan.local)
|
|
742
990
|
moduleSources[plan.module] = manifestSource(plan.module, plan.local, moduleSources[plan.module]);
|
|
743
|
-
|
|
744
|
-
|
|
991
|
+
// First installing a module records it the way `create-env` would: a
|
|
992
|
+
// selectable one joins the selection, a core component or an automatic
|
|
993
|
+
// dependency joins the automatically added list instead.
|
|
994
|
+
if (!installed.has(plan.module)) {
|
|
995
|
+
if (selectable.has(plan.module)) {
|
|
996
|
+
if (selectedModules)
|
|
997
|
+
selectedModules.push(plan.module);
|
|
998
|
+
}
|
|
999
|
+
else if (autoAddedModules)
|
|
1000
|
+
autoAddedModules.push(plan.module);
|
|
1001
|
+
}
|
|
745
1002
|
}
|
|
746
1003
|
if (selectedModules) {
|
|
747
1004
|
const unique = [...new Set(selectedModules)].sort();
|
|
748
1005
|
selectedModules.length = 0;
|
|
749
1006
|
selectedModules.push(...unique);
|
|
750
1007
|
}
|
|
751
|
-
|
|
1008
|
+
if (autoAddedModules) {
|
|
1009
|
+
const unique = [...new Set(autoAddedModules)].sort();
|
|
1010
|
+
autoAddedModules.length = 0;
|
|
1011
|
+
autoAddedModules.push(...unique);
|
|
1012
|
+
}
|
|
1013
|
+
const nextManifest = { ...manifest, artifacts: nextArtifacts, downloads: nextDownloads, requirements: [...new Set(requirements)] };
|
|
1014
|
+
if (autoAddedModules)
|
|
1015
|
+
nextManifest.autoAddedModules = autoAddedModules;
|
|
752
1016
|
if (Object.keys(moduleSources).length)
|
|
753
1017
|
nextManifest.moduleSources = moduleSources;
|
|
754
1018
|
else
|
|
755
1019
|
delete nextManifest.moduleSources;
|
|
756
1020
|
if (selectedModules)
|
|
757
1021
|
nextManifest.selectedModules = selectedModules;
|
|
758
|
-
|
|
1022
|
+
// The resource is recorded in place, in exactly the shape create-env writes:
|
|
1023
|
+
// a second update finds the entry and refreshes it instead of adding a
|
|
1024
|
+
// duplicate. The result carries the added/updated counts; the manifest keeps
|
|
1025
|
+
// the durable four-field description.
|
|
1026
|
+
const resources = [...(manifest.resources ?? [])];
|
|
1027
|
+
const recorded = resources.findIndex(candidate => candidate.name === package_resource_1.sceneResourceName);
|
|
1028
|
+
const record = { name: package_resource_1.sceneResourceName, source: resource.source, destination: resource.destination, files: resource.files };
|
|
1029
|
+
if (recorded < 0)
|
|
1030
|
+
resources.push(record);
|
|
1031
|
+
else
|
|
1032
|
+
resources[recorded] = { ...resources[recorded], ...record };
|
|
1033
|
+
nextManifest.resources = resources;
|
|
1034
|
+
const modules = [...new Set([...entry.modules, ...plans.filter(plan => !installed.has(plan.module) && selectable.has(plan.module)).map(plan => plan.module)])].sort();
|
|
759
1035
|
const localModules = Object.keys(moduleSources).sort();
|
|
760
1036
|
if (state === 'running')
|
|
761
1037
|
warnings.push(`${entry.name} is running; restart it to load the updated modules.`);
|
|
762
1038
|
for (const item of skipped)
|
|
763
1039
|
warnings.push(`${item.module} has a local source and was not updated from artifacts.`);
|
|
1040
|
+
// The Server and Monitor packages each ship the XSD assets for a
|
|
1041
|
+
// configuration schema, so replacing one of them alone leaves the
|
|
1042
|
+
// environment mixing two package generations for the same configuration.
|
|
1043
|
+
const runtime = plans.filter(plan => plan.module === 'server' || plan.module === 'monitoring').map(plan => plan.module).sort();
|
|
1044
|
+
if (runtime.length === 1)
|
|
1045
|
+
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.`);
|
|
1046
|
+
if (parsed.some(spec => spec.module === 'documents') && !parsed.some(spec => spec.module === 'collaboration')) {
|
|
1047
|
+
warnings.push('documents was refreshed without collaboration; the smart documents plugin may expect the deployed collaboration service version, so refresh collaboration too.');
|
|
1048
|
+
}
|
|
764
1049
|
if (!dryRun) {
|
|
765
1050
|
for (const plan of plans) {
|
|
1051
|
+
// Take the environment-owned settings aside first, and drop the Monitor
|
|
1052
|
+
// configuration junction before the directory holding it: a recursive
|
|
1053
|
+
// delete must never reach the canonical `conf` the junction points at.
|
|
1054
|
+
for (const file of plan.preserved) {
|
|
1055
|
+
await fs.mkdir(path.dirname(file.source), { recursive: true });
|
|
1056
|
+
await fs.copyFile(path.join(file.directory, file.name), file.source);
|
|
1057
|
+
}
|
|
1058
|
+
if (plan.configurationLink) {
|
|
1059
|
+
const existing = await fs.lstat(plan.configurationLink).catch(() => undefined);
|
|
1060
|
+
if (existing?.isSymbolicLink())
|
|
1061
|
+
await fs.unlink(plan.configurationLink);
|
|
1062
|
+
else
|
|
1063
|
+
await fs.rm(plan.configurationLink, { recursive: true, force: true });
|
|
1064
|
+
}
|
|
766
1065
|
for (const removal of plan.removals)
|
|
767
1066
|
await fs.rm(removal, { recursive: true, force: true });
|
|
768
1067
|
for (const copy of plan.copies)
|
|
769
|
-
await (0, builder_1.copyTree)(copy.source, copy.destination);
|
|
1068
|
+
await (0, builder_1.copyTree)(copy.source, copy.destination, copy.exclude);
|
|
1069
|
+
for (const file of plan.preserved)
|
|
1070
|
+
await fs.copyFile(file.source, path.join(file.directory, file.name));
|
|
1071
|
+
if (plan.configurationAssets) {
|
|
1072
|
+
onProgress?.(`Refreshing Monitor configuration assets in ${path.relative(entry.root, plan.configurationAssets.destination)}`);
|
|
1073
|
+
await mergeConfigurationAssets(plan.configurationAssets.source, plan.configurationAssets.destination);
|
|
1074
|
+
}
|
|
1075
|
+
if (plan.configurationLink)
|
|
1076
|
+
await fs.symlink(path.join(entry.root, 'conf'), plan.configurationLink, 'junction');
|
|
1077
|
+
}
|
|
1078
|
+
// The staged resources are written with the module changes, before the
|
|
1079
|
+
// manifest that records them; a failure rolls the layout back.
|
|
1080
|
+
if (resourceWrites) {
|
|
1081
|
+
if (!workspace)
|
|
1082
|
+
throw new Error('Missing staging workspace for the bundled resource layout.');
|
|
1083
|
+
onProgress?.(`Laying out bundled 3D scene resources into ${package_resource_1.sceneResourceDestination}`);
|
|
1084
|
+
const applied = await (0, package_resource_1.applyBundledResource)(resourceSource, resourceDestination, resourcePlan, path.join(workspace, 'replaced-resources'));
|
|
1085
|
+
if (applied.added.length !== resourcePlan.added || applied.updated.length !== resourcePlan.updated)
|
|
1086
|
+
throw new Error('Bundled resource layout did not match its plan.');
|
|
770
1087
|
}
|
|
771
|
-
|
|
772
|
-
|
|
1088
|
+
// A resource-only update (no module spec) leaves the Server configuration
|
|
1089
|
+
// exactly as it is: the assets need no plugin, worker or service entry.
|
|
1090
|
+
onProgress?.(plans.length ? 'Writing configuration, manifest and registry record' : 'Writing manifest and registry record');
|
|
1091
|
+
if (plans.length)
|
|
1092
|
+
await writeAtomic(path.join(entry.root, 'conf', 'Wyn.conf'), configuration);
|
|
773
1093
|
await writeJsonAtomic(path.join(entry.root, 'environment.lock.json'), nextManifest);
|
|
774
1094
|
await this.registry.update(entries => {
|
|
775
1095
|
const index = entries.findIndex(candidate => candidate.id === entry.id);
|
|
@@ -784,10 +1104,10 @@ class EnvironmentUpdater {
|
|
|
784
1104
|
delete entries[index].localModules;
|
|
785
1105
|
});
|
|
786
1106
|
}
|
|
787
|
-
const restartRequired = !dryRun && changes.length > 0;
|
|
1107
|
+
const restartRequired = !dryRun && (changes.length > 0 || resourceWrites > 0);
|
|
788
1108
|
if (restartRequired)
|
|
789
|
-
onProgress?.(`${entry.name}: modules updated; restart the environment to load the change (no service was started).`);
|
|
790
|
-
return { name: entry.name, root: entry.root, dryRun, changes, skipped, restartRequired, warnings, localTargets: plans.flatMap(plan => plan.local ?? []) };
|
|
1109
|
+
onProgress?.(`${entry.name}: ${changes.length ? 'modules updated' : 'bundled resources laid out'}; restart the environment to load the change (no service was started).`);
|
|
1110
|
+
return { name: entry.name, root: entry.root, dryRun, changes, skipped, restartRequired, warnings, localTargets: plans.flatMap(plan => plan.local ?? []), resource };
|
|
791
1111
|
}
|
|
792
1112
|
finally {
|
|
793
1113
|
if (workspace)
|