fix: flag worker set as stopped as soon the stopped event is emitted
[e-mobility-charging-stations-simulator.git] / src / charging-station / Bootstrap.ts
CommitLineData
a19b897d 1// Partial Copyright Jerome Benoit. 2021-2024. All Rights Reserved.
b4d34251 2
66a7748d 3import { EventEmitter } from 'node:events'
2f989136 4import { dirname, extname, join, parse } from 'node:path'
66a7748d
JB
5import process, { exit } from 'node:process'
6import { fileURLToPath } from 'node:url'
c5ecc04d 7import { isMainThread } from 'node:worker_threads'
ba9a56a6 8import type { Worker } from 'worker_threads'
8114d10e 9
66a7748d 10import chalk from 'chalk'
ba9a56a6 11import { type MessageHandler, availableParallelism } from 'poolifier'
8114d10e 12
66a7748d
JB
13import { waitChargingStationEvents } from './Helpers.js'
14import type { AbstractUIServer } from './ui-server/AbstractUIServer.js'
15import { UIServerFactory } from './ui-server/UIServerFactory.js'
16import { version } from '../../package.json'
17import { BaseError } from '../exception/index.js'
18import { type Storage, StorageFactory } from '../performance/index.js'
e7aeea18 19import {
bbe10d5f 20 type ChargingStationData,
71ac2bd7 21 type ChargingStationOptions,
bbe10d5f 22 type ChargingStationWorkerData,
244c1396 23 type ChargingStationWorkerEventError,
bbe10d5f
JB
24 type ChargingStationWorkerMessage,
25 type ChargingStationWorkerMessageData,
e7aeea18 26 ChargingStationWorkerMessageEvents,
5d049829 27 ConfigurationSection,
6bd808fd 28 ProcedureName,
268a74bb 29 type Statistics,
5d049829
JB
30 type StorageConfiguration,
31 type UIServerConfiguration,
66a7748d
JB
32 type WorkerConfiguration
33} from '../types/index.js'
fa5995d6
JB
34import {
35 Configuration,
36 Constants,
9bf0ef23
JB
37 formatDurationMilliSeconds,
38 generateUUID,
fa5995d6
JB
39 handleUncaughtException,
40 handleUnhandledRejection,
be0a4d4d 41 isAsyncFunction,
9bf0ef23 42 isNotEmptyArray,
9bf0ef23 43 logPrefix,
c5ecc04d
JB
44 logger,
45 max
66a7748d
JB
46} from '../utils/index.js'
47import { type WorkerAbstract, WorkerFactory } from '../worker/index.js'
ded13d97 48
66a7748d 49const moduleName = 'Bootstrap'
32de5a57 50
a307349b 51enum exitCodes {
a51a4ead 52 succeeded = 0,
a307349b 53 missingChargingStationsConfiguration = 1,
2f989136
JB
54 duplicateChargingStationTemplateUrls = 2,
55 noChargingStationTemplates = 3,
56 gracefulShutdownError = 4
a307349b 57}
e4cb2c14 58
efc411f7
JB
59interface TemplateChargingStations {
60 configured: number
244c1396 61 added: number
efc411f7
JB
62 started: number
63 lastIndex: number
64}
65
f130b8e6 66export class Bootstrap extends EventEmitter {
66a7748d 67 private static instance: Bootstrap | null = null
66a7748d
JB
68 private workerImplementation?: WorkerAbstract<ChargingStationWorkerData>
69 private readonly uiServer?: AbstractUIServer
70 private storage?: Storage
efc411f7 71 private readonly chargingStationsByTemplate: Map<string, TemplateChargingStations>
66a7748d
JB
72 private readonly version: string = version
73 private initializedCounters: boolean
74 private started: boolean
75 private starting: boolean
76 private stopping: boolean
ded13d97 77
66a7748d
JB
78 private constructor () {
79 super()
6bd808fd 80 for (const signal of ['SIGINT', 'SIGQUIT', 'SIGTERM']) {
66a7748d 81 process.on(signal, this.gracefulShutdown.bind(this))
6bd808fd 82 }
4724a293 83 // Enable unconditionally for now
66a7748d
JB
84 handleUnhandledRejection()
85 handleUncaughtException()
86 this.started = false
87 this.starting = false
88 this.stopping = false
efc411f7 89 this.chargingStationsByTemplate = new Map<string, TemplateChargingStations>()
36adaf06 90 this.uiServer = UIServerFactory.getUIServerImplementation(
66a7748d
JB
91 Configuration.getConfigurationSection<UIServerConfiguration>(ConfigurationSection.uiServer)
92 )
42e341c4
JB
93 this.initializedCounters = false
94 this.initializeCounters()
66a7748d 95 Configuration.configurationChangeCallback = async () => {
c5ecc04d
JB
96 if (isMainThread) {
97 await Bootstrap.getInstance().restart()
98 }
66a7748d 99 }
ded13d97
JB
100 }
101
66a7748d 102 public static getInstance (): Bootstrap {
1ca780f9 103 if (Bootstrap.instance === null) {
66a7748d 104 Bootstrap.instance = new Bootstrap()
ded13d97 105 }
66a7748d 106 return Bootstrap.instance
ded13d97
JB
107 }
108
2f989136
JB
109 public get numberOfChargingStationTemplates (): number {
110 return this.chargingStationsByTemplate.size
111 }
112
113 public get numberOfConfiguredChargingStations (): number {
114 return [...this.chargingStationsByTemplate.values()].reduce(
115 (accumulator, value) => accumulator + value.configured,
116 0
117 )
118 }
119
c5ecc04d
JB
120 public getLastIndex (templateName: string): number {
121 return this.chargingStationsByTemplate.get(templateName)?.lastIndex ?? 0
122 }
123
a66bbcfe
JB
124 public getPerformanceStatistics (): IterableIterator<Statistics> | undefined {
125 return this.storage?.getPerformanceStatistics()
126 }
127
244c1396
JB
128 private get numberOfAddedChargingStations (): number {
129 return [...this.chargingStationsByTemplate.values()].reduce(
130 (accumulator, value) => accumulator + value.added,
131 0
132 )
133 }
134
2f989136
JB
135 private get numberOfStartedChargingStations (): number {
136 return [...this.chargingStationsByTemplate.values()].reduce(
137 (accumulator, value) => accumulator + value.started,
138 0
139 )
140 }
141
66a7748d
JB
142 public async start (): Promise<void> {
143 if (!this.started) {
144 if (!this.starting) {
145 this.starting = true
244c1396 146 this.on(ChargingStationWorkerMessageEvents.added, this.workerEventAdded)
09e5a7a8 147 this.on(ChargingStationWorkerMessageEvents.deleted, this.workerEventDeleted)
66a7748d
JB
148 this.on(ChargingStationWorkerMessageEvents.started, this.workerEventStarted)
149 this.on(ChargingStationWorkerMessageEvents.stopped, this.workerEventStopped)
150 this.on(ChargingStationWorkerMessageEvents.updated, this.workerEventUpdated)
4354af5a
JB
151 this.on(
152 ChargingStationWorkerMessageEvents.performanceStatistics,
66a7748d
JB
153 this.workerEventPerformanceStatistics
154 )
44fccdf0
JB
155 this.on(
156 ChargingStationWorkerMessageEvents.workerElementError,
887a125e 157 (eventError: ChargingStationWorkerEventError) => {
44fccdf0 158 logger.error(
3ab32759 159 `${this.logPrefix()} ${moduleName}.start: Error occurred while handling '${eventError.event}' event on worker:`,
887a125e 160 eventError
44fccdf0
JB
161 )
162 }
163 )
66a7748d 164 this.initializeCounters()
5b373a23 165 const workerConfiguration = Configuration.getConfigurationSection<WorkerConfiguration>(
66a7748d
JB
166 ConfigurationSection.worker
167 )
168 this.initializeWorkerImplementation(workerConfiguration)
169 await this.workerImplementation?.start()
6d2b7d01
JB
170 const performanceStorageConfiguration =
171 Configuration.getConfigurationSection<StorageConfiguration>(
66a7748d
JB
172 ConfigurationSection.performanceStorage
173 )
6d2b7d01
JB
174 if (performanceStorageConfiguration.enabled === true) {
175 this.storage = StorageFactory.getStorage(
66a7748d 176 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
6d2b7d01 177 performanceStorageConfiguration.type!,
66a7748d 178 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
6d2b7d01 179 performanceStorageConfiguration.uri!,
66a7748d
JB
180 this.logPrefix()
181 )
182 await this.storage?.open()
6d2b7d01 183 }
36adaf06 184 Configuration.getConfigurationSection<UIServerConfiguration>(ConfigurationSection.uiServer)
66a7748d 185 .enabled === true && this.uiServer?.start()
82e9c15a 186 // Start ChargingStation object instance in worker thread
66a7748d 187 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
e1d9a0f4 188 for (const stationTemplateUrl of Configuration.getStationTemplateUrls()!) {
82e9c15a 189 try {
2f989136
JB
190 const nbStations =
191 this.chargingStationsByTemplate.get(parse(stationTemplateUrl.file).name)
192 ?.configured ?? stationTemplateUrl.numberOfStations
82e9c15a 193 for (let index = 1; index <= nbStations; index++) {
c5ecc04d 194 await this.addChargingStation(index, stationTemplateUrl.file)
82e9c15a
JB
195 }
196 } catch (error) {
197 console.error(
198 chalk.red(
66a7748d 199 `Error at starting charging station with template file ${stationTemplateUrl.file}: `
82e9c15a 200 ),
66a7748d
JB
201 error
202 )
ded13d97 203 }
ded13d97 204 }
82e9c15a
JB
205 console.info(
206 chalk.green(
207 `Charging stations simulator ${
208 this.version
c5ecc04d 209 } started with ${this.numberOfConfiguredChargingStations} configured charging station(s) from ${this.numberOfChargingStationTemplates} charging station template(s) and ${
2f989136 210 Configuration.workerDynamicPoolInUse() ? `${workerConfiguration.poolMinSize}/` : ''
82e9c15a 211 }${this.workerImplementation?.size}${
2f989136 212 Configuration.workerPoolInUse() ? `/${workerConfiguration.poolMaxSize}` : ''
5b373a23 213 } worker(s) concurrently running in '${workerConfiguration.processType}' mode${
401fa922 214 this.workerImplementation?.maxElementsPerWorker != null
5199f9fd 215 ? ` (${this.workerImplementation.maxElementsPerWorker} charging station(s) per worker)`
82e9c15a 216 : ''
66a7748d
JB
217 }`
218 )
219 )
56e2e1ab
JB
220 Configuration.workerDynamicPoolInUse() &&
221 console.warn(
222 chalk.yellow(
66a7748d
JB
223 'Charging stations simulator is using dynamic pool mode. This is an experimental feature with known issues.\nPlease consider using fixed pool or worker set mode instead'
224 )
225 )
226 console.info(chalk.green('Worker set/pool information:'), this.workerImplementation?.info)
227 this.started = true
228 this.starting = false
82e9c15a 229 } else {
66a7748d 230 console.error(chalk.red('Cannot start an already starting charging stations simulator'))
ded13d97 231 }
b322b8b4 232 } else {
66a7748d 233 console.error(chalk.red('Cannot start an already started charging stations simulator'))
ded13d97
JB
234 }
235 }
236
c5ecc04d 237 public async stop (): Promise<void> {
66a7748d
JB
238 if (this.started) {
239 if (!this.stopping) {
240 this.stopping = true
c5ecc04d
JB
241 await this.uiServer?.sendInternalRequest(
242 this.uiServer.buildProtocolRequest(
243 generateUUID(),
244 ProcedureName.STOP_CHARGING_STATION,
245 Constants.EMPTY_FROZEN_OBJECT
66a7748d 246 )
c5ecc04d
JB
247 )
248 try {
249 await this.waitChargingStationsStopped()
250 } catch (error) {
251 console.error(chalk.red('Error while waiting for charging stations to stop: '), error)
ab7a96fa 252 }
66a7748d
JB
253 await this.workerImplementation?.stop()
254 delete this.workerImplementation
255 this.removeAllListeners()
256 await this.storage?.close()
257 delete this.storage
66a7748d
JB
258 this.started = false
259 this.stopping = false
82e9c15a 260 } else {
66a7748d 261 console.error(chalk.red('Cannot stop an already stopping charging stations simulator'))
82e9c15a 262 }
b322b8b4 263 } else {
66a7748d 264 console.error(chalk.red('Cannot stop an already stopped charging stations simulator'))
ded13d97 265 }
ded13d97
JB
266 }
267
c5ecc04d
JB
268 private async restart (): Promise<void> {
269 await this.stop()
73edcc94 270 Configuration.getConfigurationSection<UIServerConfiguration>(ConfigurationSection.uiServer)
baf34a77 271 .enabled !== true && this.uiServer?.stop()
2f989136 272 this.initializedCounters = false
66a7748d 273 await this.start()
ded13d97
JB
274 }
275
66a7748d
JB
276 private async waitChargingStationsStopped (): Promise<string> {
277 return await new Promise<string>((resolve, reject) => {
5b2721db 278 const waitTimeout = setTimeout(() => {
a01134ed 279 const timeoutMessage = `Timeout ${formatDurationMilliSeconds(
66a7748d
JB
280 Constants.STOP_CHARGING_STATIONS_TIMEOUT
281 )} reached at stopping charging stations`
a01134ed
JB
282 console.warn(chalk.yellow(timeoutMessage))
283 reject(new Error(timeoutMessage))
66a7748d 284 }, Constants.STOP_CHARGING_STATIONS_TIMEOUT)
36adaf06
JB
285 waitChargingStationEvents(
286 this,
287 ChargingStationWorkerMessageEvents.stopped,
a01134ed 288 this.numberOfStartedChargingStations
5b2721db
JB
289 )
290 .then(() => {
66a7748d 291 resolve('Charging stations stopped')
5b2721db 292 })
b7ee97c1 293 .catch(reject)
5b2721db 294 .finally(() => {
66a7748d
JB
295 clearTimeout(waitTimeout)
296 })
297 })
36adaf06
JB
298 }
299
66a7748d 300 private initializeWorkerImplementation (workerConfiguration: WorkerConfiguration): void {
c5ecc04d
JB
301 if (!isMainThread) {
302 return
303 }
1feac591 304 let elementsPerWorker: number
5199f9fd 305 switch (workerConfiguration.elementsPerWorker) {
1feac591
JB
306 case 'all':
307 elementsPerWorker = this.numberOfConfiguredChargingStations
308 break
487f0dfd 309 case 'auto':
1feac591 310 default:
487f0dfd 311 elementsPerWorker =
2f989136
JB
312 this.numberOfConfiguredChargingStations > availableParallelism()
313 ? Math.round(this.numberOfConfiguredChargingStations / (availableParallelism() * 1.5))
66a7748d
JB
314 : 1
315 break
8603c1ca 316 }
6d2b7d01
JB
317 this.workerImplementation = WorkerFactory.getWorkerImplementation<ChargingStationWorkerData>(
318 join(
319 dirname(fileURLToPath(import.meta.url)),
66a7748d 320 `ChargingStationWorker${extname(fileURLToPath(import.meta.url))}`
6d2b7d01 321 ),
66a7748d 322 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
6d2b7d01
JB
323 workerConfiguration.processType!,
324 {
325 workerStartDelay: workerConfiguration.startDelay,
326 elementStartDelay: workerConfiguration.elementStartDelay,
66a7748d 327 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
6d2b7d01 328 poolMaxSize: workerConfiguration.poolMaxSize!,
66a7748d 329 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
6d2b7d01 330 poolMinSize: workerConfiguration.poolMinSize!,
1feac591 331 elementsPerWorker,
6d2b7d01 332 poolOptions: {
ba9a56a6 333 messageHandler: this.messageHandler.bind(this) as MessageHandler<Worker>,
66a7748d
JB
334 workerOptions: { resourceLimits: workerConfiguration.resourceLimits }
335 }
336 }
337 )
ded13d97 338 }
81797102 339
66a7748d
JB
340 private messageHandler (
341 msg: ChargingStationWorkerMessage<ChargingStationWorkerMessageData>
32de5a57
LM
342 ): void {
343 // logger.debug(
344 // `${this.logPrefix()} ${moduleName}.messageHandler: Worker channel message received: ${JSON.stringify(
345 // msg,
4ed03b6e 346 // undefined,
66a7748d
JB
347 // 2
348 // )}`
349 // )
32de5a57 350 try {
8cc482a9 351 switch (msg.event) {
244c1396 352 case ChargingStationWorkerMessageEvents.added:
44fccdf0 353 this.emit(ChargingStationWorkerMessageEvents.added, msg.data)
244c1396 354 break
09e5a7a8
JB
355 case ChargingStationWorkerMessageEvents.deleted:
356 this.emit(ChargingStationWorkerMessageEvents.deleted, msg.data)
357 break
721646e9 358 case ChargingStationWorkerMessageEvents.started:
44fccdf0 359 this.emit(ChargingStationWorkerMessageEvents.started, msg.data)
66a7748d 360 break
721646e9 361 case ChargingStationWorkerMessageEvents.stopped:
44fccdf0 362 this.emit(ChargingStationWorkerMessageEvents.stopped, msg.data)
66a7748d 363 break
721646e9 364 case ChargingStationWorkerMessageEvents.updated:
44fccdf0 365 this.emit(ChargingStationWorkerMessageEvents.updated, msg.data)
66a7748d 366 break
721646e9 367 case ChargingStationWorkerMessageEvents.performanceStatistics:
44fccdf0 368 this.emit(ChargingStationWorkerMessageEvents.performanceStatistics, msg.data)
66a7748d 369 break
e1a3f3c1 370 case ChargingStationWorkerMessageEvents.addedWorkerElement:
a492245c 371 this.emit(ChargingStationWorkerMessageEvents.addWorkerElement, msg.data)
e1a3f3c1 372 break
244c1396 373 case ChargingStationWorkerMessageEvents.workerElementError:
244c1396 374 this.emit(ChargingStationWorkerMessageEvents.workerElementError, msg.data)
66a7748d 375 break
32de5a57
LM
376 default:
377 throw new BaseError(
f93dda6a
JB
378 `Unknown charging station worker event: '${
379 msg.event
66a7748d
JB
380 }' received with data: ${JSON.stringify(msg.data, undefined, 2)}`
381 )
32de5a57
LM
382 }
383 } catch (error) {
384 logger.error(
385 `${this.logPrefix()} ${moduleName}.messageHandler: Error occurred while handling '${
8cc482a9 386 msg.event
32de5a57 387 }' event:`,
66a7748d
JB
388 error
389 )
32de5a57
LM
390 }
391 }
392
244c1396
JB
393 private readonly workerEventAdded = (data: ChargingStationData): void => {
394 this.uiServer?.chargingStations.set(data.stationInfo.hashId, data)
395 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
396 ++this.chargingStationsByTemplate.get(data.stationInfo.templateName)!.added
397 logger.info(
398 `${this.logPrefix()} ${moduleName}.workerEventAdded: Charging station ${
399 data.stationInfo.chargingStationId
400 } (hashId: ${data.stationInfo.hashId}) added (${
401 this.numberOfAddedChargingStations
402 } added from ${this.numberOfConfiguredChargingStations} configured charging station(s))`
09e5a7a8
JB
403 )
404 }
405
406 private readonly workerEventDeleted = (data: ChargingStationData): void => {
407 this.uiServer?.chargingStations.delete(data.stationInfo.hashId)
408 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
409 --this.chargingStationsByTemplate.get(data.stationInfo.templateName)!.added
410 logger.info(
411 `${this.logPrefix()} ${moduleName}.workerEventDeleted: Charging station ${
412 data.stationInfo.chargingStationId
413 } (hashId: ${data.stationInfo.hashId}) deleted (${
414 this.numberOfAddedChargingStations
415 } added from ${this.numberOfConfiguredChargingStations} configured charging station(s))`
244c1396
JB
416 )
417 }
418
66a7748d
JB
419 private readonly workerEventStarted = (data: ChargingStationData): void => {
420 this.uiServer?.chargingStations.set(data.stationInfo.hashId, data)
2f989136
JB
421 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
422 ++this.chargingStationsByTemplate.get(data.stationInfo.templateName)!.started
56eb297e 423 logger.info(
e6159ce8 424 `${this.logPrefix()} ${moduleName}.workerEventStarted: Charging station ${
56eb297e 425 data.stationInfo.chargingStationId
e6159ce8 426 } (hashId: ${data.stationInfo.hashId}) started (${
56eb297e 427 this.numberOfStartedChargingStations
244c1396 428 } started from ${this.numberOfAddedChargingStations} added charging station(s))`
66a7748d
JB
429 )
430 }
32de5a57 431
66a7748d
JB
432 private readonly workerEventStopped = (data: ChargingStationData): void => {
433 this.uiServer?.chargingStations.set(data.stationInfo.hashId, data)
2f989136
JB
434 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
435 --this.chargingStationsByTemplate.get(data.stationInfo.templateName)!.started
56eb297e 436 logger.info(
e6159ce8 437 `${this.logPrefix()} ${moduleName}.workerEventStopped: Charging station ${
56eb297e 438 data.stationInfo.chargingStationId
e6159ce8 439 } (hashId: ${data.stationInfo.hashId}) stopped (${
56eb297e 440 this.numberOfStartedChargingStations
244c1396 441 } started from ${this.numberOfAddedChargingStations} added charging station(s))`
66a7748d
JB
442 )
443 }
32de5a57 444
66a7748d
JB
445 private readonly workerEventUpdated = (data: ChargingStationData): void => {
446 this.uiServer?.chargingStations.set(data.stationInfo.hashId, data)
447 }
32de5a57 448
66a7748d 449 private readonly workerEventPerformanceStatistics = (data: Statistics): void => {
be0a4d4d
JB
450 // eslint-disable-next-line @typescript-eslint/unbound-method
451 if (isAsyncFunction(this.storage?.storePerformanceStatistics)) {
452 (
453 this.storage.storePerformanceStatistics as (
454 performanceStatistics: Statistics
455 ) => Promise<void>
456 )(data).catch(Constants.EMPTY_FUNCTION)
457 } else {
458 (this.storage?.storePerformanceStatistics as (performanceStatistics: Statistics) => void)(
459 data
460 )
461 }
66a7748d 462 }
32de5a57 463
66a7748d
JB
464 private initializeCounters (): void {
465 if (!this.initializedCounters) {
66a7748d
JB
466 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
467 const stationTemplateUrls = Configuration.getStationTemplateUrls()!
9bf0ef23 468 if (isNotEmptyArray(stationTemplateUrls)) {
7436ee0d 469 for (const stationTemplateUrl of stationTemplateUrls) {
2f989136
JB
470 const templateName = parse(stationTemplateUrl.file).name
471 this.chargingStationsByTemplate.set(templateName, {
472 configured: stationTemplateUrl.numberOfStations,
244c1396 473 added: 0,
c5ecc04d
JB
474 started: 0,
475 lastIndex: 0
2f989136
JB
476 })
477 this.uiServer?.chargingStationTemplates.add(templateName)
478 }
479 if (this.chargingStationsByTemplate.size !== stationTemplateUrls.length) {
480 console.error(
481 chalk.red(
482 "'stationTemplateUrls' contains duplicate entries, please check your configuration"
483 )
484 )
485 exit(exitCodes.duplicateChargingStationTemplateUrls)
7436ee0d 486 }
a596d200 487 } else {
2f989136
JB
488 console.error(
489 chalk.red("'stationTemplateUrls' not defined or empty, please check your configuration")
66a7748d
JB
490 )
491 exit(exitCodes.missingChargingStationsConfiguration)
a596d200 492 }
c5ecc04d
JB
493 if (
494 this.numberOfConfiguredChargingStations === 0 &&
495 Configuration.getConfigurationSection<UIServerConfiguration>(ConfigurationSection.uiServer)
baf34a77 496 .enabled !== true
c5ecc04d 497 ) {
2f989136
JB
498 console.error(
499 chalk.red(
c5ecc04d 500 "'stationTemplateUrls' has no charging station enabled and UI server is disabled, please check your configuration"
2f989136
JB
501 )
502 )
66a7748d 503 exit(exitCodes.noChargingStationTemplates)
a596d200 504 }
66a7748d 505 this.initializedCounters = true
846d2851 506 }
7c72977b
JB
507 }
508
71ac2bd7
JB
509 public async addChargingStation (
510 index: number,
511 stationTemplateFile: string,
512 options?: ChargingStationOptions
513 ): Promise<void> {
6ed3c845 514 await this.workerImplementation?.addElement({
717c1e56 515 index,
d972af76
JB
516 templateFile: join(
517 dirname(fileURLToPath(import.meta.url)),
e7aeea18
JB
518 'assets',
519 'station-templates',
c5ecc04d 520 stationTemplateFile
71ac2bd7
JB
521 ),
522 options
66a7748d 523 })
c5ecc04d
JB
524 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
525 this.chargingStationsByTemplate.get(parse(stationTemplateFile).name)!.lastIndex = max(
526 index,
527 this.chargingStationsByTemplate.get(parse(stationTemplateFile).name)?.lastIndex ?? -Infinity
528 )
717c1e56
JB
529 }
530
66a7748d 531 private gracefulShutdown (): void {
f130b8e6
JB
532 this.stop()
533 .then(() => {
5199f9fd 534 console.info(chalk.green('Graceful shutdown'))
66a7748d 535 this.uiServer?.stop()
36adaf06
JB
536 this.waitChargingStationsStopped()
537 .then(() => {
66a7748d 538 exit(exitCodes.succeeded)
36adaf06 539 })
5b2721db 540 .catch(() => {
66a7748d
JB
541 exit(exitCodes.gracefulShutdownError)
542 })
f130b8e6 543 })
a974c8e4 544 .catch(error => {
66a7748d
JB
545 console.error(chalk.red('Error while shutdowning charging stations simulator: '), error)
546 exit(exitCodes.gracefulShutdownError)
547 })
36adaf06 548 }
f130b8e6 549
66a7748d
JB
550 private readonly logPrefix = (): string => {
551 return logPrefix(' Bootstrap |')
552 }
ded13d97 553}