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

node-opcua / node-opcua / 26060433962

18 May 2026 09:05PM UTC coverage: 92.031% (-0.01%) from 92.042%
26060433962

Pull #1516

github

web-flow
Merge 239ef8144 into 5b2631b6f
Pull Request #1516: Chore/client browser unblock e2e

18417 of 21717 branches covered (84.8%)

33 of 44 new or added lines in 1 file covered. (75.0%)

29 existing lines in 4 files now uncovered.

164038 of 178242 relevant lines covered (92.03%)

433806.78 hits per line

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

85.12
/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 {
632✔
37
    switch (securityPolicy) {
632✔
38
        case SecurityPolicy.None:
632!
39
            return 0;
×
40
        case SecurityPolicy.Basic128:
632!
41
            return 1;
×
42
        case SecurityPolicy.Basic128Rsa15:
632!
43
            return 2;
×
44
        case SecurityPolicy.Basic192:
632!
45
            return 3;
×
46
        case SecurityPolicy.Basic192Rsa15:
632!
47
            return 4;
×
48
        case SecurityPolicy.Basic256:
632!
49
            return 5;
×
50
        case SecurityPolicy.Basic256Rsa15:
632!
51
            return 6;
×
52
        case SecurityPolicy.Aes128_Sha256_RsaOaep:
632✔
53
            return 7;
237✔
54
        case SecurityPolicy.Basic256Sha256:
632✔
55
            return 8;
158✔
56
        case SecurityPolicy.Aes256_Sha256_RsaPss:
632✔
57
            return 9;
237✔
58
        default:
632!
59
            return 0;
×
60
    }
632✔
61
}
632✔
62

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

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

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

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

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

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

169✔
113
    const serverUri = info.tbsCertificate.extensions?.subjectAltName?.uniformResourceIdentifier[0];
169✔
114
    // c8 ignore next
169✔
115
    if (serverUri !== server.serverInfo.applicationUri) {
169!
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

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

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

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

169✔
149
        discoveryUrls,
169✔
150
        gatewayServerUri: null,
169✔
151
        isOnline,
169✔
152
        semaphoreFilePath: null
169✔
153
    };
169✔
154
    return s;
169✔
155
}
169✔
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 {
168✔
164
    const server = constructRegisteredServer(serverB, isOnline);
168✔
165

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

168✔
203
    await new Promise<void>((resolve, reject) => {
168✔
204
        client.performMessageTransaction(request, (err: Error | null, _response?: RegisterServer2Response) => {
168✔
205
            if (!err) {
168✔
206
                // RegisterServerResponse
167✔
207
                debugLog("RegisterServerManager#_registerServer sendRegisterServer2Request has succeeded (isOnline", isOnline, ")");
167✔
208
                return resolve();
167✔
209
            }
167✔
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
        });
168✔
231
    });
168✔
232
}
167✔
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;
91✔
287
    public timeout: number;
91✔
288

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

75✔
399
                this.#_setState(RegisterServerManagerStatus.REGISTERED);
75✔
400
                this.#_emitEvent("serverRegistered");
75✔
401
                this.#_setState(RegisterServerManagerStatus.WAITING);
75✔
402
                this.#_trigger_next();
75✔
403
                return;
75✔
404
            } catch (err) {
91✔
405
                debugLog("RegisterServerManager#_runRegistrationProcess - operation failed!", (err as Error).message);
12✔
406
                if (!this.#_isTerminating()) {
12✔
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
            }
12✔
416
        }
91✔
417
    }
91✔
418
    #_isTerminating(): boolean {
91✔
419
        return (
335✔
420
            this.getState() === RegisterServerManagerStatus.UNREGISTERING ||
335✔
421
            this.getState() === RegisterServerManagerStatus.DISPOSING
310✔
422
        );
335✔
423
    }
335✔
424

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

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

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

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

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

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

91✔
466
        registrationClient.on("backoff", (nbRetry: number, delay: number) => {
91✔
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
        });
91✔
481

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

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

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

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

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

79✔
505
            this.selectedEndpoint = endpoint.serverCertificate ? endpoint : undefined;
91✔
506

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

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

93✔
526
        debugLog(
93✔
527
            "RegisterServerManager#_trigger_next " + ": installing timeout to perform registerServer renewal (timeout =",
93✔
528
            this.timeout,
93✔
529
            ")"
93✔
530
        );
93✔
531
        if (this._registrationTimerId) clearTimeout(this._registrationTimerId);
93✔
532
        this._registrationTimerId = setTimeout(() => {
93✔
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);
93✔
562
    }
93✔
563

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

91✔
705
    /**
91✔
706
     * Cancels any pending client connections.
91✔
707
     * This is crucial for a clean shutdown.
91✔
708
     * @private
91✔
709
     */
91✔
710
    async #_cancel_pending_client_if_any(): Promise<void> {
91✔
711
        debugLog("RegisterServerManager#_cancel_pending_client_if_any");
108✔
712
        if (this._registration_client) {
108✔
713
            const client = this._registration_client;
17✔
714
            this._registration_client = null;
17✔
715
            debugLog("RegisterServerManager#_cancel_pending_client_if_any " + "=> wee need to disconnect_registration_client");
17✔
716
            try {
17✔
717
                await client.disconnect();
17✔
718
            } catch (err) {
17✔
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
17✔
722
        } else {
108✔
723
            debugLog("RegisterServerManager#_cancel_pending_client_if_any : done (nothing to do)");
91✔
724
        }
91✔
725
    }
108✔
726
}
91✔
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