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