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

node-opcua / node-opcua / 23974043205

04 Apr 2026 07:17AM UTC coverage: 92.589% (+0.01%) from 92.576%
23974043205

push

github

erossignon
chore: fix Mocha.Suite.settimeout misused

18408 of 21832 branches covered (84.32%)

161708 of 174651 relevant lines covered (92.59%)

461089.77 hits per line

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

91.5
/packages/node-opcua-server/source/register_server_manager.ts
1
/**
2✔
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 type { OPCUACertificateManager } from "node-opcua-certificate-manager";
2✔
11
import { coerceLocalizedText, type LocalizedTextOptions, OPCUAClientBase, type ResponseCallback } from "node-opcua-client";
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 {
849✔
37
    switch (securityPolicy) {
849✔
38
        case SecurityPolicy.None:
849✔
39
            return 0;
1✔
40
        case SecurityPolicy.Basic128:
849✔
41
            return 1;
1✔
42
        case SecurityPolicy.Basic128Rsa15:
849✔
43
            return 2;
1✔
44
        case SecurityPolicy.Basic192:
849✔
45
            return 3;
1✔
46
        case SecurityPolicy.Basic192Rsa15:
849✔
47
            return 4;
1✔
48
        case SecurityPolicy.Basic256:
849✔
49
            return 5;
1✔
50
        case SecurityPolicy.Basic256Rsa15:
849✔
51
            return 6;
1✔
52
        case SecurityPolicy.Aes128_Sha256_RsaOaep:
849✔
53
            return 7;
319✔
54
        case SecurityPolicy.Basic256Sha256:
849✔
55
            return 8;
213✔
56
        case SecurityPolicy.Aes256_Sha256_RsaPss:
849✔
57
            return 9;
319✔
58
        default:
849✔
59
            return 0;
1✔
60
    }
849✔
61
}
849✔
62

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

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

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

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

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

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

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

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

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

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

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

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

223✔
166
    return new RegisterServer2Request({
223✔
167
        discoveryConfiguration: [
223✔
168
            new MdnsDiscoveryConfiguration({
223✔
169
                mdnsServerName: serverB.serverInfo.applicationUri,
223✔
170
                serverCapabilities: serverB.capabilitiesForMDNS
223✔
171
            })
223✔
172
        ],
223✔
173
        server
223✔
174
    });
223✔
175
}
223✔
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) {
223✔
200
    // try to send a RegisterServer2Request
223✔
201
    const request = constructRegisterServer2Request(server, isOnline);
223✔
202

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

2✔
234
export interface IPartialServer {
2✔
235
    serverCertificateManager: OPCUACertificateManager;
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 {
118✔
286
    public discoveryServerEndpointUrl: string;
117✔
287
    public timeout: number;
117✔
288

117✔
289
    private server: IPartialServer | null;
117✔
290
    private _registrationTimerId: NodeJS.Timeout | null;
117✔
291
    private state: RegisterServerManagerStatus = RegisterServerManagerStatus.INACTIVE;
117✔
292
    private _registration_client: OPCUAClientBase | null = null;
117✔
293
    private selectedEndpoint?: EndpointDescription;
117✔
294
    private _serverEndpoints: EndpointDescription[] = [];
117✔
295

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

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

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

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

117✔
315
        if (this._registrationTimerId) {
117!
316
            clearTimeout(this._registrationTimerId);
×
317
            this._registrationTimerId = null;
×
318
        }
×
319

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

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

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

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

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

117✔
356
        // This method is called without await to ensure it is non-blocking.
117✔
357
        // The catch block handles any synchronous errors.
117✔
358
        this.#_runRegistrationProcess().catch((err) => {
117✔
359
            warningLog("Synchronous error in #_runRegistrationProcess: ", err?.message);
×
360
        });
117✔
361
    }
117✔
362

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

106✔
382
                if (this.getState() !== RegisterServerManagerStatus.INITIALIZING) {
117!
383
                    debugLog("RegisterServerManager#_runRegistrationProcess: aborted during initialization");
×
384
                    return;
×
385
                }
×
386

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

106✔
391
                await this.#_registerOrUnregisterServer(true);
106✔
392

106✔
393
                if (this.getState() !== RegisterServerManagerStatus.REGISTERING) {
117✔
394
                    debugLog("RegisterServerManager#_runRegistrationProcess: aborted during registration");
5✔
395
                    return;
5✔
396
                }
5✔
397

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

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

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

117✔
443
        this.selectedEndpoint = undefined;
117✔
444

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

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

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

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

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

117✔
484
        try {
117✔
485
            await registrationClient.connect(this.discoveryServerEndpointUrl);
117✔
486

106✔
487
            if (!this._registration_client) return;
106!
488

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

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

106✔
504
            this.selectedEndpoint = endpoint.serverCertificate ? endpoint : undefined;
117!
505

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

117✔
521
    #_trigger_next(): void {
117✔
522
        assert(!this._registrationTimerId);
119✔
523
        assert(this.state === RegisterServerManagerStatus.WAITING);
119✔
524

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

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

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

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

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

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

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

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

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

117✔
583
        if (this.selectedEndpoint) {
117✔
584
            try {
106✔
585
                await this.#_registerOrUnregisterServer(/* isOnline= */ false);
106✔
586
                this.#_setState(RegisterServerManagerStatus.UNREGISTERED);
106✔
587
                this.#_emitEvent("serverUnregistered");
106✔
588
            } catch (err) {
106!
589
                warningLog(err);
×
590
                warningLog("RegisterServerManager#stop: Unregistration failed.", (err as Error).message);
×
591
            }
×
592
        }
106✔
593

117✔
594
        // Final state transition to INACTIVE
117✔
595
        this.#_setState(RegisterServerManagerStatus.DISPOSING);
117✔
596
    }
117✔
597

117✔
598
    /**
117✔
599
     * Handles the actual registration/unregistration request.
117✔
600
     * It is designed to be interruptible by checking the state.
117✔
601
     * @param isOnline - true for registration, false for unregistration
117✔
602
     * @private
117✔
603
     */
117✔
604
    async #_registerOrUnregisterServer(isOnline: boolean): Promise<void> {
117✔
605
        const expectedState = isOnline ? RegisterServerManagerStatus.REGISTERING : RegisterServerManagerStatus.UNREGISTERING;
231✔
606
        if (this.getState() !== expectedState) {
231!
607
            debugLog("RegisterServerManager#_registerServer: aborting due to state change");
×
608
            return;
×
609
        }
×
610

231✔
611
        debugLog("RegisterServerManager#_registerServer isOnline:", isOnline);
231✔
612

231✔
613
        assert(this.selectedEndpoint, "must have a selected endpoint");
231✔
614
        assert(this.server?.serverType !== undefined, " must have a valid server Type");
231✔
615

231✔
616
        if (!this.server) {
231!
617
            throw new Error("RegisterServerManager: server is not set");
×
618
        }
×
619
        const server = this.server;
231✔
620
        const selectedEndpoint = this.selectedEndpoint;
231✔
621
        if (!selectedEndpoint) {
231!
622
            warningLog("Warning: cannot register server - no endpoint available");
×
623
            // Do not rethrow here, let the caller handle it.
×
624
            return;
×
625
        }
×
626

231✔
627
        server.serverCertificateManager.referenceCounter++;
231✔
628
        const applicationName: string | undefined = coerceLocalizedText(server.serverInfo.applicationName)?.text || undefined;
231!
629

231✔
630
        const prefix = `Client-${g_registeringClientCounter++}`;
231✔
631
        const action = isOnline ? "registering" : "unregistering";
231✔
632
        const ldsInfo = this.discoveryServerEndpointUrl;
231✔
633
        const serverAppUri = server.serverInfo.applicationUri ?? "";
231!
634
        const clientName = `${prefix} for server ${serverAppUri} to LDS ${ldsInfo} for ${action}`;
231✔
635

231✔
636
        const client = OPCUAClientBase.create({
231✔
637
            clientName,
231✔
638
            applicationName,
231✔
639
            applicationUri: server.serverInfo.applicationUri ?? "",
231!
640
            connectionStrategy: no_retry_connectivity_strategy,
231✔
641
            clientCertificateManager: server.serverCertificateManager,
231✔
642

231✔
643
            securityMode: selectedEndpoint.securityMode,
231✔
644
            securityPolicy: coerceSecurityPolicy(selectedEndpoint.securityPolicyUri),
231✔
645
            serverCertificate: selectedEndpoint.serverCertificate,
231✔
646
            certificateFile: server.certificateFile,
231✔
647
            privateKeyFile: server.privateKeyFile
231✔
648
        }) as ClientBaseEx;
231✔
649
        client.on("backoff", (nbRetry, delay) => {
231✔
650
            debugLog(client.clientCertificateManager, "backoff trying to connect to the LDS has failed", nbRetry, delay);
5✔
651
        });
231✔
652

231✔
653
        this._registration_client = client;
231✔
654

231✔
655
        const endpointUrl = selectedEndpoint.endpointUrl;
231✔
656
        if (!endpointUrl) {
231!
657
            throw new Error("selectedEndpoint.endpointUrl is missing — cannot connect to LDS");
×
658
        }
×
659
        debugLog("lds endpoint uri : ", endpointUrl);
231✔
660

231✔
661
        const state = isOnline ? "RegisterServer" : "UnRegisterServer";
231✔
662
        try {
231✔
663
            await client.connect(endpointUrl);
231✔
664

222✔
665
            // Check state again after connection is established
222✔
666
            if (this.getState() === expectedState) {
222✔
667
                try {
222✔
668
                    await sendRegisterServerRequest(server, client as ClientBaseEx, isOnline);
222✔
669
                } catch (err) {
222✔
670
                    if (this.getState() !== expectedState) {
2✔
671
                        warningLog(
1✔
672
                            `${state} '${this.server?.serverInfo.applicationUri}' to the LDS has failed during secure connection to the LDS server`
1✔
673
                        );
1✔
674
                        warningLog(chalk.red("  Error message:"), (err as Error).message); // Do not rethrow here, let the caller
1✔
675
                    }
1✔
676
                }
2✔
677
            } else {
231!
678
                debugLog("RegisterServerManager#_registerServer: aborted ");
×
679
            }
×
680
        } catch (err) {
231✔
681
            if (this.getState() !== expectedState) {
9✔
682
                warningLog(
5✔
683
                    `${state} '${this.server?.serverInfo.applicationUri}' cannot connect to LDS at endpoint ${client.clientName}, ${selectedEndpoint.endpointUrl} :`
5✔
684
                );
5✔
685
                warningLog(chalk.red("  Error message:"), (err as Error).message);
5✔
686
            }
5✔
687
        } finally {
231✔
688
            if (this._registration_client) {
231✔
689
                const tmp = this._registration_client;
225✔
690
                this._registration_client = null;
225✔
691
                await tmp.disconnect();
225✔
692
            }
225✔
693
            server.serverCertificateManager.referenceCounter--;
231✔
694
        }
231✔
695
    }
231✔
696

117✔
697
    /**
117✔
698
     * Cancels any pending client connections.
117✔
699
     * This is crucial for a clean shutdown.
117✔
700
     * @private
117✔
701
     */
117✔
702
    async #_cancel_pending_client_if_any(): Promise<void> {
117✔
703
        debugLog("RegisterServerManager#_cancel_pending_client_if_any");
134✔
704
        if (this._registration_client) {
134✔
705
            const client = this._registration_client;
17✔
706
            this._registration_client = null;
17✔
707
            debugLog("RegisterServerManager#_cancel_pending_client_if_any " + "=> wee need to disconnect_registration_client");
17✔
708
            try {
17✔
709
                await client.disconnect();
17✔
710
            } catch (err) {
17!
711
                warningLog("Error disconnecting registration client:", (err as Error).message);
×
712
            }
×
713
            await this.#_cancel_pending_client_if_any(); // Recursive call to ensure all are handled
17✔
714
        } else {
134✔
715
            debugLog("RegisterServerManager#_cancel_pending_client_if_any : done (nothing to do)");
117✔
716
        }
117✔
717
    }
134✔
718
}
117✔
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