refactor(ui): cleanup eslint configuration
[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'
ba9a56a6 8import type { Worker } from 'worker_threads'
8114d10e 9
66a7748d 10import chalk from 'chalk'
ba9a56a6 11import { type MessageHandler, availableParallelism } from 'poolifier'
8114d10e 12
ffb833ec 13import { buildTemplateName, waitChargingStationEvents } from './Helpers.js'
66a7748d
JB
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,
e8237645 28 type InternalTemplateStatistics,
6bd808fd 29 ProcedureName,
e8237645 30 type SimulatorState,
268a74bb 31 type Statistics,
5d049829
JB
32 type StorageConfiguration,
33 type UIServerConfiguration,
66a7748d
JB
34 type WorkerConfiguration
35} from '../types/index.js'
fa5995d6
JB
36import {
37 Configuration,
38 Constants,
e8237645 39 buildTemplateStatisticsPayload,
9bf0ef23
JB
40 formatDurationMilliSeconds,
41 generateUUID,
fa5995d6
JB
42 handleUncaughtException,
43 handleUnhandledRejection,
be0a4d4d 44 isAsyncFunction,
9bf0ef23 45 isNotEmptyArray,
9bf0ef23 46 logPrefix,
e375708d 47 logger
66a7748d
JB
48} from '../utils/index.js'
49import { type WorkerAbstract, WorkerFactory } from '../worker/index.js'
ded13d97 50
66a7748d 51const moduleName = 'Bootstrap'
32de5a57 52
a307349b 53enum exitCodes {
a51a4ead 54 succeeded = 0,
a307349b 55 missingChargingStationsConfiguration = 1,
2f989136
JB
56 duplicateChargingStationTemplateUrls = 2,
57 noChargingStationTemplates = 3,
58 gracefulShutdownError = 4
a307349b 59}
e4cb2c14 60
f130b8e6 61export class Bootstrap extends EventEmitter {
66a7748d 62 private static instance: Bootstrap | null = null
66a7748d 63 private workerImplementation?: WorkerAbstract<ChargingStationWorkerData>
a1cfaa16 64 private readonly uiServer: AbstractUIServer
66a7748d 65 private storage?: Storage
e8237645 66 private readonly templateStatistics: Map<string, InternalTemplateStatistics>
66a7748d
JB
67 private readonly version: string = version
68 private initializedCounters: boolean
69 private started: boolean
70 private starting: boolean
71 private stopping: boolean
a1cfaa16 72 private uiServerStarted: boolean
ded13d97 73
66a7748d
JB
74 private constructor () {
75 super()
6bd808fd 76 for (const signal of ['SIGINT', 'SIGQUIT', 'SIGTERM']) {
66a7748d 77 process.on(signal, this.gracefulShutdown.bind(this))
6bd808fd 78 }
4724a293 79 // Enable unconditionally for now
66a7748d
JB
80 handleUnhandledRejection()
81 handleUncaughtException()
82 this.started = false
83 this.starting = false
84 this.stopping = false
a1cfaa16 85 this.uiServerStarted = false
36adaf06 86 this.uiServer = UIServerFactory.getUIServerImplementation(
66a7748d
JB
87 Configuration.getConfigurationSection<UIServerConfiguration>(ConfigurationSection.uiServer)
88 )
e8237645 89 this.templateStatistics = new Map<string, InternalTemplateStatistics>()
42e341c4
JB
90 this.initializedCounters = false
91 this.initializeCounters()
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,
121 templateStatistics: buildTemplateStatisticsPayload(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 )
66a7748d 178 this.initializeCounters()
5b373a23 179 const workerConfiguration = Configuration.getConfigurationSection<WorkerConfiguration>(
66a7748d
JB
180 ConfigurationSection.worker
181 )
182 this.initializeWorkerImplementation(workerConfiguration)
183 await this.workerImplementation?.start()
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 }
82e9c15a
JB
224 console.info(
225 chalk.green(
226 `Charging stations simulator ${
227 this.version
c5ecc04d 228 } started with ${this.numberOfConfiguredChargingStations} configured charging station(s) from ${this.numberOfChargingStationTemplates} charging station template(s) and ${
2f989136 229 Configuration.workerDynamicPoolInUse() ? `${workerConfiguration.poolMinSize}/` : ''
82e9c15a 230 }${this.workerImplementation?.size}${
2f989136 231 Configuration.workerPoolInUse() ? `/${workerConfiguration.poolMaxSize}` : ''
5b373a23 232 } worker(s) concurrently running in '${workerConfiguration.processType}' mode${
401fa922 233 this.workerImplementation?.maxElementsPerWorker != null
5199f9fd 234 ? ` (${this.workerImplementation.maxElementsPerWorker} charging station(s) per worker)`
82e9c15a 235 : ''
66a7748d
JB
236 }`
237 )
238 )
56e2e1ab
JB
239 Configuration.workerDynamicPoolInUse() &&
240 console.warn(
241 chalk.yellow(
66a7748d
JB
242 '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'
243 )
244 )
245 console.info(chalk.green('Worker set/pool information:'), this.workerImplementation?.info)
246 this.started = true
247 this.starting = false
82e9c15a 248 } else {
66a7748d 249 console.error(chalk.red('Cannot start an already starting charging stations simulator'))
ded13d97 250 }
b322b8b4 251 } else {
66a7748d 252 console.error(chalk.red('Cannot start an already started charging stations simulator'))
ded13d97
JB
253 }
254 }
255
c5ecc04d 256 public async stop (): Promise<void> {
66a7748d
JB
257 if (this.started) {
258 if (!this.stopping) {
259 this.stopping = true
a1cfaa16 260 await this.uiServer.sendInternalRequest(
c5ecc04d
JB
261 this.uiServer.buildProtocolRequest(
262 generateUUID(),
263 ProcedureName.STOP_CHARGING_STATION,
264 Constants.EMPTY_FROZEN_OBJECT
66a7748d 265 )
c5ecc04d
JB
266 )
267 try {
268 await this.waitChargingStationsStopped()
269 } catch (error) {
270 console.error(chalk.red('Error while waiting for charging stations to stop: '), error)
ab7a96fa 271 }
66a7748d
JB
272 await this.workerImplementation?.stop()
273 delete this.workerImplementation
274 this.removeAllListeners()
a1cfaa16 275 this.uiServer.clearCaches()
9d289c63 276 this.initializedCounters = false
66a7748d
JB
277 await this.storage?.close()
278 delete this.storage
66a7748d
JB
279 this.started = false
280 this.stopping = false
82e9c15a 281 } else {
66a7748d 282 console.error(chalk.red('Cannot stop an already stopping charging stations simulator'))
82e9c15a 283 }
b322b8b4 284 } else {
66a7748d 285 console.error(chalk.red('Cannot stop an already stopped charging stations simulator'))
ded13d97 286 }
ded13d97
JB
287 }
288
c5ecc04d
JB
289 private async restart (): Promise<void> {
290 await this.stop()
a1cfaa16
JB
291 if (
292 this.uiServerStarted &&
293 Configuration.getConfigurationSection<UIServerConfiguration>(ConfigurationSection.uiServer)
294 .enabled !== true
295 ) {
296 this.uiServer.stop()
297 this.uiServerStarted = false
298 }
66a7748d 299 await this.start()
ded13d97
JB
300 }
301
66a7748d
JB
302 private async waitChargingStationsStopped (): Promise<string> {
303 return await new Promise<string>((resolve, reject) => {
5b2721db 304 const waitTimeout = setTimeout(() => {
a01134ed 305 const timeoutMessage = `Timeout ${formatDurationMilliSeconds(
66a7748d
JB
306 Constants.STOP_CHARGING_STATIONS_TIMEOUT
307 )} reached at stopping charging stations`
a01134ed
JB
308 console.warn(chalk.yellow(timeoutMessage))
309 reject(new Error(timeoutMessage))
66a7748d 310 }, Constants.STOP_CHARGING_STATIONS_TIMEOUT)
36adaf06
JB
311 waitChargingStationEvents(
312 this,
313 ChargingStationWorkerMessageEvents.stopped,
a01134ed 314 this.numberOfStartedChargingStations
5b2721db
JB
315 )
316 .then(() => {
66a7748d 317 resolve('Charging stations stopped')
5b2721db 318 })
b7ee97c1 319 .catch(reject)
5b2721db 320 .finally(() => {
66a7748d
JB
321 clearTimeout(waitTimeout)
322 })
323 })
36adaf06
JB
324 }
325
66a7748d 326 private initializeWorkerImplementation (workerConfiguration: WorkerConfiguration): void {
c5ecc04d
JB
327 if (!isMainThread) {
328 return
329 }
1feac591 330 let elementsPerWorker: number
5199f9fd 331 switch (workerConfiguration.elementsPerWorker) {
1feac591
JB
332 case 'all':
333 elementsPerWorker = this.numberOfConfiguredChargingStations
334 break
487f0dfd 335 case 'auto':
1feac591 336 default:
487f0dfd 337 elementsPerWorker =
2f989136
JB
338 this.numberOfConfiguredChargingStations > availableParallelism()
339 ? Math.round(this.numberOfConfiguredChargingStations / (availableParallelism() * 1.5))
66a7748d
JB
340 : 1
341 break
8603c1ca 342 }
6d2b7d01
JB
343 this.workerImplementation = WorkerFactory.getWorkerImplementation<ChargingStationWorkerData>(
344 join(
345 dirname(fileURLToPath(import.meta.url)),
66a7748d 346 `ChargingStationWorker${extname(fileURLToPath(import.meta.url))}`
6d2b7d01 347 ),
66a7748d 348 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
6d2b7d01
JB
349 workerConfiguration.processType!,
350 {
351 workerStartDelay: workerConfiguration.startDelay,
352 elementStartDelay: workerConfiguration.elementStartDelay,
66a7748d 353 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
6d2b7d01 354 poolMaxSize: workerConfiguration.poolMaxSize!,
66a7748d 355 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
6d2b7d01 356 poolMinSize: workerConfiguration.poolMinSize!,
1feac591 357 elementsPerWorker,
6d2b7d01 358 poolOptions: {
ba9a56a6 359 messageHandler: this.messageHandler.bind(this) as MessageHandler<Worker>,
66a7748d
JB
360 workerOptions: { resourceLimits: workerConfiguration.resourceLimits }
361 }
362 }
363 )
ded13d97 364 }
81797102 365
66a7748d
JB
366 private messageHandler (
367 msg: ChargingStationWorkerMessage<ChargingStationWorkerMessageData>
32de5a57
LM
368 ): void {
369 // logger.debug(
370 // `${this.logPrefix()} ${moduleName}.messageHandler: Worker channel message received: ${JSON.stringify(
371 // msg,
4ed03b6e 372 // undefined,
66a7748d
JB
373 // 2
374 // )}`
375 // )
32de5a57 376 try {
8cc482a9 377 switch (msg.event) {
244c1396 378 case ChargingStationWorkerMessageEvents.added:
44fccdf0 379 this.emit(ChargingStationWorkerMessageEvents.added, msg.data)
244c1396 380 break
09e5a7a8
JB
381 case ChargingStationWorkerMessageEvents.deleted:
382 this.emit(ChargingStationWorkerMessageEvents.deleted, msg.data)
383 break
721646e9 384 case ChargingStationWorkerMessageEvents.started:
44fccdf0 385 this.emit(ChargingStationWorkerMessageEvents.started, msg.data)
66a7748d 386 break
721646e9 387 case ChargingStationWorkerMessageEvents.stopped:
44fccdf0 388 this.emit(ChargingStationWorkerMessageEvents.stopped, msg.data)
66a7748d 389 break
721646e9 390 case ChargingStationWorkerMessageEvents.updated:
44fccdf0 391 this.emit(ChargingStationWorkerMessageEvents.updated, msg.data)
66a7748d 392 break
721646e9 393 case ChargingStationWorkerMessageEvents.performanceStatistics:
44fccdf0 394 this.emit(ChargingStationWorkerMessageEvents.performanceStatistics, msg.data)
66a7748d 395 break
e1a3f3c1 396 case ChargingStationWorkerMessageEvents.addedWorkerElement:
a492245c 397 this.emit(ChargingStationWorkerMessageEvents.addWorkerElement, msg.data)
e1a3f3c1 398 break
244c1396 399 case ChargingStationWorkerMessageEvents.workerElementError:
244c1396 400 this.emit(ChargingStationWorkerMessageEvents.workerElementError, msg.data)
66a7748d 401 break
32de5a57
LM
402 default:
403 throw new BaseError(
f93dda6a
JB
404 `Unknown charging station worker event: '${
405 msg.event
66a7748d
JB
406 }' received with data: ${JSON.stringify(msg.data, undefined, 2)}`
407 )
32de5a57
LM
408 }
409 } catch (error) {
410 logger.error(
411 `${this.logPrefix()} ${moduleName}.messageHandler: Error occurred while handling '${
8cc482a9 412 msg.event
32de5a57 413 }' event:`,
66a7748d
JB
414 error
415 )
32de5a57
LM
416 }
417 }
418
244c1396 419 private readonly workerEventAdded = (data: ChargingStationData): void => {
a1cfaa16 420 this.uiServer.chargingStations.set(data.stationInfo.hashId, data)
244c1396
JB
421 logger.info(
422 `${this.logPrefix()} ${moduleName}.workerEventAdded: Charging station ${
423 data.stationInfo.chargingStationId
424 } (hashId: ${data.stationInfo.hashId}) added (${
425 this.numberOfAddedChargingStations
426 } added from ${this.numberOfConfiguredChargingStations} configured charging station(s))`
09e5a7a8
JB
427 )
428 }
429
430 private readonly workerEventDeleted = (data: ChargingStationData): void => {
a1cfaa16 431 this.uiServer.chargingStations.delete(data.stationInfo.hashId)
09e5a7a8 432 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
e8237645
JB
433 const templateStatistics = this.templateStatistics.get(data.stationInfo.templateName)!
434 --templateStatistics.added
435 templateStatistics.indexes.delete(data.stationInfo.templateIndex)
09e5a7a8
JB
436 logger.info(
437 `${this.logPrefix()} ${moduleName}.workerEventDeleted: Charging station ${
438 data.stationInfo.chargingStationId
439 } (hashId: ${data.stationInfo.hashId}) deleted (${
440 this.numberOfAddedChargingStations
441 } added from ${this.numberOfConfiguredChargingStations} configured charging station(s))`
244c1396
JB
442 )
443 }
444
66a7748d 445 private readonly workerEventStarted = (data: ChargingStationData): void => {
a1cfaa16 446 this.uiServer.chargingStations.set(data.stationInfo.hashId, data)
2f989136 447 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
e8237645 448 ++this.templateStatistics.get(data.stationInfo.templateName)!.started
56eb297e 449 logger.info(
e6159ce8 450 `${this.logPrefix()} ${moduleName}.workerEventStarted: Charging station ${
56eb297e 451 data.stationInfo.chargingStationId
e6159ce8 452 } (hashId: ${data.stationInfo.hashId}) started (${
56eb297e 453 this.numberOfStartedChargingStations
244c1396 454 } started from ${this.numberOfAddedChargingStations} added charging station(s))`
66a7748d
JB
455 )
456 }
32de5a57 457
66a7748d 458 private readonly workerEventStopped = (data: ChargingStationData): void => {
a1cfaa16 459 this.uiServer.chargingStations.set(data.stationInfo.hashId, data)
2f989136 460 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
e8237645 461 --this.templateStatistics.get(data.stationInfo.templateName)!.started
56eb297e 462 logger.info(
e6159ce8 463 `${this.logPrefix()} ${moduleName}.workerEventStopped: Charging station ${
56eb297e 464 data.stationInfo.chargingStationId
e6159ce8 465 } (hashId: ${data.stationInfo.hashId}) stopped (${
56eb297e 466 this.numberOfStartedChargingStations
244c1396 467 } started from ${this.numberOfAddedChargingStations} added charging station(s))`
66a7748d
JB
468 )
469 }
32de5a57 470
66a7748d 471 private readonly workerEventUpdated = (data: ChargingStationData): void => {
a1cfaa16 472 this.uiServer.chargingStations.set(data.stationInfo.hashId, data)
66a7748d 473 }
32de5a57 474
66a7748d 475 private readonly workerEventPerformanceStatistics = (data: Statistics): void => {
be0a4d4d
JB
476 // eslint-disable-next-line @typescript-eslint/unbound-method
477 if (isAsyncFunction(this.storage?.storePerformanceStatistics)) {
478 (
479 this.storage.storePerformanceStatistics as (
480 performanceStatistics: Statistics
481 ) => Promise<void>
482 )(data).catch(Constants.EMPTY_FUNCTION)
483 } else {
484 (this.storage?.storePerformanceStatistics as (performanceStatistics: Statistics) => void)(
485 data
486 )
487 }
66a7748d 488 }
32de5a57 489
66a7748d
JB
490 private initializeCounters (): void {
491 if (!this.initializedCounters) {
66a7748d
JB
492 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
493 const stationTemplateUrls = Configuration.getStationTemplateUrls()!
9bf0ef23 494 if (isNotEmptyArray(stationTemplateUrls)) {
7436ee0d 495 for (const stationTemplateUrl of stationTemplateUrls) {
a33026fe 496 const templateName = buildTemplateName(stationTemplateUrl.file)
e8237645 497 this.templateStatistics.set(templateName, {
2f989136 498 configured: stationTemplateUrl.numberOfStations,
244c1396 499 added: 0,
c5ecc04d 500 started: 0,
e375708d 501 indexes: new Set<number>()
2f989136 502 })
a1cfaa16 503 this.uiServer.chargingStationTemplates.add(templateName)
2f989136 504 }
e8237645 505 if (this.templateStatistics.size !== stationTemplateUrls.length) {
2f989136
JB
506 console.error(
507 chalk.red(
508 "'stationTemplateUrls' contains duplicate entries, please check your configuration"
509 )
510 )
511 exit(exitCodes.duplicateChargingStationTemplateUrls)
7436ee0d 512 }
a596d200 513 } else {
2f989136
JB
514 console.error(
515 chalk.red("'stationTemplateUrls' not defined or empty, please check your configuration")
66a7748d
JB
516 )
517 exit(exitCodes.missingChargingStationsConfiguration)
a596d200 518 }
c5ecc04d
JB
519 if (
520 this.numberOfConfiguredChargingStations === 0 &&
521 Configuration.getConfigurationSection<UIServerConfiguration>(ConfigurationSection.uiServer)
baf34a77 522 .enabled !== true
c5ecc04d 523 ) {
2f989136
JB
524 console.error(
525 chalk.red(
c5ecc04d 526 "'stationTemplateUrls' has no charging station enabled and UI server is disabled, please check your configuration"
2f989136
JB
527 )
528 )
66a7748d 529 exit(exitCodes.noChargingStationTemplates)
a596d200 530 }
66a7748d 531 this.initializedCounters = true
846d2851 532 }
7c72977b
JB
533 }
534
71ac2bd7
JB
535 public async addChargingStation (
536 index: number,
a33026fe 537 templateFile: string,
71ac2bd7
JB
538 options?: ChargingStationOptions
539 ): Promise<void> {
f6cb1767
JB
540 if (!this.started && !this.starting) {
541 throw new BaseError(
2762ad62 542 'Cannot add charging station while the charging stations simulator is not started'
f6cb1767
JB
543 )
544 }
6ed3c845 545 await this.workerImplementation?.addElement({
717c1e56 546 index,
d972af76
JB
547 templateFile: join(
548 dirname(fileURLToPath(import.meta.url)),
e7aeea18
JB
549 'assets',
550 'station-templates',
a33026fe 551 templateFile
71ac2bd7
JB
552 ),
553 options
66a7748d 554 })
c5ecc04d 555 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
a33026fe 556 const templateStatistics = this.templateStatistics.get(buildTemplateName(templateFile))!
e8237645
JB
557 ++templateStatistics.added
558 templateStatistics.indexes.add(index)
717c1e56
JB
559 }
560
66a7748d 561 private gracefulShutdown (): void {
f130b8e6
JB
562 this.stop()
563 .then(() => {
5199f9fd 564 console.info(chalk.green('Graceful shutdown'))
a1cfaa16
JB
565 this.uiServer.stop()
566 this.uiServerStarted = false
36adaf06
JB
567 this.waitChargingStationsStopped()
568 .then(() => {
66a7748d 569 exit(exitCodes.succeeded)
36adaf06 570 })
5b2721db 571 .catch(() => {
66a7748d
JB
572 exit(exitCodes.gracefulShutdownError)
573 })
f130b8e6 574 })
a974c8e4 575 .catch(error => {
66a7748d
JB
576 console.error(chalk.red('Error while shutdowning charging stations simulator: '), error)
577 exit(exitCodes.gracefulShutdownError)
578 })
36adaf06 579 }
f130b8e6 580
66a7748d
JB
581 private readonly logPrefix = (): string => {
582 return logPrefix(' Bootstrap |')
583 }
ded13d97 584}