• 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

95.36
/packages/node-opcua-server/source/server_session.ts
1
/**
36✔
2
 * @module node-opcua-server
2✔
3
 */
2✔
4
// tslint:disable:no-console
2✔
5

2✔
6
import { randomBytes } from "crypto";
2✔
7
import { EventEmitter } from "events";
2✔
8
import {
2✔
9
    type AddressSpace,
2✔
10
    addElement,
2✔
11
    ContinuationPointManager,
2✔
12
    createExtObjArrayNode,
2✔
13
    type DTSessionDiagnostics,
2✔
14
    type DTSessionSecurityDiagnostics,
2✔
15
    ensureObjectIsSecure,
2✔
16
    type ISessionBase,
2✔
17
    type IUserManager,
2✔
18
    removeElement,
2✔
19
    SessionContext,
2✔
20
    type UADynamicVariableArray,
2✔
21
    type UAObject,
2✔
22
    type UASessionDiagnosticsVariable,
2✔
23
    type UASessionSecurityDiagnostics
2✔
24
} from "node-opcua-address-space";
2✔
25
import type { ISessionContext } from "node-opcua-address-space-base";
2✔
26
import { assert } from "node-opcua-assert";
2✔
27
import { getMinOPCUADate, randomGuid } from "node-opcua-basic-types";
2✔
28
import {
2✔
29
    type SessionDiagnosticsDataType,
2✔
30
    type SessionSecurityDiagnosticsDataType,
2✔
31
    SubscriptionDiagnosticsDataType
2✔
32
} from "node-opcua-common";
2✔
33
import { NodeClass, QualifiedName } from "node-opcua-data-model";
2✔
34
import { checkDebugFlag, make_debugLog, make_errorLog } from "node-opcua-debug";
2✔
35
import { makeNodeId, NodeId, NodeIdType, sameNodeId } from "node-opcua-nodeid";
2✔
36
import { ObjectRegistry } from "node-opcua-object-registry";
2✔
37
import type { IServerSession, IServerSessionBase, ServerSecureChannelLayer } from "node-opcua-secure-channel";
2✔
38
import { type StatusCode, StatusCodes } from "node-opcua-status-code";
2✔
39
import type {
2✔
40
    ApplicationDescription,
2✔
41
    CreateSubscriptionRequestOptions,
2✔
42
    EndpointDescription,
2✔
43
    UserIdentityToken
2✔
44
} from "node-opcua-types";
2✔
45
import { type ISubscriber, type IWatchdogData2, lowerFirstLetter, WatchDog } from "node-opcua-utils";
2✔
46
import type { ServerEngine } from "./server_engine";
2✔
47
import { ServerSidePublishEngine } from "./server_publish_engine";
2✔
48
import { type Subscription, SubscriptionState } from "./server_subscription";
2✔
49

2✔
50
const debugLog = make_debugLog(__filename);
2✔
51
const errorLog = make_errorLog(__filename);
2✔
52
const doDebug = checkDebugFlag(__filename);
2✔
53

2✔
54
const theWatchDog = new WatchDog();
2✔
55

2✔
56
const registeredNodeNameSpace = 9999;
2✔
57

2✔
58
function on_channel_abort(this: ServerSession) {
29✔
59
    debugLog("ON CHANNEL ABORT ON  SESSION!!!");
29✔
60
    /**
29✔
61
     * @event channel_aborted
29✔
62
     */
29✔
63
    this.emit("channel_aborted");
29✔
64
}
29✔
65

2✔
66
interface SessionDiagnosticsDataTypeEx extends SessionDiagnosticsDataType {
2✔
67
    $session: any;
2✔
68
}
2✔
69
interface SessionSecurityDiagnosticsDataTypeEx extends SessionSecurityDiagnosticsDataType {
2✔
70
    $session: any;
2✔
71
}
2✔
72

2✔
73
export type SessionStatus = "new" | "active" | "screwed" | "disposed" | "closed";
2✔
74
/**
2✔
75
 *
2✔
76
 * A Server session object.
2✔
77
 *
2✔
78
 * **from OPCUA Spec 1.02:**
2✔
79
 *
2✔
80
 * * Sessions are created to be independent of the underlying communications connection. Therefore, if a communication
2✔
81
 *   connection fails, the Session is not immediately affected. The exact mechanism to recover from an underlying
2✔
82
 *   communication connection error depends on the SecureChannel mapping as described in Part 6.
2✔
83
 *
2✔
84
 * * Sessions are terminated by the Server automatically if the Client fails to issue a Service request on the Session
2✔
85
 *   within the timeout period negotiated by the Server in the CreateSession Service response. This protects the Server
2✔
86
 *   against Client failures and against situations where a failed underlying connection cannot be re-established.
2✔
87
 *
2✔
88
 * * Clients shall be prepared to submit requests in a timely manner to prevent the Session from closing automatically.
2✔
89
 *
2✔
90
 * * Clients may explicitly terminate Sessions using the CloseSession Service.
2✔
91
 *
2✔
92
 * * When a Session is terminated, all outstanding requests on the Session are aborted and BadSessionClosed StatusCodes
2✔
93
 *   are returned to the Client. In addition, the Server deletes the entry for the Client from its
2✔
94
 *   SessionDiagnosticsArray Variable and notifies any other Clients who were subscribed to this entry.
2✔
95
 *
2✔
96
 */
2✔
97
export class ServerSession extends EventEmitter implements ISubscriber, ISessionBase, IServerSession, IServerSessionBase {
2✔
98
    public static registry = new ObjectRegistry();
36✔
99
    public static maxPublishRequestInQueue = 100;
36✔
100

36✔
101
    public __status: SessionStatus = "new";
36✔
102
    public parent: ServerEngine;
36✔
103
    public authenticationToken: NodeId;
36✔
104
    public nodeId: NodeId;
36✔
105
    public sessionName = "";
36✔
106

36✔
107
    public publishEngine: ServerSidePublishEngine;
36✔
108
    public sessionObject: any;
36✔
109
    public readonly creationDate: Date;
36✔
110
    public sessionTimeout: number;
36✔
111
    public sessionDiagnostics?: UASessionDiagnosticsVariable<DTSessionDiagnostics>;
36✔
112
    public sessionSecurityDiagnostics?: UASessionSecurityDiagnostics<DTSessionSecurityDiagnostics>;
36✔
113
    public subscriptionDiagnosticsArray?: UADynamicVariableArray<SubscriptionDiagnosticsDataType>;
36✔
114
    public channel?: ServerSecureChannelLayer;
36✔
115
    public nonce?: Buffer;
36✔
116
    public userIdentityToken?: UserIdentityToken;
36✔
117
    public clientDescription?: ApplicationDescription;
36✔
118
    public channelId?: number | null;
36✔
119
    public continuationPointManager: ContinuationPointManager;
36✔
120
    public sessionContext: ISessionContext;
36✔
121

36✔
122
    // ISubscriber
36✔
123
    public _watchDog?: WatchDog;
36✔
124
    public _watchDogData?: IWatchdogData2;
36✔
125
    keepAlive: () => void = WatchDog.emptyKeepAlive;
36✔
126

36✔
127
    private _registeredNodesCounter: number;
36✔
128
    private _registeredNodes: any;
36✔
129
    private _registeredNodesInv: any;
36✔
130
    private _cumulatedSubscriptionCount: number;
36✔
131
    private _sessionDiagnostics?: SessionDiagnosticsDataTypeEx;
36✔
132
    private _sessionSecurityDiagnostics?: SessionSecurityDiagnosticsDataTypeEx;
36✔
133

36✔
134
    private channel_abort_event_handler: any;
36✔
135

36✔
136
    constructor(parent: ServerEngine, userManager: IUserManager, sessionTimeout: number) {
36✔
137
        super();
1,072✔
138

1,072✔
139
        this.parent = parent; // SessionEngine
1,072✔
140

1,072✔
141
        ServerSession.registry.register(this);
1,072✔
142

1,072✔
143
        this.sessionContext = new SessionContext({
1,072✔
144
            session: this,
1,072✔
145
            server: { userManager }
1,072✔
146
        });
1,072✔
147

1,072✔
148
        assert(isFinite(sessionTimeout));
1,072✔
149
        assert(sessionTimeout >= 0, " sessionTimeout");
1,072✔
150
        this.sessionTimeout = sessionTimeout;
1,072✔
151

1,072✔
152
        const authenticationTokenBuf = randomBytes(16);
1,072✔
153
        this.authenticationToken = new NodeId(NodeIdType.BYTESTRING, authenticationTokenBuf);
1,072✔
154

1,072✔
155
        // the sessionId
1,072✔
156
        const ownNamespaceIndex = 1; // addressSpace.getOwnNamespace().index;
1,072✔
157
        this.nodeId = new NodeId(NodeIdType.GUID, randomGuid(), ownNamespaceIndex);
1,072✔
158

1,072✔
159
        assert(this.authenticationToken instanceof NodeId);
1,072✔
160
        assert(this.nodeId instanceof NodeId);
1,072✔
161

1,072✔
162
        this._cumulatedSubscriptionCount = 0;
1,072✔
163

1,072✔
164
        this.publishEngine = new ServerSidePublishEngine({
1,072✔
165
            maxPublishRequestInQueue: ServerSession.maxPublishRequestInQueue
1,072✔
166
        });
1,072✔
167

1,072✔
168
        this.publishEngine.setMaxListeners(100);
1,072✔
169

1,072✔
170
        theWatchDog.addSubscriber(this, this.sessionTimeout);
1,072✔
171

1,072✔
172
        this.__status = "new";
1,072✔
173

1,072✔
174
        /**
1,072✔
175
         * the continuation point manager for this session
1,072✔
176
         * @property continuationPointManager
1,072✔
177
         * @type {ContinuationPointManager}
1,072✔
178
         */
1,072✔
179
        this.continuationPointManager = new ContinuationPointManager();
1,072✔
180

1,072✔
181
        /**
1,072✔
182
         * @property creationDate
1,072✔
183
         * @type {Date}
1,072✔
184
         */
1,072✔
185
        this.creationDate = new Date();
1,072✔
186

1,072✔
187
        this._registeredNodesCounter = 0;
1,072✔
188
        this._registeredNodes = {};
1,072✔
189
        this._registeredNodesInv = {};
1,072✔
190
    }
1,072✔
191

36✔
192
    public getSessionId(): NodeId {
36✔
193
        return this.nodeId;
4,285✔
194
    }
4,285✔
195
    public endpoint?: EndpointDescription;
36✔
196
    public getEndpointDescription(): EndpointDescription {
36✔
197
        return this.endpoint!;
1,021✔
198
    }
1,021✔
199

36✔
200
    public dispose(): void {
36✔
201
        debugLog("ServerSession#dispose()");
1,072✔
202

1,072✔
203
        assert(!this.sessionObject, " sessionObject has not been cleared !");
1,072✔
204

1,072✔
205
        this.parent = null as any as ServerEngine;
1,072✔
206
        this.authenticationToken = new NodeId();
1,072✔
207

1,072✔
208
        if (this.publishEngine) {
1,072!
209
            this.publishEngine.dispose();
×
210
            (this as any).publishEngine = null;
×
211
        }
×
212

1,072✔
213
        this._sessionDiagnostics = undefined;
1,072✔
214

1,072✔
215
        this._registeredNodesCounter = 0;
1,072✔
216
        this._registeredNodes = null;
1,072✔
217
        this._registeredNodesInv = null;
1,072✔
218
        (this as any).continuationPointManager = null;
1,072✔
219
        this.removeAllListeners();
1,072✔
220
        this.__status = "disposed";
1,072✔
221

1,072✔
222
        ServerSession.registry.unregister(this);
1,072✔
223
    }
1,072✔
224

36✔
225
    public get clientConnectionTime(): Date {
36✔
226
        return this.creationDate;
2,110✔
227
    }
2,110✔
228

36✔
229
    /**
36✔
230
     * return the number of milisecond since last session transaction occurs from client
36✔
231
     * the first transaction is the creation of the session
36✔
232
     */
36✔
233
    public get clientLastContactTime(): number {
36✔
234
        const lastSeen = this._watchDogData ? this._watchDogData.lastSeen : getMinOPCUADate().getTime();
×
235
        return WatchDog.lastSeenToDuration(lastSeen);
×
236
    }
×
237

36✔
238
    public get status(): SessionStatus {
36✔
239
        return this.__status;
132,988✔
240
    }
132,988✔
241

36✔
242
    public set status(value: SessionStatus) {
36✔
243
        if (value === "active") {
2,074✔
244
            this._createSessionObjectInAddressSpace();
1,001✔
245
        }
1,001✔
246
        if (this.__status !== value) {
2,074✔
247
            this.emit("statusChanged", value);
2,053✔
248
        }
2,053✔
249
        this.__status = value;
2,074✔
250
    }
2,074✔
251

36✔
252
    get addressSpace(): AddressSpace | null {
36✔
253
        if (this.parent && this.parent.addressSpace) {
19,459✔
254
            return this.parent.addressSpace!;
19,456✔
255
        }
19,456✔
256
        return null;
121✔
257
    }
121✔
258

36✔
259
    get currentPublishRequestInQueue(): number {
36✔
260
        return this.publishEngine ? this.publishEngine.pendingPublishRequestCount : 0;
43,678✔
261
    }
43,678✔
262

36✔
263
    public updateClientLastContactTime(): void {
36✔
264
        if (this._sessionDiagnostics && this._sessionDiagnostics.clientLastContactTime) {
43,642✔
265
            const currentTime = new Date();
41,450✔
266
            // do not record all ticks as this may be overwhelming,
41,450✔
267
            if (currentTime.getTime() - 250 >= this._sessionDiagnostics.clientLastContactTime.getTime()) {
41,450✔
268
                this._sessionDiagnostics.clientLastContactTime = currentTime;
1,464✔
269
            }
1,464✔
270
        }
41,450✔
271
    }
43,642✔
272

36✔
273
    /**
36✔
274
     * required for watch dog
36✔
275
     * @param currentTime {DateTime}
36✔
276
     * @private
36✔
277
     */
36✔
278
    public onClientSeen(): void {
36✔
279
        this.updateClientLastContactTime();
43,642✔
280

43,642✔
281
        if (this._sessionDiagnostics) {
43,642✔
282
            // see https://opcfoundation-onlineapplications.org/mantis/view.php?id=4111
41,450✔
283
            assert(Object.hasOwn(this._sessionDiagnostics, "currentMonitoredItemsCount"));
41,450✔
284
            assert(Object.hasOwn(this._sessionDiagnostics, "currentSubscriptionsCount"));
41,450✔
285
            assert(Object.hasOwn(this._sessionDiagnostics, "currentPublishRequestsInQueue"));
41,450✔
286

41,450✔
287
            // note : https://opcfoundation-onlineapplications.org/mantis/view.php?id=4111
41,450✔
288
            // sessionDiagnostics extension object uses a different spelling
41,450✔
289
            // here with an S !!!!
41,450✔
290
            if (this._sessionDiagnostics.currentMonitoredItemsCount !== this.currentMonitoredItemCount) {
41,450✔
291
                this._sessionDiagnostics.currentMonitoredItemsCount = this.currentMonitoredItemCount;
772✔
292
            }
772✔
293
            if (this._sessionDiagnostics.currentSubscriptionsCount !== this.currentSubscriptionCount) {
41,450✔
294
                this._sessionDiagnostics.currentSubscriptionsCount = this.currentSubscriptionCount;
437✔
295
            }
437✔
296
            if (this._sessionDiagnostics.currentPublishRequestsInQueue !== this.currentPublishRequestInQueue) {
41,450✔
297
                this._sessionDiagnostics.currentPublishRequestsInQueue = this.currentPublishRequestInQueue;
2,228✔
298
            }
2,228✔
299
        }
41,450✔
300
    }
43,642✔
301

36✔
302
    public incrementTotalRequestCount(): void {
36✔
303
        if (this._sessionDiagnostics && this._sessionDiagnostics.totalRequestCount) {
41,532✔
304
            this._sessionDiagnostics.totalRequestCount.totalCount += 1;
41,421✔
305
        }
41,421✔
306
    }
41,532✔
307

36✔
308
    public incrementRequestTotalCounter(counterName: string): void {
36✔
309
        if (this._sessionDiagnostics) {
41,476✔
310
            const propName = lowerFirstLetter(counterName + "Count");
41,363✔
311
            // c8 ignore next
41,363✔
312
            if (!Object.hasOwn(this._sessionDiagnostics, propName)) {
41,363✔
313
                errorLog("incrementRequestTotalCounter: cannot find", propName);
×
314
                // xx return;
×
315
            } else {
41,363✔
316
                (this._sessionDiagnostics as any)[propName].totalCount += 1;
41,363✔
317
            }
41,363✔
318
        }
41,363✔
319
    }
41,476✔
320

36✔
321
    public incrementRequestErrorCounter(counterName: string): void {
36✔
322
        this.parent?.incrementRejectedRequestsCount();
57✔
323
        if (this._sessionDiagnostics) {
57✔
324
            const propName = lowerFirstLetter(counterName + "Count");
56✔
325
            // c8 ignore next
56✔
326
            if (!Object.hasOwn(this._sessionDiagnostics, propName)) {
56✔
327
                errorLog("incrementRequestErrorCounter: cannot find", propName);
×
328
                // xx  return;
×
329
            } else {
56✔
330
                (this._sessionDiagnostics as any)[propName].errorCount += 1;
56✔
331
            }
56✔
332
        }
56✔
333
    }
57✔
334

36✔
335
    /**
36✔
336
     * returns rootFolder.objects.server.serverDiagnostics.sessionsDiagnosticsSummary.sessionDiagnosticsArray
36✔
337
     */
36✔
338
    public getSessionDiagnosticsArray(): UADynamicVariableArray<SessionDiagnosticsDataType> {
36✔
339
        const server = this.addressSpace!.rootFolder.objects.server;
1,944✔
340
        return server.serverDiagnostics.sessionsDiagnosticsSummary.sessionDiagnosticsArray as any;
1,944✔
341
    }
1,944✔
342

36✔
343
    /**
36✔
344
     * returns rootFolder.objects.server.serverDiagnostics.sessionsDiagnosticsSummary.sessionSecurityDiagnosticsArray
36✔
345
     */
36✔
346
    public getSessionSecurityDiagnosticsArray(): UADynamicVariableArray<SessionSecurityDiagnosticsDataType> {
36✔
347
        const server = this.addressSpace!.rootFolder.objects.server;
1,904✔
348
        return server.serverDiagnostics.sessionsDiagnosticsSummary.sessionSecurityDiagnosticsArray as any;
1,904✔
349
    }
1,904✔
350

36✔
351
    /**
36✔
352
     * number of active subscriptions
36✔
353
     */
36✔
354
    public get currentSubscriptionCount(): number {
36✔
355
        return this.publishEngine ? this.publishEngine.subscriptionCount : 0;
50,927!
356
    }
50,927✔
357

36✔
358
    /**
36✔
359
     * number of subscriptions ever created since this object is live
36✔
360
     */
36✔
361
    public get cumulatedSubscriptionCount(): number {
36✔
362
        return this._cumulatedSubscriptionCount;
6✔
363
    }
6✔
364

36✔
365
    /**
36✔
366
     * number of monitored items
36✔
367
     */
36✔
368
    public get currentMonitoredItemCount(): number {
36✔
369
        return this.publishEngine ? this.publishEngine.currentMonitoredItemCount : 0;
42,222✔
370
    }
42,222✔
371

36✔
372
    /**
36✔
373
     * retrieve an existing subscription by subscriptionId
36✔
374
     * @param subscriptionId {Number}
36✔
375
     */
36✔
376
    public getSubscription(subscriptionId: number): Subscription | null {
36✔
377
        if (!this.publishEngine) return null;
1,678!
378
        const subscription = this.publishEngine.getSubscriptionById(subscriptionId);
1,678✔
379
        if (subscription && subscription.state === SubscriptionState.CLOSED) {
1,678!
380
            // subscription is CLOSED but has not been notified yet
×
381
            // it should be considered as excluded
×
382
            return null;
×
383
        }
×
384
        assert(
1,678✔
385
            !subscription || subscription.state !== SubscriptionState.CLOSED,
1,678✔
386
            "CLOSED subscription shall not be managed by publish engine anymore"
1,678✔
387
        );
1,678✔
388
        return subscription;
1,678✔
389
    }
1,678✔
390

36✔
391
    /**
36✔
392
     * @param subscriptionId {Number}
36✔
393
     * @return {StatusCode}
36✔
394
     */
36✔
395
    public deleteSubscription(subscriptionId: number): StatusCode {
36✔
396
        const subscription = this.getSubscription(subscriptionId);
453✔
397
        if (!subscription) {
453✔
398
            return StatusCodes.BadSubscriptionIdInvalid;
10✔
399
        }
10✔
400

447✔
401
        // xx this.publishEngine.remove_subscription(subscription);
447✔
402
        subscription.terminate();
447✔
403

443✔
404
        if (this.currentSubscriptionCount === 0) {
453✔
405
            const local_publishEngine = this.publishEngine;
420✔
406
            local_publishEngine.cancelPendingPublishRequest();
420✔
407
        }
420✔
408
        return StatusCodes.Good;
447✔
409
    }
447✔
410

36✔
411
    /**
36✔
412
     * close a ServerSession, this will also delete the subscriptions if the flag is set.
36✔
413
     *
36✔
414
     * Spec extract:
36✔
415
     *
36✔
416
     * If a Client invokes the CloseSession Service then all Subscriptions associated with the Session are also deleted
36✔
417
     * if the deleteSubscriptions flag is set to TRUE. If a Server terminates a Session for any other reason,
36✔
418
     * Subscriptions associated with the Session, are not deleted. Each Subscription has its own lifetime to protect
36✔
419
     * against data loss in the case of a Session termination. In these cases, the Subscription can be reassigned to
36✔
420
     * another Client before its lifetime expires.
36✔
421
     *
36✔
422
     * @param deleteSubscriptions : should we delete subscription ?
36✔
423
     * @param [reason = "CloseSession"] the reason for closing the session
36✔
424
     *         (shall be "Timeout", "Terminated" or "CloseSession")
36✔
425
     *
36✔
426
     */
36✔
427
    public close(deleteSubscriptions: boolean, reason: string): void {
36✔
428
        debugLog(" closing session deleteSubscriptions = ", deleteSubscriptions);
1,072✔
429
        if (this.publishEngine) {
1,072✔
430
            this.publishEngine.onSessionClose();
1,072✔
431
        }
1,072✔
432

1,072✔
433
        theWatchDog.removeSubscriber(this);
1,072✔
434
        // ---------------  delete associated subscriptions ---------------------
1,072✔
435

1,072✔
436
        if (!deleteSubscriptions && this.currentSubscriptionCount !== 0) {
1,072!
437
            // I don't know what to do yet if deleteSubscriptions is false
×
438
            errorLog("TO DO : Closing session without deleting subscription not yet implemented");
×
439
            // to do: Put subscriptions in safe place for future transfer if any
×
440
        }
×
441

1,072✔
442
        this._deleteSubscriptions();
1,072✔
443

1,072✔
444
        assert(this.currentSubscriptionCount === 0);
1,072✔
445

1,072✔
446
        // Post-Conditions
1,072✔
447
        assert(this.currentSubscriptionCount === 0);
1,072✔
448

1,072✔
449
        this.status = "closed";
1,072✔
450

1,072✔
451
        this._detach_channel();
1,072✔
452

1,072✔
453
        /**
1,072✔
454
         * @event session_closed
1,072✔
455
         * @param deleteSubscriptions {Boolean}
1,072✔
456
         * @param reason {String}
1,072✔
457
         */
1,072✔
458
        this.emit("session_closed", this, deleteSubscriptions, reason);
1,072✔
459

1,072✔
460
        // ---------------- shut down publish engine
1,072✔
461
        if (this.publishEngine) {
1,072✔
462
            // remove subscription
1,072✔
463
            this.publishEngine.shutdown();
1,072✔
464

1,072✔
465
            assert(this.publishEngine.subscriptionCount === 0);
1,072✔
466
            this.publishEngine.dispose();
1,072✔
467
            this.publishEngine = null as any as ServerSidePublishEngine;
1,072✔
468
        }
1,072✔
469

1,072✔
470
        this._removeSessionObjectFromAddressSpace();
1,072✔
471

1,072✔
472
        assert(!this.sessionDiagnostics, "ServerSession#_removeSessionObjectFromAddressSpace must be called");
1,072✔
473
        assert(!this.sessionObject, "ServerSession#_removeSessionObjectFromAddressSpace must be called");
1,072✔
474
    }
1,072✔
475

36✔
476
    public registerNode(nodeId: NodeId): NodeId {
36✔
477
        assert(nodeId instanceof NodeId);
2✔
478

2✔
479
        if (nodeId.namespace === 0 && nodeId.identifierType === NodeIdType.NUMERIC) {
2✔
480
            return nodeId;
1✔
481
        }
1✔
482

1✔
483
        const key = nodeId.toString();
1✔
484

1✔
485
        const registeredNode = this._registeredNodes[key];
1✔
486
        if (registeredNode) {
2✔
487
            // already registered
×
488
            return registeredNode;
×
489
        }
×
490

1✔
491
        const node = this.addressSpace!.findNode(nodeId);
1✔
492
        if (!node) {
2✔
493
            return nodeId;
×
494
        }
×
495

1✔
496
        this._registeredNodesCounter += 1;
1✔
497

1✔
498
        const aliasNodeId = makeNodeId(this._registeredNodesCounter, registeredNodeNameSpace);
1✔
499
        this._registeredNodes[key] = aliasNodeId;
1✔
500
        this._registeredNodesInv[aliasNodeId.toString()] = node;
1✔
501
        return aliasNodeId;
1✔
502
    }
1✔
503

36✔
504
    public unRegisterNode(aliasNodeId: NodeId): void {
36✔
505
        assert(aliasNodeId instanceof NodeId);
2✔
506
        if (aliasNodeId.namespace !== registeredNodeNameSpace) {
2✔
507
            return; // not a registered Node
1✔
508
        }
1✔
509

1✔
510
        const node = this._registeredNodesInv[aliasNodeId.toString()];
1✔
511
        if (!node) {
2✔
512
            return;
×
513
        }
×
514
        this._registeredNodesInv[aliasNodeId.toString()] = null;
1✔
515
        this._registeredNodes[node.nodeId.toString()] = null;
1✔
516
    }
1✔
517

36✔
518
    public resolveRegisteredNode(aliasNodeId: NodeId): NodeId {
36✔
519
        if (aliasNodeId.namespace !== registeredNodeNameSpace) {
247,330✔
520
            return aliasNodeId; // not a registered Node
247,328✔
521
        }
247,328✔
522
        const node = this._registeredNodesInv[aliasNodeId.toString()];
2✔
523
        if (!node) {
247,330✔
524
            return aliasNodeId;
×
525
        }
×
526
        return node.nodeId;
2✔
527
    }
2✔
528

36✔
529
    /**
36✔
530
     * true if the underlying channel has been closed or aborted...
36✔
531
     */
36✔
532
    public get aborted(): boolean {
36✔
533
        if (!this.channel) {
×
534
            return true;
×
535
        }
×
536
        return this.channel.aborted;
×
537
    }
×
538

36✔
539
    public createSubscription(parameters: CreateSubscriptionRequestOptions): Subscription {
36✔
540
        const subscription = this.parent._createSubscriptionOnSession(this, parameters);
457✔
541
        assert(!Object.hasOwn(parameters, "id"));
457✔
542
        this.assignSubscription(subscription);
457✔
543
        assert(subscription.$session === this);
457✔
544
        assert(subscription.sessionId instanceof NodeId);
457✔
545
        assert(sameNodeId(subscription.sessionId, this.nodeId));
457✔
546
        return subscription;
457✔
547
    }
457✔
548

36✔
549
    public _attach_channel(channel: ServerSecureChannelLayer): void {
36✔
550
        assert(this.nonce && this.nonce instanceof Buffer);
1,056✔
551
        this.channel = channel;
1,056✔
552
        this.channelId = channel.channelId;
1,056✔
553
        const key = this.authenticationToken.toString();
1,056✔
554
        assert(!Object.hasOwn(channel.sessionTokens, key), "channel has already a session");
1,056✔
555

1,056✔
556
        channel.sessionTokens[key] = this;
1,056✔
557

1,056✔
558
        // when channel is aborting
1,056✔
559
        this.channel_abort_event_handler = on_channel_abort.bind(this);
1,056✔
560
        channel.on("abort", this.channel_abort_event_handler);
1,056✔
561
    }
1,056✔
562

36✔
563
    public _detach_channel(): void {
36✔
564
        const channel = this.channel;
1,093✔
565

1,093✔
566
        // c8 ignore next
1,093✔
567
        if (!channel) {
1,093✔
568
            return;
37✔
569
            // already detached !
37✔
570
            // throw new Error("expecting a valid channel");
37✔
571
        }
37✔
572
        assert(this.nonce && this.nonce instanceof Buffer);
1,093✔
573
        assert(this.authenticationToken);
1,093✔
574
        const key = this.authenticationToken.toString();
1,093✔
575
        assert(Object.hasOwn(channel.sessionTokens, key));
1,093✔
576
        assert(this.channel);
1,093✔
577
        assert(typeof this.channel_abort_event_handler === "function");
1,093✔
578
        channel.removeListener("abort", this.channel_abort_event_handler);
1,093✔
579

1,093✔
580
        delete channel.sessionTokens[key];
1,093✔
581
        this.channel = undefined;
1,093✔
582
        this.channelId = undefined;
1,093✔
583
    }
1,093✔
584

36✔
585
    public _exposeSubscriptionDiagnostics(subscription: Subscription): void {
36✔
586
        debugLog("ServerSession#_exposeSubscriptionDiagnostics");
477✔
587
        assert(subscription.$session === this);
477✔
588
        const subscriptionDiagnosticsArray = this._getSubscriptionDiagnosticsArray();
477✔
589
        const subscriptionDiagnostics = subscription.subscriptionDiagnostics;
477✔
590
        assert(subscriptionDiagnostics.$subscription === subscription);
477✔
591

477✔
592
        if (subscriptionDiagnostics && subscriptionDiagnosticsArray) {
477✔
593
            // subscription.id,"on session", session.nodeId.toString());
432✔
594
            addElement(subscriptionDiagnostics, subscriptionDiagnosticsArray);
432✔
595
        }
432✔
596
    }
477✔
597

36✔
598
    public _unexposeSubscriptionDiagnostics(subscription: Subscription): void {
36✔
599
        const subscriptionDiagnosticsArray = this._getSubscriptionDiagnosticsArray();
477✔
600
        const subscriptionDiagnostics = subscription.subscriptionDiagnostics;
477✔
601
        assert(subscriptionDiagnostics instanceof SubscriptionDiagnosticsDataType);
477✔
602
        if (subscriptionDiagnostics && subscriptionDiagnosticsArray) {
477✔
603
            // subscription.id,"on session", session.nodeId.toString());
432✔
604
            removeElement(subscriptionDiagnosticsArray, (a) => a.subscriptionId === subscription.id);
432✔
605
        }
432✔
606
        debugLog("ServerSession#_unexposeSubscriptionDiagnostics");
477✔
607
    }
477✔
608
    /**
36✔
609
     * used as a callback for the Watchdog
36✔
610
     * @private
36✔
611
     */
36✔
612
    public watchdogReset(): void {
36✔
613
        debugLog("Session#watchdogReset: the server session has expired and must be removed from the server");
11✔
614
        // the server session has expired and must be removed from the server
11✔
615
        this.emit("timeout");
11✔
616
    }
11✔
617

36✔
618
    private _createSessionObjectInAddressSpace() {
36✔
619
        if (this.sessionObject) {
1,001✔
620
            return;
21✔
621
        }
21✔
622
        assert(!this.sessionObject, "ServerSession#_createSessionObjectInAddressSpace already called ?");
980✔
623

980✔
624
        this.sessionObject = null;
980✔
625
        if (!this.addressSpace) {
1,001✔
626
            debugLog("ServerSession#_createSessionObjectInAddressSpace : no addressSpace");
×
627
            return; // no addressSpace
×
628
        }
×
629
        const root = this.addressSpace.rootFolder;
980✔
630
        assert(root, "expecting a root object");
980✔
631

980✔
632
        if (!root.objects) {
1,001✔
633
            debugLog("ServerSession#_createSessionObjectInAddressSpace : no object folder");
7✔
634
            return false;
7✔
635
        }
7✔
636
        if (!root.objects.server) {
1,001✔
637
            debugLog("ServerSession#_createSessionObjectInAddressSpace : no server object");
×
638
            return false;
×
639
        }
×
640

973✔
641
        // self.addressSpace.findNode(makeNodeId(ObjectIds.Server_ServerDiagnostics));
973✔
642
        const serverDiagnosticsNode = root.objects.server.serverDiagnostics;
973✔
643

973✔
644
        if (!serverDiagnosticsNode || !serverDiagnosticsNode.sessionsDiagnosticsSummary) {
1,001✔
645
            debugLog("ServerSession#_createSessionObjectInAddressSpace :" + " no serverDiagnostics.sessionsDiagnosticsSummary");
1✔
646
            return false;
1✔
647
        }
1✔
648

972✔
649
        const sessionDiagnosticsObjectType = this.addressSpace.findObjectType("SessionDiagnosticsObjectType");
972✔
650

972✔
651
        const sessionDiagnosticsDataType = this.addressSpace.findDataType("SessionDiagnosticsDataType");
972✔
652
        const sessionDiagnosticsVariableType = this.addressSpace.findVariableType("SessionDiagnosticsVariableType");
972✔
653

972✔
654
        const sessionSecurityDiagnosticsDataType = this.addressSpace.findDataType("SessionSecurityDiagnosticsDataType");
972✔
655
        const sessionSecurityDiagnosticsType = this.addressSpace.findVariableType("SessionSecurityDiagnosticsType");
972✔
656

972✔
657
        const namespace = this.addressSpace.getOwnNamespace();
972✔
658

972✔
659
        function createSessionDiagnosticsStuff(this: ServerSession) {
972✔
660
            if (sessionDiagnosticsDataType && sessionDiagnosticsVariableType) {
972✔
661
                // the extension object
972✔
662
                this._sessionDiagnostics = this.addressSpace!.constructExtensionObject(
972✔
663
                    sessionDiagnosticsDataType,
972✔
664
                    {}
972✔
665
                )! as SessionDiagnosticsDataTypeEx;
972✔
666
                this._sessionDiagnostics.$session = this;
972✔
667

972✔
668
                // install property getter on property that are unlikely to change
972✔
669
                if (this.parent.clientDescription) {
972✔
670
                    this._sessionDiagnostics.clientDescription = this.parent.clientDescription;
972✔
671
                }
972✔
672

972✔
673
                Object.defineProperty(this._sessionDiagnostics, "clientConnectionTime", {
972✔
674
                    get(this: any) {
972✔
675
                        return this.$session.clientConnectionTime;
2,110✔
676
                    }
2,110✔
677
                });
972✔
678

972✔
679
                Object.defineProperty(this._sessionDiagnostics, "actualSessionTimeout", {
972✔
680
                    get(this: SessionDiagnosticsDataTypeEx) {
972✔
681
                        return this.$session?.sessionTimeout;
2,110✔
682
                    }
2,110✔
683
                });
972✔
684

972✔
685
                Object.defineProperty(this._sessionDiagnostics, "sessionId", {
972✔
686
                    get(this: SessionDiagnosticsDataTypeEx) {
972✔
687
                        return this.$session ? this.$session.nodeId : NodeId.nullNodeId;
10,286✔
688
                    }
10,286✔
689
                });
972✔
690

972✔
691
                Object.defineProperty(this._sessionDiagnostics, "sessionName", {
972✔
692
                    get(this: SessionDiagnosticsDataTypeEx) {
972✔
693
                        return this.$session ? this.$session.sessionName.toString() : "";
2,110✔
694
                    }
2,110✔
695
                });
972✔
696

972✔
697
                this.sessionDiagnostics = sessionDiagnosticsVariableType.instantiate({
972✔
698
                    browseName: new QualifiedName({ name: "SessionDiagnostics", namespaceIndex: 0 }),
972✔
699
                    componentOf: this.sessionObject,
972✔
700
                    extensionObject: this._sessionDiagnostics,
972✔
701
                    minimumSamplingInterval: 2000 // 2 seconds
972✔
702
                }) as UASessionDiagnosticsVariable<DTSessionDiagnostics>;
972✔
703

972✔
704
                this._sessionDiagnostics = this.sessionDiagnostics.$extensionObject as SessionDiagnosticsDataTypeEx;
972✔
705
                assert(this._sessionDiagnostics.$session === this);
972✔
706

972✔
707
                const sessionDiagnosticsArray = this.getSessionDiagnosticsArray();
972✔
708

972✔
709
                // add sessionDiagnostics into sessionDiagnosticsArray
972✔
710
                addElement<SessionDiagnosticsDataType>(this._sessionDiagnostics, sessionDiagnosticsArray);
972✔
711
            }
972✔
712
        }
972✔
713
        function createSessionSecurityDiagnosticsStuff(this: ServerSession) {
972✔
714
            if (sessionSecurityDiagnosticsDataType && sessionSecurityDiagnosticsType) {
972✔
715
                // the extension object
952✔
716
                this._sessionSecurityDiagnostics = this.addressSpace!.constructExtensionObject(
952✔
717
                    sessionSecurityDiagnosticsDataType,
952✔
718
                    {}
952✔
719
                )! as SessionSecurityDiagnosticsDataTypeEx;
952✔
720
                this._sessionSecurityDiagnostics.$session = this;
952✔
721

952✔
722
                /*
952✔
723
                    sessionId: NodeId;
952✔
724
                    clientUserIdOfSession: UAString;
952✔
725
                    clientUserIdHistory: UAString[] | null;
952✔
726
                    authenticationMechanism: UAString;
952✔
727
                    encoding: UAString;
952✔
728
                    transportProtocol: UAString;
952✔
729
                    securityMode: MessageSecurityMode;
952✔
730
                    securityPolicyUri: UAString;
952✔
731
                    clientCertificate: ByteString;
952✔
732
                */
952✔
733
                Object.defineProperty(this._sessionSecurityDiagnostics, "sessionId", {
952✔
734
                    get(this: any) {
952✔
735
                        return this.$session?.nodeId;
9,970✔
736
                    }
9,970✔
737
                });
952✔
738

952✔
739
                Object.defineProperty(this._sessionSecurityDiagnostics, "clientUserIdOfSession", {
952✔
740
                    get(this: any) {
952✔
741
                        return ""; // UAString // TO DO : implement
1,914✔
742
                    }
1,914✔
743
                });
952✔
744

952✔
745
                Object.defineProperty(this._sessionSecurityDiagnostics, "clientUserIdHistory", {
952✔
746
                    get(this: any) {
952✔
747
                        return []; // UAString[] | null
1,914✔
748
                    }
1,914✔
749
                });
952✔
750

952✔
751
                Object.defineProperty(this._sessionSecurityDiagnostics, "authenticationMechanism", {
952✔
752
                    get(this: any) {
952✔
753
                        return "";
1,914✔
754
                    }
1,914✔
755
                });
952✔
756
                Object.defineProperty(this._sessionSecurityDiagnostics, "encoding", {
952✔
757
                    get(this: any) {
952✔
758
                        return "";
1,914✔
759
                    }
1,914✔
760
                });
952✔
761
                Object.defineProperty(this._sessionSecurityDiagnostics, "transportProtocol", {
952✔
762
                    get(this: any) {
952✔
763
                        return "opc.tcp";
1,914✔
764
                    }
1,914✔
765
                });
952✔
766
                Object.defineProperty(this._sessionSecurityDiagnostics, "securityMode", {
952✔
767
                    get(this: any) {
952✔
768
                        const session: ServerSession = this.$session;
1,914✔
769
                        return session?.channel?.securityMode;
1,914✔
770
                    }
1,914✔
771
                });
952✔
772
                Object.defineProperty(this._sessionSecurityDiagnostics, "securityPolicyUri", {
952✔
773
                    get(this: any) {
952✔
774
                        const session: ServerSession = this.$session;
1,914✔
775
                        return session?.channel?.securityPolicy;
1,914✔
776
                    }
1,914✔
777
                });
952✔
778
                Object.defineProperty(this._sessionSecurityDiagnostics, "clientCertificate", {
952✔
779
                    get(this: any) {
952✔
780
                        const session: ServerSession = this.$session;
1,914✔
781
                        return session?.channel!.clientCertificate;
1,914✔
782
                    }
1,914✔
783
                });
952✔
784

952✔
785
                this.sessionSecurityDiagnostics = sessionSecurityDiagnosticsType.instantiate({
952✔
786
                    browseName: new QualifiedName({ name: "SessionSecurityDiagnostics", namespaceIndex: 0 }),
952✔
787
                    componentOf: this.sessionObject,
952✔
788
                    extensionObject: this._sessionSecurityDiagnostics,
952✔
789
                    minimumSamplingInterval: 2000 // 2 seconds
952✔
790
                }) as UASessionSecurityDiagnostics<DTSessionSecurityDiagnostics>;
952✔
791

952✔
792
                ensureObjectIsSecure(this.sessionSecurityDiagnostics);
952✔
793

952✔
794
                this._sessionSecurityDiagnostics = this.sessionSecurityDiagnostics
952✔
795
                    .$extensionObject as SessionSecurityDiagnosticsDataTypeEx;
952✔
796
                assert(this._sessionSecurityDiagnostics.$session === this);
952✔
797

952✔
798
                const sessionSecurityDiagnosticsArray = this.getSessionSecurityDiagnosticsArray();
952✔
799

952✔
800
                // add sessionDiagnostics into sessionDiagnosticsArray
952✔
801
                const node = addElement<SessionSecurityDiagnosticsDataType>(
952✔
802
                    this._sessionSecurityDiagnostics,
952✔
803
                    sessionSecurityDiagnosticsArray
952✔
804
                );
952✔
805
                ensureObjectIsSecure(node);
952✔
806
            }
952✔
807
        }
972✔
808

972✔
809
        function createSessionDiagnosticSummaryUAObject(this: ServerSession) {
972✔
810
            const references: any[] = [];
972✔
811
            if (sessionDiagnosticsObjectType) {
972✔
812
                references.push({
952✔
813
                    isForward: true,
952✔
814
                    nodeId: sessionDiagnosticsObjectType,
952✔
815
                    referenceType: "HasTypeDefinition"
952✔
816
                });
952✔
817
            }
952✔
818

972✔
819
            this.sessionObject = namespace.createNode({
972✔
820
                browseName: this.sessionName || "Session-" + this.nodeId.toString(),
972✔
821
                componentOf: serverDiagnosticsNode.sessionsDiagnosticsSummary,
972✔
822
                nodeClass: NodeClass.Object,
972✔
823
                nodeId: this.nodeId,
972✔
824
                references,
972✔
825
                typeDefinition: sessionDiagnosticsObjectType
972✔
826
            }) as UAObject;
972✔
827

972✔
828
            createSessionDiagnosticsStuff.call(this);
972✔
829
            createSessionSecurityDiagnosticsStuff.call(this);
972✔
830
        }
972✔
831
        function createSubscriptionDiagnosticsArray(this: ServerSession) {
972✔
832
            const subscriptionDiagnosticsArrayType = this.addressSpace!.findVariableType("SubscriptionDiagnosticsArrayType")!;
972✔
833
            assert(subscriptionDiagnosticsArrayType.nodeId.toString() === "ns=0;i=2171");
972✔
834

972✔
835
            this.subscriptionDiagnosticsArray = createExtObjArrayNode<SubscriptionDiagnosticsDataType>(this.sessionObject, {
972✔
836
                browseName: { namespaceIndex: 0, name: "SubscriptionDiagnosticsArray" },
972✔
837
                complexVariableType: "SubscriptionDiagnosticsArrayType",
972✔
838
                indexPropertyName: "subscriptionId",
972✔
839
                minimumSamplingInterval: 2000, // 2 seconds
972✔
840
                variableType: "SubscriptionDiagnosticsType"
972✔
841
            });
972✔
842
        }
972✔
843
        createSessionDiagnosticSummaryUAObject.call(this);
972✔
844
        createSubscriptionDiagnosticsArray.call(this);
972✔
845
        return this.sessionObject;
972✔
846
    }
972✔
847

36✔
848
    public async resendMonitoredItemInitialValues(): Promise<void> {
36✔
849
        for (const subscription of this.publishEngine.subscriptions) {
1,000✔
850
            await subscription.resendInitialValues();
4✔
851
        }
4✔
852
    }
1,000✔
853

36✔
854
    /**
36✔
855
     *
36✔
856
     * @private
36✔
857
     */
36✔
858
    private _removeSessionObjectFromAddressSpace() {
36✔
859
        // todo : dump session statistics in a file or somewhere for deeper diagnostic analysis on closed session
1,072✔
860

1,072✔
861
        if (!this.addressSpace) {
1,072✔
862
            return;
3✔
863
        }
3✔
864
        if (this.sessionDiagnostics) {
1,072✔
865
            const sessionDiagnosticsArray = this.getSessionDiagnosticsArray()!;
972✔
866
            removeElement(sessionDiagnosticsArray, (a) => sameNodeId(a.sessionId, this.getSessionId()));
972✔
867
            this.addressSpace.deleteNode(this.sessionDiagnostics);
972✔
868

972✔
869
            assert(this._sessionDiagnostics!.$session === this);
972✔
870
            this._sessionDiagnostics!.$session = null;
972✔
871

972✔
872
            this._sessionDiagnostics = undefined;
972✔
873
            this.sessionDiagnostics = undefined;
972✔
874
        }
972✔
875

1,072✔
876
        if (this.sessionSecurityDiagnostics) {
1,072✔
877
            const sessionSecurityDiagnosticsArray = this.getSessionSecurityDiagnosticsArray()!;
952✔
878
            removeElement(sessionSecurityDiagnosticsArray, (a) => sameNodeId(a.sessionId, this.getSessionId()));
952✔
879

952✔
880
            this.addressSpace.deleteNode(this.sessionSecurityDiagnostics);
952✔
881

952✔
882
            assert(this._sessionSecurityDiagnostics!.$session === this);
952✔
883
            this._sessionSecurityDiagnostics!.$session = null;
952✔
884

952✔
885
            this._sessionSecurityDiagnostics = undefined;
952✔
886
            this.sessionSecurityDiagnostics = undefined;
952✔
887
        }
952✔
888

1,072✔
889
        if (this.sessionObject) {
1,072✔
890
            this.addressSpace.deleteNode(this.sessionObject);
972✔
891
            this.sessionObject = null;
972✔
892
        }
972✔
893
    }
1,072✔
894

36✔
895
    /**
36✔
896
     *
36✔
897
     * @private
36✔
898
     */
36✔
899
    private _getSubscriptionDiagnosticsArray() {
36✔
900
        if (!this.addressSpace) {
954!
901
            // c8 ignore next
×
902
            if (doDebug) {
×
903
                console.warn("ServerSession#_getSubscriptionDiagnosticsArray : no addressSpace");
×
904
            }
×
905
            return null; // no addressSpace
×
906
        }
×
907

954✔
908
        const subscriptionDiagnosticsArray = this.subscriptionDiagnosticsArray;
954✔
909
        if (!subscriptionDiagnosticsArray) {
954✔
910
            return null; // no subscriptionDiagnosticsArray
90✔
911
        }
90✔
912
        assert(subscriptionDiagnosticsArray.browseName.toString() === "SubscriptionDiagnosticsArray");
864✔
913
        return subscriptionDiagnosticsArray;
864✔
914
    }
950✔
915

36✔
916
    private assignSubscription(subscription: Subscription) {
36✔
917
        assert(!subscription.$session);
457✔
918
        assert(this.nodeId instanceof NodeId);
457✔
919

457✔
920
        subscription.$session = this;
457✔
921
        assert(subscription.sessionId === this.nodeId);
457✔
922

457✔
923
        this._cumulatedSubscriptionCount += 1;
457✔
924

457✔
925
        // Notify the owner that a new subscription has been created
457✔
926
        // @event new_subscription
457✔
927
        // @param {Subscription} subscription
457✔
928
        this.emit("new_subscription", subscription);
457✔
929

457✔
930
        // add subscription diagnostics to SubscriptionDiagnosticsArray
457✔
931
        this._exposeSubscriptionDiagnostics(subscription);
457✔
932

457✔
933
        subscription.once("terminated", () => {
457✔
934
            // Notify the owner that a new subscription has been terminated
457✔
935
            // @event subscription_terminated
457✔
936
            // @param {Subscription} subscription
457✔
937
            this.emit("subscription_terminated", subscription);
457✔
938
        });
457✔
939
    }
457✔
940

36✔
941
    private _deleteSubscriptions() {
36✔
942
        if (!this.publishEngine) return;
1,072!
943
        const subscriptions = this.publishEngine.subscriptions;
1,072✔
944
        for (const subscription of subscriptions) {
1,072✔
945
            this.deleteSubscription(subscription.id);
139✔
946
        }
139✔
947
    }
1,072✔
948
}
36!
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