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