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