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