• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

node-opcua / node-opcua / 25883546329

14 May 2026 08:22PM UTC coverage: 92.025% (-0.01%) from 92.037%
25883546329

push

github

erossignon
v2.172.1

18396 of 21698 branches covered (84.78%)

163847 of 178047 relevant lines covered (92.02%)

431832.59 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

85.54
/packages/node-opcua-server/source/register_server_manager.ts
1
/**
1✔
2
 * @module node-opcua-server
2✔
3
 */
2✔
4
// tslint:disable:no-console
2✔
5
import { EventEmitter } from "node:events";
2✔
6
import chalk from "chalk";
2✔
7

2✔
8
import { assert } from "node-opcua-assert";
2✔
9
import type { UAString } from "node-opcua-basic-types";
2✔
10
import { coerceLocalizedText, type LocalizedTextOptions, OPCUAClientBase, type ResponseCallback } from "node-opcua-client";
2✔
11
import type { ICertificateStore } from "node-opcua-common";
2✔
12
import { exploreCertificate } from "node-opcua-crypto/web";
2✔
13
import { checkDebugFlag, make_debugLog, make_warningLog } from "node-opcua-debug";
2✔
14
import { resolveFullyQualifiedDomainName } from "node-opcua-hostname";
2✔
15
import { coerceSecurityPolicy, MessageSecurityMode, SecurityPolicy } from "node-opcua-secure-channel";
2✔
16
import {
2✔
17
    RegisterServer2Request,
2✔
18
    type RegisterServer2Response,
2✔
19
    RegisterServerRequest,
2✔
20
    type RegisterServerResponse
2✔
21
} from "node-opcua-service-discovery";
2✔
22
import {
2✔
23
    type ApplicationType,
2✔
24
    type EndpointDescription,
2✔
25
    MdnsDiscoveryConfiguration,
2✔
26
    type RegisteredServerOptions
2✔
27
} from "node-opcua-types";
2✔
28
import { type IRegisterServerManager, RegisterServerManagerStatus } from "./i_register_server_manager";
2✔
29

2✔
30
const _doDebug = checkDebugFlag("REGISTER_LDS");
2✔
31
const debugLog = make_debugLog("REGISTER_LDS");
2✔
32
const warningLog = make_warningLog("REGISTER_LDS");
2✔
33

2✔
34
const g_DefaultRegistrationServerTimeout = 8 * 60 * 1000; // 8 minutes
2✔
35

2✔
36
function securityPolicyLevel(securityPolicy: UAString): number {
624✔
37
    switch (securityPolicy) {
624✔
38
        case SecurityPolicy.None:
624!
39
            return 0;
×
40
        case SecurityPolicy.Basic128:
624!
41
            return 1;
×
42
        case SecurityPolicy.Basic128Rsa15:
624!
43
            return 2;
×
44
        case SecurityPolicy.Basic192:
624!
45
            return 3;
×
46
        case SecurityPolicy.Basic192Rsa15:
624!
47
            return 4;
×
48
        case SecurityPolicy.Basic256:
624!
49
            return 5;
×
50
        case SecurityPolicy.Basic256Rsa15:
624!
51
            return 6;
×
52
        case SecurityPolicy.Aes128_Sha256_RsaOaep:
624✔
53
            return 7;
234✔
54
        case SecurityPolicy.Basic256Sha256:
624✔
55
            return 8;
156✔
56
        case SecurityPolicy.Aes256_Sha256_RsaPss:
624✔
57
            return 9;
234✔
58
        default:
624!
59
            return 0;
×
60
    }
624✔
61
}
624✔
62

2✔
63
function sortEndpointBySecurityLevel(endpoints: EndpointDescription[]): EndpointDescription[] {
78✔
64
    endpoints.sort((a: EndpointDescription, b: EndpointDescription) => {
78✔
65
        if (a.securityMode === b.securityMode) {
312✔
66
            if (a.securityPolicyUri === b.securityPolicyUri) {
312!
67
                const sa = a.securityLevel;
×
68
                const sb = b.securityLevel;
×
69
                return sa < sb ? 1 : sa > sb ? -1 : 0;
×
70
            } else {
312✔
71
                const sa = securityPolicyLevel(a.securityPolicyUri);
312✔
72
                const sb = securityPolicyLevel(b.securityPolicyUri);
312✔
73
                return sa < sb ? 1 : sa > sb ? -1 : 0;
312!
74
            }
312✔
75
        } else {
312!
76
            return a.securityMode < b.securityMode ? 1 : 0;
×
77
        }
×
78
    });
78✔
79
    return endpoints;
78✔
80
}
78✔
81

2✔
82
function findSecureEndpoint(endpoints: EndpointDescription[]): EndpointDescription | null {
78✔
83
    // we only care about binary tcp transport endpoint
78✔
84
    endpoints = endpoints.filter((e: EndpointDescription) => {
78✔
85
        return e.transportProfileUri === "http://opcfoundation.org/UA-Profile/Transport/uatcp-uasc-uabinary";
546✔
86
    });
78✔
87

78✔
88
    endpoints = endpoints.filter((e: EndpointDescription) => {
78✔
89
        return e.securityMode === MessageSecurityMode.SignAndEncrypt;
546✔
90
    });
78✔
91

78✔
92
    if (endpoints.length === 0) {
78!
93
        endpoints = endpoints.filter((e: EndpointDescription) => {
×
94
            return e.securityMode === MessageSecurityMode.Sign;
×
95
        });
×
96
    }
×
97
    if (endpoints.length === 0) {
78!
98
        endpoints = endpoints.filter((e: EndpointDescription) => {
×
99
            return e.securityMode === MessageSecurityMode.None;
×
100
        });
×
101
    }
×
102
    endpoints = sortEndpointBySecurityLevel(endpoints);
78✔
103
    return endpoints[0];
78✔
104
}
78✔
105

2✔
106
function constructRegisteredServer(server: IPartialServer, isOnline: boolean): RegisteredServerOptions {
167✔
107
    const discoveryUrls = server.getDiscoveryUrls();
167✔
108
    assert(!isOnline || discoveryUrls.length >= 1, "expecting some discoveryUrls if we go online .... ");
167✔
109

167✔
110
    const info = exploreCertificate(server.getCertificate());
167✔
111
    const _commonName = info.tbsCertificate.subject.commonName ?? "";
167!
112

167✔
113
    const serverUri = info.tbsCertificate.extensions?.subjectAltName?.uniformResourceIdentifier[0];
167✔
114
    // c8 ignore next
167✔
115
    if (serverUri !== server.serverInfo.applicationUri) {
167!
116
        warningLog(
×
117
            chalk.yellow("Warning certificate uniformResourceIdentifier doesn't match serverInfo.applicationUri"),
×
118
            "\n subjectKeyIdentifier      : ",
×
119
            info.tbsCertificate.extensions?.subjectKeyIdentifier,
×
120
            "\n subjectAltName            : ",
×
121
            info.tbsCertificate.extensions?.subjectAltName,
×
122
            "\n commonName                : ",
×
123
            info.tbsCertificate.subject.commonName,
×
124
            "\n serverInfo.applicationUri : ",
×
125
            server.serverInfo.applicationUri
×
126
        );
×
127
    }
×
128

167✔
129
    // c8 ignore next
167✔
130
    if (!server.serverInfo.applicationName.text) {
167!
131
        debugLog("warning: application name is missing");
×
132
    }
×
133
    // The globally unique identifier for the Server instance. The serverUri matches
167✔
134
    // the applicationUri from the ApplicationDescription defined in 7.1.
167✔
135
    const s = {
167✔
136
        serverUri: server.serverInfo.applicationUri,
167✔
137

167✔
138
        // The globally unique identifier for the Server product.
167✔
139
        productUri: server.serverInfo.productUri,
167✔
140

167✔
141
        serverNames: [
167✔
142
            {
167✔
143
                locale: "en-US",
167✔
144
                text: server.serverInfo.applicationName.text
167✔
145
            }
167✔
146
        ],
167✔
147
        serverType: server.serverType,
167✔
148

167✔
149
        discoveryUrls,
167✔
150
        gatewayServerUri: null,
167✔
151
        isOnline,
167✔
152
        semaphoreFilePath: null
167✔
153
    };
167✔
154
    return s;
167✔
155
}
167✔
156
function constructRegisterServerRequest(serverB: IPartialServer, isOnline: boolean): RegisterServerRequest {
1✔
157
    const server = constructRegisteredServer(serverB, isOnline);
1✔
158
    return new RegisterServerRequest({
1✔
159
        server
1✔
160
    });
1✔
161
}
1✔
162

2✔
163
function constructRegisterServer2Request(serverB: IPartialServer, isOnline: boolean): RegisterServer2Request {
166✔
164
    const server = constructRegisteredServer(serverB, isOnline);
166✔
165

166✔
166
    return new RegisterServer2Request({
166✔
167
        discoveryConfiguration: [
166✔
168
            new MdnsDiscoveryConfiguration({
166✔
169
                mdnsServerName: serverB.serverInfo.applicationUri,
166✔
170
                serverCapabilities: serverB.capabilitiesForMDNS
166✔
171
            })
166✔
172
        ],
166✔
173
        server
166✔
174
    });
166✔
175
}
166✔
176

2✔
177
const no_retry_connectivity_strategy = {
2✔
178
    initialDelay: 1000,
2✔
179
    maxDelay: 2000,
2✔
180
    maxRetry: 1, // NO RETRY !!!
2✔
181
    randomisationFactor: 0
2✔
182
};
2✔
183
const infinite_connectivity_strategy = {
2✔
184
    initialDelay: 2000,
2✔
185
    maxDelay: 50000,
2✔
186
    maxRetry: 10000000,
2✔
187
    randomisationFactor: 0
2✔
188
};
2✔
189

2✔
190
const pause = async (duration: number) => await new Promise<void>((resolve) => setTimeout(resolve, duration));
2✔
191

2✔
192
interface ClientBaseEx extends OPCUAClientBase {
2✔
193
    _serverEndpoints: EndpointDescription[];
2✔
194

2✔
195
    performMessageTransaction(request: RegisterServer2Request, callback: ResponseCallback<RegisterServer2Response>): void;
2✔
196
    performMessageTransaction(request: RegisterServerRequest, callback: ResponseCallback<RegisterServerResponse>): void;
2✔
197
}
2✔
198

2✔
199
async function sendRegisterServerRequest(server: IPartialServer, client: ClientBaseEx, isOnline: boolean) {
166✔
200
    // try to send a RegisterServer2Request
166✔
201
    const request = constructRegisterServer2Request(server, isOnline);
166✔
202

166✔
203
    await new Promise<void>((resolve, reject) => {
166✔
204
        client.performMessageTransaction(request, (err: Error | null, _response?: RegisterServer2Response) => {
166✔
205
            if (!err) {
166✔
206
                // RegisterServerResponse
165✔
207
                debugLog("RegisterServerManager#_registerServer sendRegisterServer2Request has succeeded (isOnline", isOnline, ")");
165✔
208
                return resolve();
165✔
209
            }
165✔
210
            debugLog("RegisterServerManager#_registerServer sendRegisterServer2Request has failed " + "(isOnline", isOnline, ")");
1✔
211
            debugLog("RegisterServerManager#_registerServer" + " falling back to using sendRegisterServerRequest instead");
1✔
212
            // fall back to
1✔
213
            const request1 = constructRegisterServerRequest(server, isOnline);
1✔
214
            client.performMessageTransaction(request1, (err1: Error | null, _response1?: RegisterServerResponse) => {
1✔
215
                if (!err1) {
1!
216
                    debugLog(
×
217
                        "RegisterServerManager#_registerServer sendRegisterServerRequest " + "has succeeded (isOnline",
×
218
                        isOnline,
×
219
                        ")"
×
220
                    );
×
221
                    return resolve();
×
222
                }
×
223
                debugLog(
1✔
224
                    "RegisterServerManager#_registerServer sendRegisterServerRequest " + "has failed (isOnline",
1✔
225
                    isOnline,
1✔
226
                    ")"
1✔
227
                );
1✔
228
                reject(err1);
1✔
229
            });
1✔
230
        });
166✔
231
    });
166✔
232
}
165✔
233

2✔
234
export interface IPartialServer {
2✔
235
    serverCertificateManager: ICertificateStore;
2✔
236
    certificateFile: string;
2✔
237
    privateKeyFile: string;
2✔
238
    serverType: ApplicationType;
2✔
239
    serverInfo: {
2✔
240
        applicationUri: UAString;
2✔
241
        applicationName: LocalizedTextOptions;
2✔
242
        productUri: UAString;
2✔
243
    };
2✔
244
    capabilitiesForMDNS: string[];
2✔
245
    getDiscoveryUrls(): string[];
2✔
246
    getCertificate(): Buffer;
2✔
247
}
2✔
248
export interface RegisterServerManagerOptions {
2✔
249
    server: IPartialServer;
2✔
250
    discoveryServerEndpointUrl: string;
2✔
251
}
2✔
252

2✔
253
let g_registeringClientCounter = 0;
2✔
254
/**
2✔
255
 * RegisterServerManager is responsible to Register an opcua server on a LDS or LDS-ME server
2✔
256
 * This class takes in charge :
2✔
257
 * - the initial registration of a server
2✔
258
 * - the regular registration renewal (every 8 minutes or so ...)
2✔
259
 * - dealing with cases where LDS is not up and running when server starts.
2✔
260
 * ( in this case the connection will be continuously attempted using the infinite
2✔
261
 * back-off strategy
2✔
262
 * - the un-registration of the server ( during shutdown for instance)
2✔
263
 *
2✔
264
 * Events:
2✔
265
 *
2✔
266
 * Emitted when the server is trying to registered the LDS
2✔
267
 * but when the connection to the lds has failed
2✔
268
 * serverRegistrationPending is sent when the backoff signal of the
2✔
269
 * connection process is rained
2✔
270
 * @event serverRegistrationPending
2✔
271
 *
2✔
272
 * emitted when the server is successfully registered to the LDS
2✔
273
 * @event serverRegistered
2✔
274
 *
2✔
275
 * emitted when the server has successfully renewed its registration to the LDS
2✔
276
 * @event serverRegistrationRenewed
2✔
277
 *
2✔
278
 * emitted when the server is successfully unregistered to the LDS
2✔
279
 * ( for instance during shutdown)
2✔
280
 * @event serverUnregistered
2✔
281
 *
2✔
282
 *
2✔
283
 * (LDS => Local Discovery Server)
2✔
284
 */
2✔
285
export class RegisterServerManager extends EventEmitter implements IRegisterServerManager {
2✔
286
    public discoveryServerEndpointUrl: string;
88✔
287
    public timeout: number;
88✔
288

88✔
289
    private server: IPartialServer | null;
88✔
290
    private _registrationTimerId: NodeJS.Timeout | null;
88✔
291
    private state: RegisterServerManagerStatus = RegisterServerManagerStatus.INACTIVE;
88✔
292
    private _registration_client: OPCUAClientBase | null = null;
88✔
293
    private selectedEndpoint?: EndpointDescription;
88✔
294
    private _serverEndpoints: EndpointDescription[] = [];
88✔
295
    private _backgroundProcessPromise: Promise<void> | null = null;
88✔
296

88✔
297
    getState(): RegisterServerManagerStatus {
88✔
298
        return this.state;
1,313✔
299
    }
1,313✔
300
    constructor(options: RegisterServerManagerOptions) {
88✔
301
        super();
88✔
302

88✔
303
        this.server = options.server;
88✔
304
        this.#_setState(RegisterServerManagerStatus.INACTIVE);
88✔
305
        this.timeout = g_DefaultRegistrationServerTimeout;
88✔
306
        this.discoveryServerEndpointUrl = options.discoveryServerEndpointUrl || "opc.tcp://localhost:4840";
88✔
307

88✔
308
        assert(typeof this.discoveryServerEndpointUrl === "string");
88✔
309
        this._registrationTimerId = null;
88✔
310
    }
88✔
311

88✔
312
    public dispose(): void {
88✔
313
        this.server = null;
88✔
314
        debugLog("RegisterServerManager#dispose", this.state.toString());
88✔
315

88✔
316
        if (this._registrationTimerId) {
88✔
317
            clearTimeout(this._registrationTimerId);
×
318
            this._registrationTimerId = null;
×
319
        }
×
320

88✔
321
        assert(this._registrationTimerId === null, "stop has not been called");
88✔
322
        this.removeAllListeners();
88✔
323
    }
88✔
324

88✔
325
    #_emitEvent(eventName: string): void {
88✔
326
        setImmediate(() => {
270✔
327
            debugLog("emiting event", eventName);
270✔
328
            this.emit(eventName);
270✔
329
        });
270✔
330
    }
270✔
331

88✔
332
    #_setState(status: RegisterServerManagerStatus): void {
88✔
333
        const previousState = this.state || RegisterServerManagerStatus.INACTIVE;
769✔
334
        debugLog(
769✔
335
            "RegisterServerManager#setState : ",
769✔
336
            RegisterServerManagerStatus[previousState],
769✔
337
            " => ",
769✔
338
            RegisterServerManagerStatus[status]
769✔
339
        );
769✔
340
        this.state = status;
769✔
341
    }
769✔
342

88✔
343
    /**
88✔
344
     * The start method initiates the registration process in a non-blocking way.
88✔
345
     * It immediately returns while the actual work is performed in a background task.
88✔
346
     */
88✔
347
    public async start(): Promise<void> {
88✔
348
        debugLog("RegisterServerManager#start");
88✔
349
        if (this.state !== RegisterServerManagerStatus.INACTIVE) {
88✔
350
            throw new Error(`RegisterServer process already started: ${RegisterServerManagerStatus[this.state]}`);
×
351
        }
×
352
        this.discoveryServerEndpointUrl = resolveFullyQualifiedDomainName(this.discoveryServerEndpointUrl);
88✔
353

88✔
354
        // Immediately set the state to INITIALIZING and run the process in the background.
88✔
355
        this.#_setState(RegisterServerManagerStatus.INITIALIZING);
88✔
356

88✔
357
        // Run the registration process in the background.
88✔
358
        // Store the promise so stop() can await full exit.
88✔
359
        this._backgroundProcessPromise = this.#_runRegistrationProcess().catch((err) => {
88✔
360
            warningLog("Synchronous error in #_runRegistrationProcess: ", err?.message);
×
361
        });
88✔
362
    }
88✔
363

88✔
364
    /**
88✔
365
     * Private method to run the entire registration process in the background.
88✔
366
     * It handles the state machine transitions and re-connection logic.
88✔
367
     * @private
88✔
368
     */
88✔
369
    async #_runRegistrationProcess(): Promise<void> {
88✔
370
        while (this.getState() !== RegisterServerManagerStatus.WAITING && !this.#_isTerminating()) {
88✔
371
            debugLog(
88✔
372
                "RegisterServerManager#_runRegistrationProcess - state =",
88✔
373
                RegisterServerManagerStatus[this.state],
88✔
374
                "isTerminating =",
88✔
375
                this.#_isTerminating()
88✔
376
            );
88✔
377
            try {
88✔
378
                if (this.getState() === RegisterServerManagerStatus.INACTIVE) {
88✔
379
                    this.#_setState(RegisterServerManagerStatus.INITIALIZING);
×
380
                }
×
381
                await this.#_establish_initial_connection();
88✔
382

78✔
383
                if (this.getState() !== RegisterServerManagerStatus.INITIALIZING) {
88✔
384
                    debugLog("RegisterServerManager#_runRegistrationProcess: aborted during initialization");
1✔
385
                    return;
1✔
386
                }
1✔
387

77✔
388
                this.#_setState(RegisterServerManagerStatus.INITIALIZED);
77✔
389
                this.#_setState(RegisterServerManagerStatus.REGISTERING);
77✔
390
                this.#_emitEvent("serverRegistrationPending");
77✔
391

77✔
392
                await this.#_registerOrUnregisterServer(true);
77✔
393

77✔
394
                if (this.getState() !== RegisterServerManagerStatus.REGISTERING) {
88✔
395
                    debugLog("RegisterServerManager#_runRegistrationProcess: aborted during registration");
3✔
396
                    return;
3✔
397
                }
3✔
398

74✔
399
                this.#_setState(RegisterServerManagerStatus.REGISTERED);
74✔
400
                this.#_emitEvent("serverRegistered");
74✔
401
                this.#_setState(RegisterServerManagerStatus.WAITING);
74✔
402
                this.#_trigger_next();
74✔
403
                return;
74✔
404
            } catch (err) {
88✔
405
                debugLog("RegisterServerManager#_runRegistrationProcess - operation failed!", (err as Error).message);
10✔
406
                if (!this.#_isTerminating()) {
10✔
407
                    this.#_setState(RegisterServerManagerStatus.INACTIVE);
×
408
                    this.#_emitEvent("serverRegistrationFailure");
×
409
                    // interruptible pause: check for shutdown every 100ms
×
410
                    const delay = Math.min(5000, this.timeout);
×
411
                    for (let elapsed = 0; elapsed < delay && !this.#_isTerminating(); elapsed += 100) {
×
412
                        await pause(100);
×
413
                    }
×
414
                }
×
415
            }
10✔
416
        }
88✔
417
    }
88✔
418
    #_isTerminating(): boolean {
88✔
419
        return (
322✔
420
            this.getState() === RegisterServerManagerStatus.UNREGISTERING ||
322✔
421
            this.getState() === RegisterServerManagerStatus.DISPOSING
301✔
422
        );
322✔
423
    }
322✔
424

88✔
425
    /**
88✔
426
     * Establish the initial connection with the Discovery Server to extract best endpoint to use.
88✔
427
     * @private
88✔
428
     */
88✔
429
    async #_establish_initial_connection(): Promise<void> {
88✔
430
        if (!this.server) {
88✔
431
            this.#_setState(RegisterServerManagerStatus.DISPOSING);
×
432
            return;
×
433
        }
×
434
        if (this.state !== RegisterServerManagerStatus.INITIALIZING) {
88✔
435
            debugLog("RegisterServerManager#_establish_initial_connection: aborting due to state change");
×
436
            return;
×
437
        }
×
438
        debugLog("RegisterServerManager#_establish_initial_connection");
88✔
439

88✔
440
        assert(!this._registration_client);
88✔
441
        assert(typeof this.discoveryServerEndpointUrl === "string");
88✔
442
        assert(this.state === RegisterServerManagerStatus.INITIALIZING);
88✔
443

88✔
444
        this.selectedEndpoint = undefined;
88✔
445

88✔
446
        const applicationName = coerceLocalizedText(this.server.serverInfo.applicationName)?.text || undefined;
88✔
447
        this.server.serverCertificateManager.referenceCounter++;
88✔
448

88✔
449
        const server = this.server;
88✔
450
        const prefix = `Client-${g_registeringClientCounter++}`;
88✔
451
        const action = "initializing";
88✔
452
        const ldsInfo = this.discoveryServerEndpointUrl;
88✔
453
        const serverAppUri = this.server?.serverInfo.applicationUri ?? "";
88✔
454
        const clientName = `${prefix} for server ${serverAppUri} to LDS ${ldsInfo} for ${action}`;
88✔
455

88✔
456
        const registrationClient = OPCUAClientBase.create({
88✔
457
            clientName,
88✔
458
            applicationName,
88✔
459
            applicationUri: server.serverInfo.applicationUri ?? "",
88✔
460
            connectionStrategy: infinite_connectivity_strategy,
88✔
461
            clientCertificateManager: server.serverCertificateManager,
88✔
462
            certificateFile: server.certificateFile,
88✔
463
            privateKeyFile: server.privateKeyFile
88✔
464
        }) as ClientBaseEx;
88✔
465

88✔
466
        registrationClient.on("backoff", (nbRetry: number, delay: number) => {
88✔
467
            if (this.state !== RegisterServerManagerStatus.INITIALIZING) return; // Ignore event if state has changed
4✔
468
            debugLog("RegisterServerManager - received backoff");
4✔
469
            debugLog(
4✔
470
                registrationClient.clientName,
4✔
471
                chalk.bgWhite.cyan("contacting discovery server backoff "),
4✔
472
                this.discoveryServerEndpointUrl,
4✔
473
                " attempt #",
4✔
474
                nbRetry,
4✔
475
                " retrying in ",
4✔
476
                delay / 1000.0,
4✔
477
                " seconds"
4✔
478
            );
4✔
479
            this.#_emitEvent("serverRegistrationPending");
4✔
480
        });
88✔
481

88✔
482
        // Keep track of the client to allow cancellation during connect()
88✔
483
        this._registration_client = registrationClient;
88✔
484

88✔
485
        try {
88✔
486
            await registrationClient.connect(this.discoveryServerEndpointUrl);
88✔
487

78✔
488
            if (!this._registration_client) return;
78✔
489

78✔
490
            // Re-check state after the long-running connect operation
78✔
491
            if (this.state !== RegisterServerManagerStatus.INITIALIZING) {
88✔
492
                debugLog("RegisterServerManager#_establish_initial_connection: aborted after connection");
×
493
                return;
×
494
            }
×
495

78✔
496
            const endpoints = await registrationClient.getEndpoints();
78✔
497
            if (!endpoints || endpoints.length === 0) {
88✔
498
                throw new Error("Cannot retrieve endpoints from discovery server");
×
499
            }
×
500
            const endpoint = findSecureEndpoint(endpoints);
78✔
501
            if (!endpoint) {
88✔
502
                throw new Error("Cannot find Secure endpoint");
×
503
            }
×
504

78✔
505
            this.selectedEndpoint = endpoint.serverCertificate ? endpoint : undefined;
88✔
506

88✔
507
            this._serverEndpoints = registrationClient._serverEndpoints;
88✔
508
        } finally {
88✔
509
            if (this._registration_client) {
88✔
510
                const tmp = this._registration_client;
78✔
511
                this._registration_client = null;
78✔
512
                try {
78✔
513
                    await tmp.disconnect();
78✔
514
                } catch (err) {
78✔
515
                    warningLog("RegisterServerManager#_establish_initial_connection: error disconnecting client", err);
×
516
                }
×
517
            }
78✔
518
            server.serverCertificateManager.referenceCounter--;
88✔
519
        }
88✔
520
    }
88✔
521

88✔
522
    #_trigger_next(): void {
88✔
523
        assert(!this._registrationTimerId);
92✔
524
        assert(this.state === RegisterServerManagerStatus.WAITING);
92✔
525

92✔
526
        debugLog(
92✔
527
            "RegisterServerManager#_trigger_next " + ": installing timeout to perform registerServer renewal (timeout =",
92✔
528
            this.timeout,
92✔
529
            ")"
92✔
530
        );
92✔
531
        if (this._registrationTimerId) clearTimeout(this._registrationTimerId);
92✔
532
        this._registrationTimerId = setTimeout(() => {
92✔
533
            if (!this._registrationTimerId) {
19✔
534
                debugLog("RegisterServerManager => cancelling re registration");
×
535
                return;
×
536
            }
×
537
            this._registrationTimerId = null;
19✔
538

19✔
539
            if (this.#_isTerminating()) {
19✔
540
                debugLog("RegisterServerManager#_trigger_next : cancelling re registration");
×
541
                return;
×
542
            }
×
543
            debugLog("RegisterServerManager#_trigger_next : renewing RegisterServer");
19✔
544

19✔
545
            const after_register = (err?: Error) => {
19✔
546
                if (!this.#_isTerminating()) {
19✔
547
                    debugLog("RegisterServerManager#_trigger_next : renewed ! err:", err?.message);
18✔
548
                    this.#_setState(RegisterServerManagerStatus.WAITING);
18✔
549
                    this.#_emitEvent("serverRegistrationRenewed");
18✔
550
                    this.#_trigger_next();
18✔
551
                }
18✔
552
            };
19✔
553

19✔
554
            // State transition before the call
19✔
555
            this.#_setState(RegisterServerManagerStatus.REGISTERING);
19✔
556
            this.#_emitEvent("serverRegistrationPending");
19✔
557

19✔
558
            this.#_registerOrUnregisterServer(/*isOnline=*/ true)
19✔
559
                .then(() => after_register())
19✔
560
                .catch((err) => after_register(err));
19✔
561
        }, this.timeout);
92✔
562
    }
92✔
563

88✔
564
    public async stop(): Promise<void> {
88✔
565
        debugLog("RegisterServerManager#stop");
88✔
566
        if (this.#_isTerminating()) {
88✔
567
            debugLog("Already stopping  or stopped...");
×
568
            return;
×
569
        }
×
570

88✔
571
        // make sure we don't have any timer running
88✔
572
        // so a registration renewal won't happen while we are stopping
88✔
573
        if (this._registrationTimerId) {
88✔
574
            clearTimeout(this._registrationTimerId);
73✔
575
            this._registrationTimerId = null;
73✔
576
        }
73✔
577

88✔
578
        // Immediately set state to signal a stop
88✔
579
        this.#_setState(RegisterServerManagerStatus.UNREGISTERING);
88✔
580

88✔
581
        // Cancel any pending client connections
88✔
582
        await this.#_cancel_pending_client_if_any();
88✔
583

88✔
584
        // Wait for the background registration loop to fully exit
88✔
585
        // before proceeding with unregistration.
88✔
586
        if (this._backgroundProcessPromise) {
88✔
587
            await this._backgroundProcessPromise;
88✔
588
            this._backgroundProcessPromise = null;
88✔
589
        }
88✔
590

88✔
591
        if (this.selectedEndpoint) {
88✔
592
            try {
78✔
593
                await this.#_registerOrUnregisterServer(/* isOnline= */ false);
78✔
594
                this.#_setState(RegisterServerManagerStatus.UNREGISTERED);
78✔
595
                this.#_emitEvent("serverUnregistered");
78✔
596
            } catch (err) {
78✔
597
                warningLog(err);
×
598
                warningLog("RegisterServerManager#stop: Unregistration failed.", (err as Error).message);
×
599
            }
×
600
        }
78✔
601

88✔
602
        // Final state transition to INACTIVE
88✔
603
        this.#_setState(RegisterServerManagerStatus.DISPOSING);
88✔
604
    }
88✔
605

88✔
606
    /**
88✔
607
     * Handles the actual registration/unregistration request.
88✔
608
     * It is designed to be interruptible by checking the state.
88✔
609
     * @param isOnline - true for registration, false for unregistration
88✔
610
     * @private
88✔
611
     */
88✔
612
    async #_registerOrUnregisterServer(isOnline: boolean): Promise<void> {
88✔
613
        const expectedState = isOnline ? RegisterServerManagerStatus.REGISTERING : RegisterServerManagerStatus.UNREGISTERING;
174✔
614
        if (this.getState() !== expectedState) {
174✔
615
            debugLog("RegisterServerManager#_registerServer: aborting due to state change");
×
616
            return;
×
617
        }
×
618

174✔
619
        debugLog("RegisterServerManager#_registerServer isOnline:", isOnline);
174✔
620

174✔
621
        assert(this.selectedEndpoint, "must have a selected endpoint");
174✔
622
        assert(this.server?.serverType !== undefined, " must have a valid server Type");
174✔
623

174✔
624
        if (!this.server) {
174✔
625
            throw new Error("RegisterServerManager: server is not set");
×
626
        }
×
627
        const server = this.server;
174✔
628
        const selectedEndpoint = this.selectedEndpoint;
174✔
629
        if (!selectedEndpoint) {
174✔
630
            warningLog("Warning: cannot register server - no endpoint available");
×
631
            // Do not rethrow here, let the caller handle it.
×
632
            return;
×
633
        }
×
634

174✔
635
        server.serverCertificateManager.referenceCounter++;
174✔
636
        const applicationName: string | undefined = coerceLocalizedText(server.serverInfo.applicationName)?.text || undefined;
174✔
637

174✔
638
        const prefix = `Client-${g_registeringClientCounter++}`;
174✔
639
        const action = isOnline ? "registering" : "unregistering";
174✔
640
        const ldsInfo = this.discoveryServerEndpointUrl;
174✔
641
        const serverAppUri = server.serverInfo.applicationUri ?? "";
174✔
642
        const clientName = `${prefix} for server ${serverAppUri} to LDS ${ldsInfo} for ${action}`;
174✔
643

174✔
644
        const client = OPCUAClientBase.create({
174✔
645
            clientName,
174✔
646
            applicationName,
174✔
647
            applicationUri: server.serverInfo.applicationUri ?? "",
174✔
648
            connectionStrategy: no_retry_connectivity_strategy,
174✔
649
            clientCertificateManager: server.serverCertificateManager,
174✔
650

174✔
651
            securityMode: selectedEndpoint.securityMode,
174✔
652
            securityPolicy: coerceSecurityPolicy(selectedEndpoint.securityPolicyUri),
174✔
653
            serverCertificate: selectedEndpoint.serverCertificate,
174✔
654
            certificateFile: server.certificateFile,
174✔
655
            privateKeyFile: server.privateKeyFile
174✔
656
        }) as ClientBaseEx;
174✔
657
        client.on("backoff", (nbRetry, delay) => {
174✔
658
            debugLog(client.clientCertificateManager, "backoff trying to connect to the LDS has failed", nbRetry, delay);
5✔
659
        });
174✔
660

174✔
661
        this._registration_client = client;
174✔
662

174✔
663
        const endpointUrl = selectedEndpoint.endpointUrl;
174✔
664
        if (!endpointUrl) {
174✔
665
            throw new Error("selectedEndpoint.endpointUrl is missing — cannot connect to LDS");
×
666
        }
×
667
        debugLog("lds endpoint uri : ", endpointUrl);
174✔
668

174✔
669
        const state = isOnline ? "RegisterServer" : "UnRegisterServer";
174✔
670
        try {
174✔
671
            await client.connect(endpointUrl);
174✔
672

166✔
673
            // Check state again after connection is established
166✔
674
            if (this.getState() === expectedState) {
166✔
675
                try {
166✔
676
                    await sendRegisterServerRequest(server, client as ClientBaseEx, isOnline);
166✔
677
                } catch (err) {
166✔
678
                    if (this.getState() !== expectedState) {
1✔
679
                        warningLog(
×
680
                            `${state} '${this.server?.serverInfo.applicationUri}' to the LDS has failed during secure connection to the LDS server`
×
681
                        );
×
682
                        warningLog(chalk.red("  Error message:"), (err as Error).message); // Do not rethrow here, let the caller
×
683
                    }
×
684
                }
1✔
685
            } else {
174✔
686
                debugLog("RegisterServerManager#_registerServer: aborted ");
×
687
            }
×
688
        } catch (err) {
174✔
689
            if (this.getState() !== expectedState) {
8✔
690
                warningLog(
4✔
691
                    `${state} '${this.server?.serverInfo.applicationUri}' cannot connect to LDS at endpoint ${client.clientName}, ${selectedEndpoint.endpointUrl} :`
4✔
692
                );
4✔
693
                warningLog(chalk.red("  Error message:"), (err as Error).message);
4✔
694
            }
4✔
695
        } finally {
174✔
696
            if (this._registration_client) {
174✔
697
                const tmp = this._registration_client;
170✔
698
                this._registration_client = null;
170✔
699
                await tmp.disconnect();
170✔
700
            }
170✔
701
            server.serverCertificateManager.referenceCounter--;
174✔
702
        }
174✔
703
    }
174✔
704

88✔
705
    /**
88✔
706
     * Cancels any pending client connections.
88✔
707
     * This is crucial for a clean shutdown.
88✔
708
     * @private
88✔
709
     */
88✔
710
    async #_cancel_pending_client_if_any(): Promise<void> {
88✔
711
        debugLog("RegisterServerManager#_cancel_pending_client_if_any");
102✔
712
        if (this._registration_client) {
102✔
713
            const client = this._registration_client;
14✔
714
            this._registration_client = null;
14✔
715
            debugLog("RegisterServerManager#_cancel_pending_client_if_any " + "=> wee need to disconnect_registration_client");
14✔
716
            try {
14✔
717
                await client.disconnect();
14✔
718
            } catch (err) {
14✔
719
                warningLog("Error disconnecting registration client:", (err as Error).message);
×
720
            }
×
721
            await this.#_cancel_pending_client_if_any(); // Recursive call to ensure all are handled
14✔
722
        } else {
102✔
723
            debugLog("RegisterServerManager#_cancel_pending_client_if_any : done (nothing to do)");
88✔
724
        }
88✔
725
    }
102✔
726
}
88✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc