@guanjinke/artifact-client 1.7.2 → 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 +3 -1
- package/dist/cli/setup.js +2 -2
- 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 +1 -0
- package/dist/environment/builder.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/update.d.ts +9 -4
- package/dist/environment/update.d.ts.map +1 -1
- package/dist/environment/update.js +310 -37
- package/dist/environment/update.js.map +1 -1
- package/dist/web/static/app.js +1 -1
- package/package.json +1 -1
|
@@ -42,6 +42,8 @@ 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");
|
|
@@ -55,19 +57,48 @@ const builderOptions = { ignoreAttributes: false, format: true, suppressEmptyNod
|
|
|
55
57
|
const windowsPath = (value, platform) => (0, local_path_1.nativeSeparators)(value, platform);
|
|
56
58
|
/** The separator the recorded entries use on the target platform. */
|
|
57
59
|
const separatorOf = (platform) => (platform === 'win32' ? '\\' : '/');
|
|
58
|
-
/**
|
|
59
|
-
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())]);
|
|
60
97
|
const installationDirectory = (root, destination) => path.join(root, ...destination.split('/'));
|
|
61
98
|
function validateUpdatableModule(module) {
|
|
62
|
-
if (
|
|
99
|
+
if (Object.prototype.hasOwnProperty.call(catalog_1.catalog, module))
|
|
63
100
|
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.`);
|
|
101
|
+
throw new Error(`Unsupported module "${module}": supported modules are ${Object.keys(catalog_1.catalog).sort().join(', ')}.`);
|
|
71
102
|
}
|
|
72
103
|
/** `all` is the only allowed pseudo-module: every configured module from remote. */
|
|
73
104
|
const allModules = 'all';
|
|
@@ -214,6 +245,12 @@ function stringList(raw) {
|
|
|
214
245
|
throw new Error('Invalid environment configuration.');
|
|
215
246
|
return list;
|
|
216
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;
|
|
217
254
|
/** Replace or append one `<sys:string>` path list, matching by destination or assembly name. */
|
|
218
255
|
function rewritePathList(values, entries, platform) {
|
|
219
256
|
const strings = values.map(value => value.trim());
|
|
@@ -247,7 +284,10 @@ function rewritePathList(values, entries, platform) {
|
|
|
247
284
|
* is returned. Dashboard stays the first `Plugins` entry; every other entry keeps
|
|
248
285
|
* its order. A worker service keeps all of its other fields, including
|
|
249
286
|
* `Disabled`; a configuration without that node is updated through
|
|
250
|
-
* `<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.
|
|
251
291
|
*/
|
|
252
292
|
function patchEnvironmentConfiguration(xml, entries, platform = process.platform) {
|
|
253
293
|
if (typeof xml !== 'string' || /<!DOCTYPE|<!ENTITY/i.test(xml) || fast_xml_parser_1.XMLValidator.validate(xml) !== true)
|
|
@@ -256,7 +296,7 @@ function patchEnvironmentConfiguration(xml, entries, platform = process.platform
|
|
|
256
296
|
const server = document?.SystemConfig?.Services?.Server;
|
|
257
297
|
if (!server || typeof server !== 'object' || Array.isArray(server))
|
|
258
298
|
throw new Error('Environment configuration has no Server service.');
|
|
259
|
-
const pluginEntries = entries.filter(entry => entry.role
|
|
299
|
+
const pluginEntries = entries.filter(entry => entry.role === undefined || entry.role === 'plugin');
|
|
260
300
|
if (server.Plugins !== undefined || pluginEntries.length) {
|
|
261
301
|
const plugins = server.Plugins === undefined || server.Plugins === '' ? {} : server.Plugins;
|
|
262
302
|
if (typeof plugins !== 'object' || Array.isArray(plugins))
|
|
@@ -279,18 +319,29 @@ function patchEnvironmentConfiguration(xml, entries, platform = process.platform
|
|
|
279
319
|
throw new Error('Invalid environment configuration.');
|
|
280
320
|
pluginWorkers['sys:string'] = rewritePathList(stringList(pluginWorkers['sys:string']), workers, platform);
|
|
281
321
|
server.PluginWorkers = pluginWorkers;
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
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))
|
|
288
339
|
continue;
|
|
289
|
-
const
|
|
290
|
-
// The
|
|
340
|
+
const fields = node;
|
|
341
|
+
// The service is loaded from the path list above and uses its own directory
|
|
291
342
|
// as its base directory: keep the service in step without touching Disabled.
|
|
292
|
-
|
|
293
|
-
|
|
343
|
+
fields.WorkingDirectory = windowsPath((0, local_path_1.directoryOf)(entry.path, platform), platform);
|
|
344
|
+
fields.Assembly = (0, local_path_1.baseNameOf)(entry.path);
|
|
294
345
|
}
|
|
295
346
|
}
|
|
296
347
|
const output = new fast_xml_parser_1.XMLBuilder(builderOptions).build(document);
|
|
@@ -422,6 +473,8 @@ async function stageRemoteArtifact(client, artifact, workspace, onProgress) {
|
|
|
422
473
|
const source = path.join(expanded, mapping.source);
|
|
423
474
|
if (!(await fs.lstat(source)).isDirectory())
|
|
424
475
|
throw new Error('Required source directory missing.');
|
|
476
|
+
if (mapping.role === 'service' && mapping.assemblies.length === 0)
|
|
477
|
+
continue;
|
|
425
478
|
const present = [];
|
|
426
479
|
for (const name of mapping.assemblies) {
|
|
427
480
|
try {
|
|
@@ -436,6 +489,12 @@ async function stageRemoteArtifact(client, artifact, workspace, onProgress) {
|
|
|
436
489
|
if (present.length !== 1)
|
|
437
490
|
throw new Error(`Expected one runtime assembly in ${mapping.destination || 'root'}.`);
|
|
438
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')));
|
|
439
498
|
}
|
|
440
499
|
onProgress?.(`${label}: Archive and layout checks complete`);
|
|
441
500
|
return { ...artifact, sha256, expanded, assemblies };
|
|
@@ -444,6 +503,96 @@ async function stageRemoteArtifact(client, artifact, workspace, onProgress) {
|
|
|
444
503
|
throw new Error(`Module update failed for ${artifact.plugin}/${artifact.version}/${artifact.name} (download, archive or layout): ${error.message}`);
|
|
445
504
|
}
|
|
446
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
|
+
}
|
|
447
596
|
/** Mappings of one module that a local source may override. */
|
|
448
597
|
const localMappings = (module) => catalog_1.catalog[module].mappings.filter(mapping => mapping.role === 'plugin' || mapping.role === 'worker');
|
|
449
598
|
const expectedAssemblies = (module) => [...new Set(localMappings(module).flatMap(mapping => mapping.assemblies))];
|
|
@@ -468,6 +617,11 @@ function mappingForAssembly(module, assembly) {
|
|
|
468
617
|
*/
|
|
469
618
|
async function resolveLocalSource(entry, module, assembly, input) {
|
|
470
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.`);
|
|
471
625
|
const source = path.resolve(input);
|
|
472
626
|
const stat = await lstatOrUndefined(source);
|
|
473
627
|
if (!stat)
|
|
@@ -588,7 +742,7 @@ class EnvironmentUpdater {
|
|
|
588
742
|
skipped.push({ module, reason: 'local' });
|
|
589
743
|
const selected = configured.filter(module => !localModules.has(module));
|
|
590
744
|
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('; ') : '
|
|
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'}.`);
|
|
592
746
|
parsed = selected.map(module => ({ module }));
|
|
593
747
|
onProgress?.(`Updating all configured modules from remote: ${selected.join(', ')}${skipped.length ? ` (skipping local: ${skipped.map(item => item.module).join(', ')})` : ''}`);
|
|
594
748
|
}
|
|
@@ -631,17 +785,32 @@ class EnvironmentUpdater {
|
|
|
631
785
|
// the update. Planning touches nothing and runs before any download or write,
|
|
632
786
|
// so an incomplete installation fails loudly with the path it expected and
|
|
633
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');
|
|
634
794
|
const resourceSource = (0, package_resource_1.bundledResourceDirectory)(package_resource_1.sceneResourceName, this.bundledResources);
|
|
635
795
|
const resourceDestination = path.join(entry.root, ...package_resource_1.sceneResourceDestination.split('/'));
|
|
636
796
|
onProgress?.(`Checking bundled 3D scene resources in ${package_resource_1.sceneResourceDestination}`);
|
|
637
|
-
const resourcePlan = await (0, package_resource_1.planBundledResource)(resourceSource, resourceDestination);
|
|
797
|
+
const resourcePlan = await (0, package_resource_1.planBundledResource)(resourceSource, resourceDestination, refreshesServer);
|
|
638
798
|
const resourceWrites = resourcePlan.added + resourcePlan.updated;
|
|
639
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 };
|
|
640
800
|
const resourceSummary = `${resourcePlan.files.length - resourceWrites} identical, ${resourcePlan.added} missing, ${resourcePlan.updated} different`;
|
|
641
801
|
onProgress?.(`Bundled 3D scene resources in ${package_resource_1.sceneResourceDestination}: ${dryRun ? 'dry run; ' : ''}${resourceSummary}`);
|
|
642
802
|
const downloads = [...(manifest.downloads ?? [])];
|
|
643
|
-
const selectedModules = manifest.selectedModules
|
|
803
|
+
const selectedModules = stringListOrUndefined(manifest.selectedModules);
|
|
804
|
+
const autoAddedModules = stringListOrUndefined(manifest.autoAddedModules);
|
|
644
805
|
const moduleSources = { ...(manifest.moduleSources ?? {}) };
|
|
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') : [];
|
|
645
814
|
const workspace = !dryRun && (remote.length > 0 || resourceWrites > 0) ? await fs.mkdtemp(path.join(path.dirname(entry.root), '.afm-env-')) : undefined;
|
|
646
815
|
const warnings = [];
|
|
647
816
|
try {
|
|
@@ -658,6 +827,23 @@ class EnvironmentUpdater {
|
|
|
658
827
|
staged.set(spec.module, await stageRemoteArtifact(client, resolved.get(spec.module), workspace, onProgress));
|
|
659
828
|
}
|
|
660
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
|
+
}
|
|
661
847
|
const plans = [];
|
|
662
848
|
// A module may carry several local specs: plan it once, for all its targets.
|
|
663
849
|
for (const module of [...new Set(parsed.map(spec => spec.module))]) {
|
|
@@ -685,7 +871,7 @@ class EnvironmentUpdater {
|
|
|
685
871
|
});
|
|
686
872
|
plans.push({
|
|
687
873
|
module, previous, next: { local: override.map(target => target.path).join(', ') }, local: override,
|
|
688
|
-
entries, copies: [], removals, copiedDirectories: [], removedDirectories,
|
|
874
|
+
entries, copies: [], removals, preserved: [], copiedDirectories: [], removedDirectories,
|
|
689
875
|
});
|
|
690
876
|
continue;
|
|
691
877
|
}
|
|
@@ -693,30 +879,59 @@ class EnvironmentUpdater {
|
|
|
693
879
|
const stagedArtifact = staged.get(module);
|
|
694
880
|
const copies = [];
|
|
695
881
|
const removals = [];
|
|
882
|
+
const preserved = [];
|
|
696
883
|
const copiedDirectories = [];
|
|
697
884
|
const removedDirectories = [];
|
|
698
885
|
const entries = [];
|
|
886
|
+
let configurationAssets;
|
|
887
|
+
let configurationLink;
|
|
699
888
|
for (const mapping of remoteArtifact.mappings) {
|
|
700
889
|
const destination = installationDirectory(entry.root, mapping.destination);
|
|
701
890
|
if (await exists(destination)) {
|
|
702
891
|
removals.push(destination);
|
|
703
892
|
removedDirectories.push(mapping.destination);
|
|
704
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
|
+
}
|
|
705
903
|
if (stagedArtifact)
|
|
706
|
-
copies.push({ source: path.join(stagedArtifact.expanded, mapping.source), destination });
|
|
904
|
+
copies.push({ source: path.join(stagedArtifact.expanded, mapping.source), destination, exclude: excludedCopyNames(mapping) });
|
|
707
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
|
+
}
|
|
708
913
|
const assembly = stagedArtifact?.assemblies.get(mapping.destination) ?? knownAssembly(configured, mapping, this.platform);
|
|
709
914
|
// Plugins stay relative to the environment root; a worker entry does too,
|
|
710
915
|
// and its service keeps using the deployed destination as working directory.
|
|
711
|
-
|
|
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') {
|
|
712
918
|
const entry = { destination: mapping.destination, assemblies: mapping.assemblies, path: path.posix.join(mapping.destination, assembly) };
|
|
713
|
-
if (mapping.role === 'worker') {
|
|
714
|
-
entry.role =
|
|
919
|
+
if (mapping.role === 'worker' || mapping.role === 'server') {
|
|
920
|
+
entry.role = mapping.role;
|
|
715
921
|
if (mapping.service)
|
|
716
922
|
entry.service = mapping.service;
|
|
717
923
|
}
|
|
718
924
|
entries.push(entry);
|
|
719
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
|
+
}
|
|
720
935
|
}
|
|
721
936
|
if (module === 'dashboard' && stagedArtifact) {
|
|
722
937
|
const contents = path.join(stagedArtifact.expanded, 'Web Contents');
|
|
@@ -726,13 +941,14 @@ class EnvironmentUpdater {
|
|
|
726
941
|
removals.push(destination);
|
|
727
942
|
removedDirectories.push('Web Contents');
|
|
728
943
|
}
|
|
729
|
-
copies.push({ source: contents, destination });
|
|
944
|
+
copies.push({ source: contents, destination, exclude: [] });
|
|
730
945
|
copiedDirectories.push('Web Contents');
|
|
731
946
|
}
|
|
732
947
|
}
|
|
733
948
|
plans.push({
|
|
734
949
|
module, previous, next: { version: remoteArtifact.version }, artifact: remoteArtifact, staged: stagedArtifact,
|
|
735
|
-
entries, copies, removals,
|
|
950
|
+
entries, copies, removals, preserved, ...(configurationAssets ? { configurationAssets } : {}), ...(configurationLink ? { configurationLink } : {}),
|
|
951
|
+
copiedDirectories, removedDirectories,
|
|
736
952
|
});
|
|
737
953
|
}
|
|
738
954
|
const changes = plans.map(plan => ({ module: plan.module, previous: plan.previous, next: plan.next, copiedDirectories: plan.copiedDirectories, removedDirectories: plan.removedDirectories }));
|
|
@@ -757,20 +973,46 @@ class EnvironmentUpdater {
|
|
|
757
973
|
nextDownloads.push(download);
|
|
758
974
|
else
|
|
759
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
|
+
}
|
|
760
986
|
}
|
|
761
987
|
delete moduleSources[plan.module];
|
|
762
988
|
}
|
|
763
989
|
if (plan.local)
|
|
764
990
|
moduleSources[plan.module] = manifestSource(plan.module, plan.local, moduleSources[plan.module]);
|
|
765
|
-
|
|
766
|
-
|
|
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
|
+
}
|
|
767
1002
|
}
|
|
768
1003
|
if (selectedModules) {
|
|
769
1004
|
const unique = [...new Set(selectedModules)].sort();
|
|
770
1005
|
selectedModules.length = 0;
|
|
771
1006
|
selectedModules.push(...unique);
|
|
772
1007
|
}
|
|
773
|
-
|
|
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;
|
|
774
1016
|
if (Object.keys(moduleSources).length)
|
|
775
1017
|
nextManifest.moduleSources = moduleSources;
|
|
776
1018
|
else
|
|
@@ -789,18 +1031,49 @@ class EnvironmentUpdater {
|
|
|
789
1031
|
else
|
|
790
1032
|
resources[recorded] = { ...resources[recorded], ...record };
|
|
791
1033
|
nextManifest.resources = resources;
|
|
792
|
-
const modules = [...new Set([...entry.modules, ...plans.filter(plan => !installed.has(plan.module)).map(plan => plan.module)])].sort();
|
|
1034
|
+
const modules = [...new Set([...entry.modules, ...plans.filter(plan => !installed.has(plan.module) && selectable.has(plan.module)).map(plan => plan.module)])].sort();
|
|
793
1035
|
const localModules = Object.keys(moduleSources).sort();
|
|
794
1036
|
if (state === 'running')
|
|
795
1037
|
warnings.push(`${entry.name} is running; restart it to load the updated modules.`);
|
|
796
1038
|
for (const item of skipped)
|
|
797
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
|
+
}
|
|
798
1049
|
if (!dryRun) {
|
|
799
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
|
+
}
|
|
800
1065
|
for (const removal of plan.removals)
|
|
801
1066
|
await fs.rm(removal, { recursive: true, force: true });
|
|
802
1067
|
for (const copy of plan.copies)
|
|
803
|
-
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');
|
|
804
1077
|
}
|
|
805
1078
|
// The staged resources are written with the module changes, before the
|
|
806
1079
|
// manifest that records them; a failure rolls the layout back.
|