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