agl 1.0.17 → 21.0.2
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/.smartconfig.json +55 -0
- package/changelog.md +1026 -0
- package/cli.js +14 -0
- package/dist_serve/bundle.js +44778 -0
- package/dist_serve/index.html +13 -0
- package/dist_ts/00_commitinfo_data.d.ts +8 -0
- package/dist_ts/00_commitinfo_data.js +9 -0
- package/dist_ts/classes.authmodels.d.ts +126 -0
- package/dist_ts/classes.authmodels.js +1422 -0
- package/dist_ts/classes.authstore.d.ts +160 -0
- package/dist_ts/classes.authstore.js +2483 -0
- package/dist_ts/classes.browserresourcehost.d.ts +85 -0
- package/dist_ts/classes.browserresourcehost.js +912 -0
- package/dist_ts/classes.cli.d.ts +21 -0
- package/dist_ts/classes.cli.js +1082 -0
- package/dist_ts/classes.config.d.ts +23 -0
- package/dist_ts/classes.config.js +267 -0
- package/dist_ts/classes.controller.d.ts +523 -0
- package/dist_ts/classes.controller.js +10611 -0
- package/dist_ts/classes.controllermanagement.d.ts +32 -0
- package/dist_ts/classes.controllermanagement.js +222 -0
- package/dist_ts/classes.draftmanager.d.ts +30 -0
- package/dist_ts/classes.draftmanager.js +179 -0
- package/dist_ts/classes.embeddeddb.d.ts +30 -0
- package/dist_ts/classes.embeddeddb.js +159 -0
- package/dist_ts/classes.flexframedtransport.d.ts +56 -0
- package/dist_ts/classes.flexframedtransport.js +275 -0
- package/dist_ts/classes.flexmodels.private.d.ts +231 -0
- package/dist_ts/classes.flexmodels.private.js +985 -0
- package/dist_ts/classes.flexmodels.public.d.ts +207 -0
- package/dist_ts/classes.flexmodels.public.js +885 -0
- package/dist_ts/classes.flexprojectionreader.d.ts +89 -0
- package/dist_ts/classes.flexprojectionreader.js +580 -0
- package/dist_ts/classes.flexservice.d.ts +196 -0
- package/dist_ts/classes.flexservice.js +3704 -0
- package/dist_ts/classes.flexstore.d.ts +174 -0
- package/dist_ts/classes.flexstore.js +3400 -0
- package/dist_ts/classes.flexsupervisor.d.ts +105 -0
- package/dist_ts/classes.flexsupervisor.js +1191 -0
- package/dist_ts/classes.gitreversion.d.ts +431 -0
- package/dist_ts/classes.gitreversion.js +10592 -0
- package/dist_ts/classes.harnessoperationmanager.d.ts +113 -0
- package/dist_ts/classes.harnessoperationmanager.js +602 -0
- package/dist_ts/classes.opencodeclient.d.ts +146 -0
- package/dist_ts/classes.opencodeclient.js +3003 -0
- package/dist_ts/classes.opencodesupervisor.d.ts +60 -0
- package/dist_ts/classes.opencodesupervisor.js +1003 -0
- package/dist_ts/classes.passkeymanager.d.ts +34 -0
- package/dist_ts/classes.passkeymanager.js +456 -0
- package/dist_ts/classes.processinspection.d.ts +50 -0
- package/dist_ts/classes.processinspection.js +484 -0
- package/dist_ts/classes.resourcecoordinator.d.ts +64 -0
- package/dist_ts/classes.resourcecoordinator.js +294 -0
- package/dist_ts/classes.sessionintelligencereader.d.ts +13 -0
- package/dist_ts/classes.sessionintelligencereader.js +40 -0
- package/dist_ts/classes.systemmetricssampler.d.ts +53 -0
- package/dist_ts/classes.systemmetricssampler.js +168 -0
- package/dist_ts/classes.terminalmanager.d.ts +95 -0
- package/dist_ts/classes.terminalmanager.js +981 -0
- package/dist_ts/classes.terminalresourcehost.d.ts +20 -0
- package/dist_ts/classes.terminalresourcehost.js +84 -0
- package/dist_ts/classes.upgradecoordinator.d.ts +244 -0
- package/dist_ts/classes.upgradecoordinator.js +2155 -0
- package/dist_ts/classes.upgradetransaction.d.ts +92 -0
- package/dist_ts/classes.upgradetransaction.js +2125 -0
- package/dist_ts/classes.uploadmanager.d.ts +24 -0
- package/dist_ts/classes.uploadmanager.js +210 -0
- package/dist_ts/classes.validation.d.ts +91 -0
- package/dist_ts/classes.validation.js +1087 -0
- package/dist_ts/flexharness.child.d.ts +1 -0
- package/dist_ts/flexharness.child.js +28 -0
- package/dist_ts/functions.canonicaldirectory.d.ts +21 -0
- package/dist_ts/functions.canonicaldirectory.js +144 -0
- package/dist_ts/functions.controllerdataroot.d.ts +15 -0
- package/dist_ts/functions.controllerdataroot.js +76 -0
- package/dist_ts/functions.embeddeddb.d.ts +2 -0
- package/dist_ts/functions.embeddeddb.js +21 -0
- package/dist_ts/functions.flexcredentialstore.d.ts +16 -0
- package/dist_ts/functions.flexcredentialstore.js +93 -0
- package/dist_ts/functions.opencodeauth.d.ts +3 -0
- package/dist_ts/functions.opencodeauth.js +31 -0
- package/dist_ts/functions.runtimeenvironment.d.ts +2 -0
- package/dist_ts/functions.runtimeenvironment.js +79 -0
- package/dist_ts/functions.upgradefollower.d.ts +3 -0
- package/dist_ts/functions.upgradefollower.js +95 -0
- package/dist_ts/index.d.ts +24 -0
- package/dist_ts/index.js +25 -0
- package/dist_ts/interfaces.auth.d.ts +181 -0
- package/dist_ts/interfaces.auth.js +82 -0
- package/dist_ts/interfaces.config.d.ts +34 -0
- package/dist_ts/interfaces.config.js +2 -0
- package/dist_ts/interfaces.flexipc.d.ts +939 -0
- package/dist_ts/interfaces.flexipc.js +1492 -0
- package/dist_ts/interfaces.gitreversion.d.ts +246 -0
- package/dist_ts/interfaces.gitreversion.js +2 -0
- package/dist_ts/interfaces.lifecycle.d.ts +37 -0
- package/dist_ts/interfaces.lifecycle.js +2 -0
- package/dist_ts/interfaces.opencode.d.ts +101 -0
- package/dist_ts/interfaces.opencode.js +3 -0
- package/dist_ts/interfaces.projects.d.ts +156 -0
- package/dist_ts/interfaces.projects.js +5 -0
- package/dist_ts/plugins.d.ts +36 -0
- package/dist_ts/plugins.js +40 -0
- package/dist_ts_interfaces/index.d.ts +2 -0
- package/dist_ts_interfaces/index.js +3 -0
- package/dist_ts_interfaces/interfaces.d.ts +747 -0
- package/dist_ts_interfaces/interfaces.js +47 -0
- package/dist_ts_interfaces/plugins.d.ts +4 -0
- package/dist_ts_interfaces/plugins.js +3 -0
- package/dist_ts_interfaces/requests.d.ts +806 -0
- package/dist_ts_interfaces/requests.js +2 -0
- package/dist_ts_migration/classes.documentmigrationrunner.d.ts +8 -0
- package/dist_ts_migration/classes.documentmigrationrunner.js +24 -0
- package/dist_ts_migration/classes.flexmigrationrunner.d.ts +11 -0
- package/dist_ts_migration/classes.flexmigrationrunner.js +25 -0
- package/dist_ts_migration/classes.flexpostharnessmigrationrunner.d.ts +10 -0
- package/dist_ts_migration/classes.flexpostharnessmigrationrunner.js +15 -0
- package/dist_ts_migration/classes.migrationrunner.d.ts +8 -0
- package/dist_ts_migration/classes.migrationrunner.js +34 -0
- package/dist_ts_migration/index.d.ts +12 -0
- package/dist_ts_migration/index.js +13 -0
- package/dist_ts_migration/v10_flexharnessv3migration.d.ts +11 -0
- package/dist_ts_migration/v10_flexharnessv3migration.js +23 -0
- package/dist_ts_migration/v13_flexprojectionorder.d.ts +11 -0
- package/dist_ts_migration/v13_flexprojectionorder.js +23 -0
- package/dist_ts_migration/v16_flexprovidercredentials.d.ts +22 -0
- package/dist_ts_migration/v16_flexprovidercredentials.js +149 -0
- package/dist_ts_migration/v18_resourcelayout.d.ts +22 -0
- package/dist_ts_migration/v18_resourcelayout.js +23 -0
- package/dist_ts_migration/v18_resourcelayoutmigration.d.ts +6 -0
- package/dist_ts_migration/v18_resourcelayoutmigration.js +61 -0
- package/dist_ts_migration/v19_flexprojectmanagement.d.ts +11 -0
- package/dist_ts_migration/v19_flexprojectmanagement.js +21 -0
- package/dist_ts_migration/v1_embeddeddatabaselocation.d.ts +28 -0
- package/dist_ts_migration/v1_embeddeddatabaselocation.js +415 -0
- package/dist_ts_migration/v22_projectfilesystemidentitymigration.d.ts +7 -0
- package/dist_ts_migration/v22_projectfilesystemidentitymigration.js +105 -0
- package/dist_ts_migration/v2_controllerdataroot.d.ts +130 -0
- package/dist_ts_migration/v2_controllerdataroot.js +1887 -0
- package/dist_ts_migration/v6_protocoldocumentmigration.d.ts +7 -0
- package/dist_ts_migration/v6_protocoldocumentmigration.js +100 -0
- package/dist_ts_migration/v6_protocoldocuments.d.ts +7 -0
- package/dist_ts_migration/v6_protocoldocuments.js +138 -0
- package/html/index.html +13 -0
- package/{LICENSE → license.md} +2 -2
- package/package.json +86 -48
- package/readme.md +480 -0
- package/readme.plan.md +188 -0
- package/ts/00_commitinfo_data.ts +8 -0
- package/ts/classes.authmodels.ts +1245 -0
- package/ts/classes.authstore.ts +3274 -0
- package/ts/classes.browserresourcehost.ts +1205 -0
- package/ts/classes.cli.ts +1254 -0
- package/ts/classes.config.ts +351 -0
- package/ts/classes.controller.ts +14426 -0
- package/ts/classes.controllermanagement.ts +330 -0
- package/ts/classes.draftmanager.ts +232 -0
- package/ts/classes.embeddeddb.ts +166 -0
- package/ts/classes.flexframedtransport.ts +283 -0
- package/ts/classes.flexmodels.private.ts +823 -0
- package/ts/classes.flexmodels.public.ts +732 -0
- package/ts/classes.flexprojectionreader.ts +816 -0
- package/ts/classes.flexservice.ts +4302 -0
- package/ts/classes.flexstore.ts +4565 -0
- package/ts/classes.flexsupervisor.ts +1453 -0
- package/ts/classes.gitreversion.ts +13353 -0
- package/ts/classes.harnessoperationmanager.ts +870 -0
- package/ts/classes.opencodeclient.ts +4110 -0
- package/ts/classes.opencodesupervisor.ts +1280 -0
- package/ts/classes.passkeymanager.ts +570 -0
- package/ts/classes.processinspection.ts +623 -0
- package/ts/classes.resourcecoordinator.ts +436 -0
- package/ts/classes.sessionintelligencereader.ts +52 -0
- package/ts/classes.systemmetricssampler.ts +215 -0
- package/ts/classes.terminalmanager.ts +1207 -0
- package/ts/classes.terminalresourcehost.ts +122 -0
- package/ts/classes.upgradecoordinator.ts +2675 -0
- package/ts/classes.upgradetransaction.ts +2900 -0
- package/ts/classes.uploadmanager.ts +240 -0
- package/ts/classes.validation.ts +1409 -0
- package/ts/flexharness.child.ts +35 -0
- package/ts/functions.canonicaldirectory.ts +213 -0
- package/ts/functions.controllerdataroot.ts +107 -0
- package/ts/functions.embeddeddb.ts +22 -0
- package/ts/functions.flexcredentialstore.ts +128 -0
- package/ts/functions.opencodeauth.ts +34 -0
- package/ts/functions.runtimeenvironment.ts +90 -0
- package/ts/functions.upgradefollower.ts +115 -0
- package/ts/index.ts +36 -6
- package/ts/interfaces.auth.ts +316 -0
- package/ts/interfaces.config.ts +37 -0
- package/ts/interfaces.flexipc.ts +2586 -0
- package/ts/interfaces.gitreversion.ts +336 -0
- package/ts/interfaces.lifecycle.ts +46 -0
- package/ts/interfaces.opencode.ts +153 -0
- package/ts/interfaces.projects.ts +182 -0
- package/ts/plugins.ts +64 -0
- package/ts_interfaces/index.ts +2 -0
- package/ts_interfaces/interfaces.ts +827 -0
- package/ts_interfaces/plugins.ts +15 -0
- package/ts_interfaces/requests.ts +1186 -0
- package/ts_migration/classes.documentmigrationrunner.ts +32 -0
- package/ts_migration/classes.flexmigrationrunner.ts +33 -0
- package/ts_migration/classes.flexpostharnessmigrationrunner.ts +22 -0
- package/ts_migration/classes.migrationrunner.ts +40 -0
- package/ts_migration/index.ts +12 -0
- package/ts_migration/v10_flexharnessv3migration.ts +35 -0
- package/ts_migration/v13_flexprojectionorder.ts +33 -0
- package/ts_migration/v16_flexprovidercredentials.ts +196 -0
- package/ts_migration/v18_resourcelayout.ts +55 -0
- package/ts_migration/v18_resourcelayoutmigration.ts +68 -0
- package/ts_migration/v19_flexprojectmanagement.ts +28 -0
- package/ts_migration/v1_embeddeddatabaselocation.ts +560 -0
- package/ts_migration/v22_projectfilesystemidentitymigration.ts +121 -0
- package/ts_migration/v2_controllerdataroot.ts +2349 -0
- package/ts_migration/v6_protocoldocumentmigration.ts +144 -0
- package/ts_migration/v6_protocoldocuments.ts +198 -0
- package/ts_web/00_commitinfo_data.ts +8 -0
- package/ts_web/classes.controllersocketclient.ts +517 -0
- package/ts_web/classes.sessiondraftsync.ts +390 -0
- package/ts_web/elements.harnesscontrollerapp.ts +11494 -0
- package/ts_web/index.ts +2 -0
- package/ts_web/plugins.ts +31 -0
- package/.gitlab-ci.yml +0 -71
- package/.npmignore +0 -5
- package/README.md +0 -40
- package/dist/cli.js +0 -2
- package/dist/index.d.ts +0 -0
- package/dist/index.js +0 -9
- package/dist/mod_bundle/index.d.ts +0 -1
- package/dist/mod_bundle/index.js +0 -18
- package/dist/mod_bundle/mod.plugins.d.ts +0 -4
- package/dist/mod_bundle/mod.plugins.js +0 -11
- package/dist/mod_install/index.d.ts +0 -4
- package/dist/mod_install/index.js +0 -21
- package/dist/mod_install/mod.plugins.d.ts +0 -1
- package/dist/mod_install/mod.plugins.js +0 -7
- package/dist/mod_standard/index.d.ts +0 -4
- package/dist/mod_standard/index.js +0 -21
- package/dist/mod_standard/mod.browser.d.ts +0 -6
- package/dist/mod_standard/mod.browser.js +0 -14
- package/dist/mod_standard/mod.clean.d.ts +0 -4
- package/dist/mod_standard/mod.clean.js +0 -12
- package/dist/mod_standard/mod.compile.d.ts +0 -4
- package/dist/mod_standard/mod.compile.js +0 -21
- package/dist/mod_standard/mod.plugins.d.ts +0 -8
- package/dist/mod_standard/mod.plugins.js +0 -19
- package/dist/mod_standard/mod.test.d.ts +0 -4
- package/dist/mod_standard/mod.test.js +0 -12
- package/dist/plr.cli.d.ts +0 -3
- package/dist/plr.cli.js +0 -57
- package/dist/plr.config.d.ts +0 -11
- package/dist/plr.config.js +0 -26
- package/dist/plr.mods.d.ts +0 -17
- package/dist/plr.mods.js +0 -17
- package/dist/plr.paths.d.ts +0 -19
- package/dist/plr.paths.js +0 -26
- package/dist/plr.plugins.d.ts +0 -8
- package/dist/plr.plugins.js +0 -16
- package/npmextra.json +0 -10
- package/ts/mod_bundle/index.ts +0 -20
- package/ts/mod_bundle/mod.plugins.ts +0 -9
- package/ts/mod_install/index.ts +0 -20
- package/ts/mod_install/mod.plugins.ts +0 -1
- package/ts/mod_standard/index.ts +0 -23
- package/ts/mod_standard/mod.browser.ts +0 -17
- package/ts/mod_standard/mod.clean.ts +0 -13
- package/ts/mod_standard/mod.compile.ts +0 -23
- package/ts/mod_standard/mod.plugins.ts +0 -17
- package/ts/mod_standard/mod.test.ts +0 -13
- package/ts/plr.cli.ts +0 -60
- package/ts/plr.config.ts +0 -36
- package/ts/plr.mods.ts +0 -25
- package/ts/plr.paths.ts +0 -29
- package/ts/plr.plugins.ts +0 -17
- package/tslint.json +0 -3
- package/yarn.lock +0 -3464
|
@@ -0,0 +1,436 @@
|
|
|
1
|
+
import {
|
|
2
|
+
controllerRuntimeIdKey,
|
|
3
|
+
type TControllerResource,
|
|
4
|
+
type TControllerSessionId,
|
|
5
|
+
} from '../ts_interfaces/index.js';
|
|
6
|
+
import type { SmartDataAuthStore } from './classes.authstore.js';
|
|
7
|
+
import type { IControllerResourceDocument } from './interfaces.projects.js';
|
|
8
|
+
import {
|
|
9
|
+
type IFlexBrowserResourceDescriptor,
|
|
10
|
+
} from './interfaces.flexipc.js';
|
|
11
|
+
|
|
12
|
+
export interface IControllerResourceRuntimeHost {
|
|
13
|
+
startResource(
|
|
14
|
+
resourceArg: IControllerResourceDocument,
|
|
15
|
+
signalArg: AbortSignal,
|
|
16
|
+
): Promise<void>;
|
|
17
|
+
stopResource(
|
|
18
|
+
resourceArg: IControllerResourceDocument,
|
|
19
|
+
signalArg: AbortSignal,
|
|
20
|
+
): Promise<void>;
|
|
21
|
+
isRunning(resourceArg: IControllerResourceDocument): boolean;
|
|
22
|
+
isAvailable(): boolean;
|
|
23
|
+
renameResource?(resourceArg: IControllerResourceDocument): Promise<void>;
|
|
24
|
+
reconcileAttachment(
|
|
25
|
+
resourceArg: IControllerResourceDocument,
|
|
26
|
+
signalArg: AbortSignal,
|
|
27
|
+
): Promise<void>;
|
|
28
|
+
retireResource(
|
|
29
|
+
resourceArg: IControllerResourceDocument,
|
|
30
|
+
signalArg: AbortSignal,
|
|
31
|
+
): Promise<void>;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface IControllerResourceCoordinatorOptions {
|
|
35
|
+
store: Pick<
|
|
36
|
+
SmartDataAuthStore,
|
|
37
|
+
| 'listResources'
|
|
38
|
+
| 'listRecoverableResources'
|
|
39
|
+
| 'createResource'
|
|
40
|
+
| 'renameResource'
|
|
41
|
+
| 'beginResourceAttachmentTransition'
|
|
42
|
+
| 'commitResourceAttachmentTransition'
|
|
43
|
+
| 'beginResourceRetirement'
|
|
44
|
+
| 'completeResourceRetirement'
|
|
45
|
+
>;
|
|
46
|
+
terminalHost: IControllerResourceRuntimeHost;
|
|
47
|
+
browserHost: IControllerResourceRuntimeHost;
|
|
48
|
+
isFlexHarnessAvailable(): boolean;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export class ControllerResourceUnavailableError extends Error {
|
|
52
|
+
public readonly code = 'resource_unavailable';
|
|
53
|
+
|
|
54
|
+
constructor(messageArg: string) {
|
|
55
|
+
super(messageArg);
|
|
56
|
+
this.name = 'ControllerResourceUnavailableError';
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export class ControllerProjectHasResourcesError extends Error {
|
|
61
|
+
public readonly code = 'project_has_resources';
|
|
62
|
+
|
|
63
|
+
constructor(public readonly resourceIds: string[]) {
|
|
64
|
+
super('Retire every project resource before removing the project.');
|
|
65
|
+
this.name = 'ControllerProjectHasResourcesError';
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
interface IProjectMutationState {
|
|
70
|
+
tail: Promise<void>;
|
|
71
|
+
admissionOpen: boolean;
|
|
72
|
+
removed: boolean;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const sessionIdsEqual = (
|
|
76
|
+
leftArg: TControllerSessionId | null,
|
|
77
|
+
rightArg: TControllerSessionId | null,
|
|
78
|
+
): boolean => leftArg === null
|
|
79
|
+
? rightArg === null
|
|
80
|
+
: rightArg !== null && controllerRuntimeIdKey(leftArg) === controllerRuntimeIdKey(rightArg);
|
|
81
|
+
|
|
82
|
+
export class ControllerResourceCoordinator {
|
|
83
|
+
private readonly projectStates = new Map<string, IProjectMutationState>();
|
|
84
|
+
private readonly deletingSessions = new Set<string>();
|
|
85
|
+
|
|
86
|
+
constructor(private readonly options: IControllerResourceCoordinatorOptions) {}
|
|
87
|
+
|
|
88
|
+
public async listResources(projectIdArg: string): Promise<TControllerResource[]> {
|
|
89
|
+
const resources = await this.options.store.listResources(projectIdArg);
|
|
90
|
+
return resources.map((resource) => this.toPublicResource(resource));
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
public async requireResourceKind(
|
|
94
|
+
projectIdArg: string,
|
|
95
|
+
resourceIdArg: string,
|
|
96
|
+
kindArg: IControllerResourceDocument['kind'],
|
|
97
|
+
): Promise<boolean> {
|
|
98
|
+
return this.runProjectMutation(projectIdArg, async () => {
|
|
99
|
+
const resource = (await this.options.store.listResources(projectIdArg))
|
|
100
|
+
.find((entry) => entry.id === resourceIdArg);
|
|
101
|
+
if (!resource) return false;
|
|
102
|
+
if (resource.kind !== kindArg) {
|
|
103
|
+
throw new ControllerResourceUnavailableError(`The requested ${kindArg} resource is unavailable.`);
|
|
104
|
+
}
|
|
105
|
+
return true;
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
public async createResource(
|
|
110
|
+
inputArg: Parameters<SmartDataAuthStore['createResource']>[0],
|
|
111
|
+
signalArg: AbortSignal,
|
|
112
|
+
): Promise<TControllerResource> {
|
|
113
|
+
return this.runProjectMutation(inputArg.projectId, async () => {
|
|
114
|
+
const resource = await this.options.store.createResource(inputArg);
|
|
115
|
+
await this.hostFor(resource).startResource(resource, signalArg);
|
|
116
|
+
const current = (await this.options.store.listResources(inputArg.projectId))
|
|
117
|
+
.find((entry) => entry.id === resource.id) ?? resource;
|
|
118
|
+
return this.toPublicResource(current);
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
public async renameResource(
|
|
123
|
+
projectIdArg: string,
|
|
124
|
+
resourceIdArg: string,
|
|
125
|
+
titleArg: string,
|
|
126
|
+
): Promise<TControllerResource> {
|
|
127
|
+
return this.runProjectMutation(projectIdArg, async () => {
|
|
128
|
+
const resource = await this.options.store.renameResource(projectIdArg, resourceIdArg, titleArg);
|
|
129
|
+
await this.hostFor(resource).renameResource?.(resource);
|
|
130
|
+
return this.toPublicResource(resource);
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
public async changeAttachment(inputArg: {
|
|
135
|
+
projectId: string;
|
|
136
|
+
resourceId: string;
|
|
137
|
+
sessionId: TControllerSessionId | null;
|
|
138
|
+
expectedAttachmentRevision: number;
|
|
139
|
+
signal: AbortSignal;
|
|
140
|
+
}): Promise<TControllerResource> {
|
|
141
|
+
return this.runProjectMutation(inputArg.projectId, async () => {
|
|
142
|
+
if (
|
|
143
|
+
inputArg.sessionId
|
|
144
|
+
&& this.deletingSessions.has(this.sessionKey(inputArg.projectId, inputArg.sessionId))
|
|
145
|
+
) {
|
|
146
|
+
throw new ControllerResourceUnavailableError('The target session is being deleted.');
|
|
147
|
+
}
|
|
148
|
+
const pending = await this.options.store.beginResourceAttachmentTransition(
|
|
149
|
+
inputArg.projectId,
|
|
150
|
+
inputArg.resourceId,
|
|
151
|
+
inputArg.sessionId,
|
|
152
|
+
inputArg.expectedAttachmentRevision,
|
|
153
|
+
);
|
|
154
|
+
return this.toPublicResource(await this.reconcilePendingAttachment(pending, inputArg.signal));
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
public async retireResource(
|
|
159
|
+
projectIdArg: string,
|
|
160
|
+
resourceIdArg: string,
|
|
161
|
+
signalArg: AbortSignal,
|
|
162
|
+
): Promise<void> {
|
|
163
|
+
await this.runProjectMutation(projectIdArg, async () => {
|
|
164
|
+
const retiring = await this.options.store.beginResourceRetirement(projectIdArg, resourceIdArg);
|
|
165
|
+
if (retiring.lifecycle === 'retired') return;
|
|
166
|
+
await this.hostFor(retiring).retireResource(retiring, signalArg);
|
|
167
|
+
await this.options.store.completeResourceRetirement(projectIdArg, resourceIdArg);
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
public async startResource(
|
|
172
|
+
projectIdArg: string,
|
|
173
|
+
resourceIdArg: string,
|
|
174
|
+
signalArg: AbortSignal,
|
|
175
|
+
): Promise<TControllerResource> {
|
|
176
|
+
return this.runProjectMutation(projectIdArg, async () => {
|
|
177
|
+
const resource = (await this.options.store.listResources(projectIdArg))
|
|
178
|
+
.find((entry) => entry.id === resourceIdArg);
|
|
179
|
+
if (!resource || resource.lifecycle !== 'active' || resource.pendingAttachment) {
|
|
180
|
+
throw new ControllerResourceUnavailableError('The resource cannot be started now.');
|
|
181
|
+
}
|
|
182
|
+
await this.hostFor(resource).startResource(resource, signalArg);
|
|
183
|
+
const current = (await this.options.store.listResources(projectIdArg))
|
|
184
|
+
.find((entry) => entry.id === resourceIdArg) ?? resource;
|
|
185
|
+
return this.toPublicResource(current);
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
public async stopResource(
|
|
190
|
+
projectIdArg: string,
|
|
191
|
+
resourceIdArg: string,
|
|
192
|
+
signalArg: AbortSignal,
|
|
193
|
+
): Promise<TControllerResource> {
|
|
194
|
+
return this.runProjectMutation(projectIdArg, async () => {
|
|
195
|
+
const resource = (await this.options.store.listResources(projectIdArg))
|
|
196
|
+
.find((entry) => entry.id === resourceIdArg);
|
|
197
|
+
if (!resource || resource.lifecycle !== 'active') {
|
|
198
|
+
throw new ControllerResourceUnavailableError('The resource cannot be stopped now.');
|
|
199
|
+
}
|
|
200
|
+
await this.hostFor(resource).stopResource(resource, signalArg);
|
|
201
|
+
const current = (await this.options.store.listResources(projectIdArg))
|
|
202
|
+
.find((entry) => entry.id === resourceIdArg) ?? resource;
|
|
203
|
+
return this.toPublicResource(current);
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
public async recover(signalArg: AbortSignal): Promise<void> {
|
|
208
|
+
for (const resource of await this.options.store.listRecoverableResources()) {
|
|
209
|
+
signalArg.throwIfAborted();
|
|
210
|
+
await this.runProjectMutation(resource.projectId, async () => {
|
|
211
|
+
if (resource.lifecycle === 'retiring') {
|
|
212
|
+
await this.hostFor(resource).retireResource(resource, signalArg);
|
|
213
|
+
await this.options.store.completeResourceRetirement(resource.projectId, resource.id);
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
if (resource.pendingAttachment) {
|
|
217
|
+
await this.reconcilePendingAttachment(resource, signalArg);
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
await this.hostFor(resource).reconcileAttachment(resource, signalArg);
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
public async withSessionDeletion<T>(
|
|
226
|
+
projectIdArg: string,
|
|
227
|
+
sessionIdArg: TControllerSessionId,
|
|
228
|
+
signalArg: AbortSignal,
|
|
229
|
+
deleteArg: () => Promise<T>,
|
|
230
|
+
): Promise<T> {
|
|
231
|
+
const sessionKey = this.sessionKey(projectIdArg, sessionIdArg);
|
|
232
|
+
if (this.deletingSessions.has(sessionKey)) {
|
|
233
|
+
throw new ControllerResourceUnavailableError('The session is already being deleted.');
|
|
234
|
+
}
|
|
235
|
+
this.deletingSessions.add(sessionKey);
|
|
236
|
+
try {
|
|
237
|
+
await this.runProjectMutation(projectIdArg, async () => {
|
|
238
|
+
const resources = await this.options.store.listResources(projectIdArg);
|
|
239
|
+
for (let resource of resources) {
|
|
240
|
+
if (resource.pendingAttachment) {
|
|
241
|
+
resource = await this.reconcilePendingAttachment(resource, signalArg);
|
|
242
|
+
}
|
|
243
|
+
if (!sessionIdsEqual(resource.sessionId, sessionIdArg)) continue;
|
|
244
|
+
const pending = await this.options.store.beginResourceAttachmentTransition(
|
|
245
|
+
projectIdArg,
|
|
246
|
+
resource.id,
|
|
247
|
+
null,
|
|
248
|
+
resource.attachmentRevision,
|
|
249
|
+
);
|
|
250
|
+
await this.reconcilePendingAttachment(pending, signalArg);
|
|
251
|
+
}
|
|
252
|
+
});
|
|
253
|
+
return await deleteArg();
|
|
254
|
+
} finally {
|
|
255
|
+
this.deletingSessions.delete(sessionKey);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
public async withProjectRemovalGuard<T>(
|
|
260
|
+
projectIdArg: string,
|
|
261
|
+
removeArg: () => Promise<T>,
|
|
262
|
+
): Promise<T> {
|
|
263
|
+
const state = this.projectState(projectIdArg);
|
|
264
|
+
if (!state.admissionOpen || state.removed) {
|
|
265
|
+
throw new ControllerResourceUnavailableError('The project is not accepting resource operations.');
|
|
266
|
+
}
|
|
267
|
+
state.admissionOpen = false;
|
|
268
|
+
await state.tail;
|
|
269
|
+
const resources = await this.options.store.listResources(projectIdArg);
|
|
270
|
+
if (resources.length > 0) {
|
|
271
|
+
state.admissionOpen = true;
|
|
272
|
+
throw new ControllerProjectHasResourcesError(resources.map((resource) => resource.id));
|
|
273
|
+
}
|
|
274
|
+
try {
|
|
275
|
+
const result = await removeArg();
|
|
276
|
+
state.removed = true;
|
|
277
|
+
this.projectStates.delete(projectIdArg);
|
|
278
|
+
return result;
|
|
279
|
+
} catch (errorArg) {
|
|
280
|
+
state.admissionOpen = true;
|
|
281
|
+
throw errorArg;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
public async resolveFlexBrowserResources(
|
|
286
|
+
projectIdArg: string,
|
|
287
|
+
sessionIdArg: string,
|
|
288
|
+
): Promise<IFlexBrowserResourceDescriptor[]> {
|
|
289
|
+
return this.runProjectMutation(projectIdArg, async () => (
|
|
290
|
+
(await this.options.store.listResources(projectIdArg))
|
|
291
|
+
.filter((resource) => (
|
|
292
|
+
resource.kind === 'browser'
|
|
293
|
+
&& resource.lifecycle === 'active'
|
|
294
|
+
&& resource.pendingAttachment === undefined
|
|
295
|
+
&& resource.sessionId?.harnessId === 'flex'
|
|
296
|
+
&& resource.sessionId.nativeId === sessionIdArg
|
|
297
|
+
))
|
|
298
|
+
.sort((left, right) => left.id.localeCompare(right.id))
|
|
299
|
+
.map((resource) => ({
|
|
300
|
+
resourceId: resource.id,
|
|
301
|
+
attachmentRevision: resource.attachmentRevision,
|
|
302
|
+
}))
|
|
303
|
+
));
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
public async requireFlexBrowserResource(inputArg: {
|
|
307
|
+
projectId: string;
|
|
308
|
+
sessionId: string;
|
|
309
|
+
resourceId: string;
|
|
310
|
+
attachmentRevision: number;
|
|
311
|
+
}): Promise<IControllerResourceDocument> {
|
|
312
|
+
return this.runProjectMutation(inputArg.projectId, async () => {
|
|
313
|
+
const resource = (await this.options.store.listResources(inputArg.projectId))
|
|
314
|
+
.find((entry) => entry.id === inputArg.resourceId);
|
|
315
|
+
if (
|
|
316
|
+
!resource
|
|
317
|
+
|| resource.kind !== 'browser'
|
|
318
|
+
|| resource.lifecycle !== 'active'
|
|
319
|
+
|| resource.pendingAttachment !== undefined
|
|
320
|
+
|| resource.attachmentRevision !== inputArg.attachmentRevision
|
|
321
|
+
|| resource.sessionId?.harnessId !== 'flex'
|
|
322
|
+
|| resource.sessionId.nativeId !== inputArg.sessionId
|
|
323
|
+
) throw new ControllerResourceUnavailableError('The browser attachment is no longer current.');
|
|
324
|
+
return resource;
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
private async reconcilePendingAttachment(
|
|
329
|
+
resourceArg: IControllerResourceDocument,
|
|
330
|
+
signalArg: AbortSignal,
|
|
331
|
+
): Promise<IControllerResourceDocument> {
|
|
332
|
+
const pending = resourceArg.pendingAttachment;
|
|
333
|
+
if (!pending) return resourceArg;
|
|
334
|
+
const target: IControllerResourceDocument = {
|
|
335
|
+
...resourceArg,
|
|
336
|
+
attachmentRevision: pending.attachmentRevision,
|
|
337
|
+
sessionId: pending.sessionId === null ? null : { ...pending.sessionId },
|
|
338
|
+
};
|
|
339
|
+
delete target.pendingAttachment;
|
|
340
|
+
await this.hostFor(resourceArg).reconcileAttachment(target, signalArg);
|
|
341
|
+
return this.options.store.commitResourceAttachmentTransition(
|
|
342
|
+
resourceArg.projectId,
|
|
343
|
+
resourceArg.id,
|
|
344
|
+
pending.operationId,
|
|
345
|
+
);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
private toPublicResource(resourceArg: IControllerResourceDocument): TControllerResource {
|
|
349
|
+
const attachment = {
|
|
350
|
+
authorityId: resourceArg.attachmentAuthorityId,
|
|
351
|
+
revision: resourceArg.attachmentRevision,
|
|
352
|
+
sessionId: resourceArg.sessionId === null ? null : { ...resourceArg.sessionId },
|
|
353
|
+
};
|
|
354
|
+
const agentAvailability = resourceArg.pendingAttachment
|
|
355
|
+
? 'transitioning' as const
|
|
356
|
+
: resourceArg.sessionId === null
|
|
357
|
+
? 'detached' as const
|
|
358
|
+
: resourceArg.kind === 'browser'
|
|
359
|
+
? resourceArg.sessionId?.harnessId === 'flex'
|
|
360
|
+
&& this.options.browserHost.isAvailable()
|
|
361
|
+
&& this.options.isFlexHarnessAvailable()
|
|
362
|
+
? 'available' as const
|
|
363
|
+
: 'dependencyUnavailable' as const
|
|
364
|
+
: 'available' as const;
|
|
365
|
+
const base = {
|
|
366
|
+
id: resourceArg.id,
|
|
367
|
+
projectId: resourceArg.projectId,
|
|
368
|
+
kind: resourceArg.kind,
|
|
369
|
+
title: resourceArg.title,
|
|
370
|
+
lifecycle: resourceArg.lifecycle,
|
|
371
|
+
attachment,
|
|
372
|
+
agentAvailability,
|
|
373
|
+
createdAt: resourceArg.createdAt.getTime(),
|
|
374
|
+
updatedAt: resourceArg.updatedAt.getTime(),
|
|
375
|
+
};
|
|
376
|
+
if (resourceArg.kind === 'browser') {
|
|
377
|
+
return {
|
|
378
|
+
...base,
|
|
379
|
+
kind: 'browser',
|
|
380
|
+
browserRuntimeState: this.options.browserHost.isAvailable() ? 'available' : 'unavailable',
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
const terminal = resourceArg.terminal!;
|
|
384
|
+
return {
|
|
385
|
+
...base,
|
|
386
|
+
kind: 'terminal',
|
|
387
|
+
command: terminal.command,
|
|
388
|
+
cwd: terminal.cwd,
|
|
389
|
+
processState: this.options.terminalHost.isRunning(resourceArg) ? 'running' : 'stopped',
|
|
390
|
+
...(terminal.stoppedAt === undefined ? {} : { stoppedAt: terminal.stoppedAt.getTime() }),
|
|
391
|
+
...(terminal.lastExitCode === undefined ? {} : { lastExitCode: terminal.lastExitCode }),
|
|
392
|
+
};
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
private hostFor(resourceArg: IControllerResourceDocument): IControllerResourceRuntimeHost {
|
|
396
|
+
return resourceArg.kind === 'terminal' ? this.options.terminalHost : this.options.browserHost;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
private projectState(projectIdArg: string): IProjectMutationState {
|
|
400
|
+
let state = this.projectStates.get(projectIdArg);
|
|
401
|
+
if (!state) {
|
|
402
|
+
state = { tail: Promise.resolve(), admissionOpen: true, removed: false };
|
|
403
|
+
this.projectStates.set(projectIdArg, state);
|
|
404
|
+
}
|
|
405
|
+
return state;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
private runProjectMutation<T>(projectIdArg: string, operationArg: () => Promise<T>): Promise<T> {
|
|
409
|
+
const state = this.projectState(projectIdArg);
|
|
410
|
+
if (!state.admissionOpen || state.removed) {
|
|
411
|
+
return Promise.reject(new ControllerResourceUnavailableError(
|
|
412
|
+
'The project is not accepting resource operations.',
|
|
413
|
+
));
|
|
414
|
+
}
|
|
415
|
+
const operation = state.tail.then(async () => {
|
|
416
|
+
if (!state.admissionOpen || state.removed) {
|
|
417
|
+
throw new ControllerResourceUnavailableError('The project stopped accepting resource operations.');
|
|
418
|
+
}
|
|
419
|
+
return operationArg();
|
|
420
|
+
});
|
|
421
|
+
const tail = operation.then(() => undefined, () => undefined);
|
|
422
|
+
state.tail = tail;
|
|
423
|
+
void tail.finally(() => {
|
|
424
|
+
if (
|
|
425
|
+
state.admissionOpen
|
|
426
|
+
&& !state.removed
|
|
427
|
+
&& state.tail === tail
|
|
428
|
+
) this.projectStates.delete(projectIdArg);
|
|
429
|
+
});
|
|
430
|
+
return operation;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
private sessionKey(projectIdArg: string, sessionIdArg: TControllerSessionId): string {
|
|
434
|
+
return `${projectIdArg}:${controllerRuntimeIdKey(sessionIdArg)}`;
|
|
435
|
+
}
|
|
436
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import * as plugins from './plugins.js';
|
|
2
|
+
import type { IOpenCodeConnectionConfig } from './interfaces.opencode.js';
|
|
3
|
+
|
|
4
|
+
export class SessionIntelligenceReader {
|
|
5
|
+
private readonly registry: plugins.crossharness.ConnectionRegistry;
|
|
6
|
+
private connectionId?: string;
|
|
7
|
+
|
|
8
|
+
constructor(private readonly config: IOpenCodeConnectionConfig) {
|
|
9
|
+
this.registry = new plugins.crossharness.ConnectionRegistry({
|
|
10
|
+
allowedServerUrls: [config.baseUrl],
|
|
11
|
+
maxConnections: 1,
|
|
12
|
+
openCodeCredentials: {
|
|
13
|
+
serverUrl: config.baseUrl,
|
|
14
|
+
username: config.username,
|
|
15
|
+
password: config.password,
|
|
16
|
+
},
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
public async init(signalArg?: AbortSignal): Promise<void> {
|
|
21
|
+
if (this.connectionId) throw new Error('The Session Intelligence reader is already initialized.');
|
|
22
|
+
const connection = await this.registry.connect(
|
|
23
|
+
'opencode',
|
|
24
|
+
this.config.baseUrl,
|
|
25
|
+
this.config.directory,
|
|
26
|
+
signalArg,
|
|
27
|
+
);
|
|
28
|
+
this.connectionId = connection.connectionId;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
public listChats(limitArg: number): Promise<plugins.crossharness.IChatSummary[]> {
|
|
32
|
+
return this.connection().listChats(limitArg);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
public readChat(
|
|
36
|
+
chatIdArg: string,
|
|
37
|
+
limitArg: number,
|
|
38
|
+
maxCharsArg: number,
|
|
39
|
+
): Promise<plugins.crossharness.IChatTranscript> {
|
|
40
|
+
return this.connection().readChat(chatIdArg, limitArg, maxCharsArg);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
public async close(): Promise<void> {
|
|
44
|
+
await this.registry.closeAll();
|
|
45
|
+
this.connectionId = undefined;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
private connection(): plugins.crossharness.IHarnessConnection {
|
|
49
|
+
if (!this.connectionId) throw new Error('The Session Intelligence reader is not initialized.');
|
|
50
|
+
return this.registry.get(this.connectionId);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
import * as plugins from './plugins.js';
|
|
2
|
+
import type { IControllerSystemMetrics } from '../ts_interfaces/index.js';
|
|
3
|
+
|
|
4
|
+
interface ISystemMetricsSource {
|
|
5
|
+
currentLoad: () => Promise<{ currentLoad: number }>;
|
|
6
|
+
mem: () => Promise<{ active: number; total: number }>;
|
|
7
|
+
networkInterfaces: () => Promise<Array<{
|
|
8
|
+
iface: string;
|
|
9
|
+
virtual: boolean;
|
|
10
|
+
}>>;
|
|
11
|
+
networkStats: (interfaces: string) => Promise<Array<{
|
|
12
|
+
rx_sec: number;
|
|
13
|
+
tx_sec: number;
|
|
14
|
+
}>>;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface IRawSystemMetrics {
|
|
18
|
+
cpuUsagePercent?: unknown;
|
|
19
|
+
memoryUsedBytes?: unknown;
|
|
20
|
+
memoryTotalBytes?: unknown;
|
|
21
|
+
networkStats?: Array<{
|
|
22
|
+
rx_sec?: unknown;
|
|
23
|
+
tx_sec?: unknown;
|
|
24
|
+
}>;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
interface ISystemMetricsSamplerOptions {
|
|
28
|
+
source?: ISystemMetricsSource;
|
|
29
|
+
now?: () => number;
|
|
30
|
+
sampleTimeoutMs?: number;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const cacheTtlMs = 1_000;
|
|
34
|
+
const defaultSampleTimeoutMs = 2_000;
|
|
35
|
+
|
|
36
|
+
const asNonNegativeNumber = (valueArg: unknown): number | null => (
|
|
37
|
+
typeof valueArg === 'number' && Number.isFinite(valueArg) && valueArg >= 0
|
|
38
|
+
? valueArg
|
|
39
|
+
: null
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
const asPercentage = (valueArg: unknown): number | null => {
|
|
43
|
+
const value = asNonNegativeNumber(valueArg);
|
|
44
|
+
return value !== null && value <= 100 ? value : null;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const aggregateNetworkRate = (
|
|
48
|
+
statsArg: IRawSystemMetrics['networkStats'],
|
|
49
|
+
keyArg: 'rx_sec' | 'tx_sec',
|
|
50
|
+
): number | null => {
|
|
51
|
+
if (!statsArg || statsArg.length === 0) return null;
|
|
52
|
+
let total = 0;
|
|
53
|
+
for (const stats of statsArg) {
|
|
54
|
+
const value = asNonNegativeNumber(stats[keyArg]);
|
|
55
|
+
if (value === null) return null;
|
|
56
|
+
total += value;
|
|
57
|
+
}
|
|
58
|
+
return Number.isFinite(total) ? total : null;
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
export const normalizeSystemMetrics = (
|
|
62
|
+
sampledAtArg: number,
|
|
63
|
+
rawArg: IRawSystemMetrics,
|
|
64
|
+
): IControllerSystemMetrics => {
|
|
65
|
+
const memoryTotalBytes = asNonNegativeNumber(rawArg.memoryTotalBytes);
|
|
66
|
+
const memoryUsedCandidate = asNonNegativeNumber(rawArg.memoryUsedBytes);
|
|
67
|
+
const memoryUsedBytes = (
|
|
68
|
+
memoryUsedCandidate !== null
|
|
69
|
+
&& memoryTotalBytes !== null
|
|
70
|
+
&& memoryTotalBytes > 0
|
|
71
|
+
&& memoryUsedCandidate <= memoryTotalBytes
|
|
72
|
+
) ? memoryUsedCandidate : null;
|
|
73
|
+
|
|
74
|
+
return {
|
|
75
|
+
sampledAt: sampledAtArg,
|
|
76
|
+
cpuUsagePercent: asPercentage(rawArg.cpuUsagePercent),
|
|
77
|
+
memoryUsedBytes,
|
|
78
|
+
memoryTotalBytes: memoryTotalBytes !== null && memoryTotalBytes > 0
|
|
79
|
+
? memoryTotalBytes
|
|
80
|
+
: null,
|
|
81
|
+
networkReceiveBytesPerSecond: aggregateNetworkRate(rawArg.networkStats, 'rx_sec'),
|
|
82
|
+
networkTransmitBytesPerSecond: aggregateNetworkRate(rawArg.networkStats, 'tx_sec'),
|
|
83
|
+
};
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
export class SystemMetricsSampler {
|
|
87
|
+
private readonly source: ISystemMetricsSource;
|
|
88
|
+
private readonly now: () => number;
|
|
89
|
+
private readonly sampleTimeoutMs: number;
|
|
90
|
+
private cachedSample: IControllerSystemMetrics | undefined;
|
|
91
|
+
private inFlightSample: Promise<IControllerSystemMetrics> | undefined;
|
|
92
|
+
private inFlightCpuRead: Promise<{ currentLoad: number }> | undefined;
|
|
93
|
+
private inFlightMemoryRead: Promise<{ active: number; total: number }> | undefined;
|
|
94
|
+
private inFlightNetworkRead: Promise<Array<{ rx_sec: number; tx_sec: number }>> | undefined;
|
|
95
|
+
|
|
96
|
+
constructor(optionsArg: ISystemMetricsSamplerOptions = {}) {
|
|
97
|
+
this.source = optionsArg.source ?? plugins.systeminformation;
|
|
98
|
+
this.now = optionsArg.now ?? Date.now;
|
|
99
|
+
this.sampleTimeoutMs = optionsArg.sampleTimeoutMs ?? defaultSampleTimeoutMs;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
public async sample(): Promise<IControllerSystemMetrics> {
|
|
103
|
+
const now = this.now();
|
|
104
|
+
if (this.cachedSample && now - this.cachedSample.sampledAt < cacheTtlMs) {
|
|
105
|
+
return this.cachedSample;
|
|
106
|
+
}
|
|
107
|
+
if (this.inFlightSample) return this.inFlightSample;
|
|
108
|
+
|
|
109
|
+
const samplePromise = this.readFreshSample();
|
|
110
|
+
this.inFlightSample = samplePromise;
|
|
111
|
+
try {
|
|
112
|
+
const sample = await samplePromise;
|
|
113
|
+
this.cachedSample = sample;
|
|
114
|
+
return sample;
|
|
115
|
+
} finally {
|
|
116
|
+
if (this.inFlightSample === samplePromise) {
|
|
117
|
+
this.inFlightSample = undefined;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
private async readFreshSample(): Promise<IControllerSystemMetrics> {
|
|
123
|
+
return normalizeSystemMetrics(this.now(), await this.readSourceMetrics());
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
private async readSourceMetrics(): Promise<IRawSystemMetrics> {
|
|
127
|
+
const [cpuResult, memoryResult, networkResult] = await Promise.all([
|
|
128
|
+
this.readCpu(),
|
|
129
|
+
this.readMemory(),
|
|
130
|
+
this.readNetwork(),
|
|
131
|
+
]);
|
|
132
|
+
return {
|
|
133
|
+
...(cpuResult
|
|
134
|
+
? { cpuUsagePercent: cpuResult.currentLoad }
|
|
135
|
+
: {}),
|
|
136
|
+
...(memoryResult
|
|
137
|
+
? {
|
|
138
|
+
memoryUsedBytes: memoryResult.active,
|
|
139
|
+
memoryTotalBytes: memoryResult.total,
|
|
140
|
+
}
|
|
141
|
+
: {}),
|
|
142
|
+
...(networkResult
|
|
143
|
+
? { networkStats: networkResult }
|
|
144
|
+
: {}),
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
private async readCpu(): Promise<{ currentLoad: number } | undefined> {
|
|
149
|
+
let sourceRead = this.inFlightCpuRead;
|
|
150
|
+
if (!sourceRead) {
|
|
151
|
+
sourceRead = Promise.resolve().then(() => this.source.currentLoad());
|
|
152
|
+
this.inFlightCpuRead = sourceRead;
|
|
153
|
+
const clear = (): void => {
|
|
154
|
+
if (this.inFlightCpuRead === sourceRead) this.inFlightCpuRead = undefined;
|
|
155
|
+
};
|
|
156
|
+
void sourceRead.then(clear, clear);
|
|
157
|
+
}
|
|
158
|
+
return this.withTimeout(sourceRead);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
private async readMemory(): Promise<{ active: number; total: number } | undefined> {
|
|
162
|
+
let sourceRead = this.inFlightMemoryRead;
|
|
163
|
+
if (!sourceRead) {
|
|
164
|
+
sourceRead = Promise.resolve().then(() => this.source.mem());
|
|
165
|
+
this.inFlightMemoryRead = sourceRead;
|
|
166
|
+
const clear = (): void => {
|
|
167
|
+
if (this.inFlightMemoryRead === sourceRead) this.inFlightMemoryRead = undefined;
|
|
168
|
+
};
|
|
169
|
+
void sourceRead.then(clear, clear);
|
|
170
|
+
}
|
|
171
|
+
return this.withTimeout(sourceRead);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
private async readNetwork(): Promise<Array<{ rx_sec: number; tx_sec: number }> | undefined> {
|
|
175
|
+
let sourceRead = this.inFlightNetworkRead;
|
|
176
|
+
if (!sourceRead) {
|
|
177
|
+
sourceRead = Promise.resolve().then(() => this.readNetworkStats());
|
|
178
|
+
this.inFlightNetworkRead = sourceRead;
|
|
179
|
+
const clear = (): void => {
|
|
180
|
+
if (this.inFlightNetworkRead === sourceRead) this.inFlightNetworkRead = undefined;
|
|
181
|
+
};
|
|
182
|
+
void sourceRead.then(clear, clear);
|
|
183
|
+
}
|
|
184
|
+
return this.withTimeout(sourceRead);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
private async withTimeout<T>(sourceReadArg: Promise<T>): Promise<T | undefined> {
|
|
188
|
+
let timeout: ReturnType<typeof setTimeout> | undefined;
|
|
189
|
+
const timedOut = Symbol('systemMetricsTimeout');
|
|
190
|
+
try {
|
|
191
|
+
const result = await Promise.race([
|
|
192
|
+
sourceReadArg,
|
|
193
|
+
new Promise<typeof timedOut>((resolve) => {
|
|
194
|
+
timeout = setTimeout(() => resolve(timedOut), this.sampleTimeoutMs);
|
|
195
|
+
}),
|
|
196
|
+
]);
|
|
197
|
+
return result === timedOut ? undefined : result;
|
|
198
|
+
} catch {
|
|
199
|
+
return undefined;
|
|
200
|
+
} finally {
|
|
201
|
+
if (timeout) clearTimeout(timeout);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
private async readNetworkStats(): Promise<Array<{ rx_sec: number; tx_sec: number }>> {
|
|
206
|
+
const interfaces = await this.source.networkInterfaces();
|
|
207
|
+
const interfaceNames = [...new Set(
|
|
208
|
+
interfaces
|
|
209
|
+
.filter((networkInterface) => !networkInterface.virtual && networkInterface.iface.length > 0)
|
|
210
|
+
.map((networkInterface) => networkInterface.iface),
|
|
211
|
+
)].slice(0, 128);
|
|
212
|
+
if (interfaceNames.length === 0) return [];
|
|
213
|
+
return this.source.networkStats(interfaceNames.join(','));
|
|
214
|
+
}
|
|
215
|
+
}
|