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