fix: fix worker event error handling
[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,
887a125e 155 (eventError: ChargingStationWorkerEventError) => {
44fccdf0 156 logger.error(
887a125e
JB
157 `${this.logPrefix()} ${moduleName}.messageHandler: Error occurred while handling '${eventError.event}' event on worker:`,
158 eventError
44fccdf0
JB
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 364 case ChargingStationWorkerMessageEvents.addedWorkerElement:
a492245c 365 this.emit(ChargingStationWorkerMessageEvents.addWorkerElement, msg.data)
e1a3f3c1 366 break
244c1396 367 case ChargingStationWorkerMessageEvents.workerElementError:
244c1396 368 this.emit(ChargingStationWorkerMessageEvents.workerElementError, msg.data)
66a7748d 369 break
32de5a57
LM
370 default:
371 throw new BaseError(
f93dda6a
JB
372 `Unknown charging station worker event: '${
373 msg.event
66a7748d
JB
374 }' received with data: ${JSON.stringify(msg.data, undefined, 2)}`
375 )
32de5a57
LM
376 }
377 } catch (error) {
378 logger.error(
379 `${this.logPrefix()} ${moduleName}.messageHandler: Error occurred while handling '${
8cc482a9 380 msg.event
32de5a57 381 }' event:`,
66a7748d
JB
382 error
383 )
32de5a57
LM
384 }
385 }
386
244c1396
JB
387 private readonly workerEventAdded = (data: ChargingStationData): void => {
388 this.uiServer?.chargingStations.set(data.stationInfo.hashId, data)
389 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
390 ++this.chargingStationsByTemplate.get(data.stationInfo.templateName)!.added
391 logger.info(
392 `${this.logPrefix()} ${moduleName}.workerEventAdded: Charging station ${
393 data.stationInfo.chargingStationId
394 } (hashId: ${data.stationInfo.hashId}) added (${
395 this.numberOfAddedChargingStations
396 } added from ${this.numberOfConfiguredChargingStations} configured charging station(s))`
397 )
398 }
399
66a7748d
JB
400 private readonly workerEventStarted = (data: ChargingStationData): void => {
401 this.uiServer?.chargingStations.set(data.stationInfo.hashId, data)
2f989136
JB
402 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
403 ++this.chargingStationsByTemplate.get(data.stationInfo.templateName)!.started
56eb297e 404 logger.info(
e6159ce8 405 `${this.logPrefix()} ${moduleName}.workerEventStarted: Charging station ${
56eb297e 406 data.stationInfo.chargingStationId
e6159ce8 407 } (hashId: ${data.stationInfo.hashId}) started (${
56eb297e 408 this.numberOfStartedChargingStations
244c1396 409 } started from ${this.numberOfAddedChargingStations} added charging station(s))`
66a7748d
JB
410 )
411 }
32de5a57 412
66a7748d
JB
413 private readonly workerEventStopped = (data: ChargingStationData): void => {
414 this.uiServer?.chargingStations.set(data.stationInfo.hashId, data)
2f989136
JB
415 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
416 --this.chargingStationsByTemplate.get(data.stationInfo.templateName)!.started
56eb297e 417 logger.info(
e6159ce8 418 `${this.logPrefix()} ${moduleName}.workerEventStopped: Charging station ${
56eb297e 419 data.stationInfo.chargingStationId
e6159ce8 420 } (hashId: ${data.stationInfo.hashId}) stopped (${
56eb297e 421 this.numberOfStartedChargingStations
244c1396 422 } started from ${this.numberOfAddedChargingStations} added charging station(s))`
66a7748d
JB
423 )
424 }
32de5a57 425
66a7748d
JB
426 private readonly workerEventUpdated = (data: ChargingStationData): void => {
427 this.uiServer?.chargingStations.set(data.stationInfo.hashId, data)
428 }
32de5a57 429
66a7748d 430 private readonly workerEventPerformanceStatistics = (data: Statistics): void => {
be0a4d4d
JB
431 // eslint-disable-next-line @typescript-eslint/unbound-method
432 if (isAsyncFunction(this.storage?.storePerformanceStatistics)) {
433 (
434 this.storage.storePerformanceStatistics as (
435 performanceStatistics: Statistics
436 ) => Promise<void>
437 )(data).catch(Constants.EMPTY_FUNCTION)
438 } else {
439 (this.storage?.storePerformanceStatistics as (performanceStatistics: Statistics) => void)(
440 data
441 )
442 }
66a7748d 443 }
32de5a57 444
66a7748d
JB
445 private initializeCounters (): void {
446 if (!this.initializedCounters) {
66a7748d
JB
447 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
448 const stationTemplateUrls = Configuration.getStationTemplateUrls()!
9bf0ef23 449 if (isNotEmptyArray(stationTemplateUrls)) {
7436ee0d 450 for (const stationTemplateUrl of stationTemplateUrls) {
2f989136
JB
451 const templateName = parse(stationTemplateUrl.file).name
452 this.chargingStationsByTemplate.set(templateName, {
453 configured: stationTemplateUrl.numberOfStations,
244c1396 454 added: 0,
c5ecc04d
JB
455 started: 0,
456 lastIndex: 0
2f989136
JB
457 })
458 this.uiServer?.chargingStationTemplates.add(templateName)
459 }
460 if (this.chargingStationsByTemplate.size !== stationTemplateUrls.length) {
461 console.error(
462 chalk.red(
463 "'stationTemplateUrls' contains duplicate entries, please check your configuration"
464 )
465 )
466 exit(exitCodes.duplicateChargingStationTemplateUrls)
7436ee0d 467 }
a596d200 468 } else {
2f989136
JB
469 console.error(
470 chalk.red("'stationTemplateUrls' not defined or empty, please check your configuration")
66a7748d
JB
471 )
472 exit(exitCodes.missingChargingStationsConfiguration)
a596d200 473 }
c5ecc04d
JB
474 if (
475 this.numberOfConfiguredChargingStations === 0 &&
476 Configuration.getConfigurationSection<UIServerConfiguration>(ConfigurationSection.uiServer)
baf34a77 477 .enabled !== true
c5ecc04d 478 ) {
2f989136
JB
479 console.error(
480 chalk.red(
c5ecc04d 481 "'stationTemplateUrls' has no charging station enabled and UI server is disabled, please check your configuration"
2f989136
JB
482 )
483 )
66a7748d 484 exit(exitCodes.noChargingStationTemplates)
a596d200 485 }
66a7748d 486 this.initializedCounters = true
846d2851 487 }
7c72977b
JB
488 }
489
c5ecc04d 490 public async addChargingStation (index: number, stationTemplateFile: string): Promise<void> {
6ed3c845 491 await this.workerImplementation?.addElement({
717c1e56 492 index,
d972af76
JB
493 templateFile: join(
494 dirname(fileURLToPath(import.meta.url)),
e7aeea18
JB
495 'assets',
496 'station-templates',
c5ecc04d 497 stationTemplateFile
66a7748d
JB
498 )
499 })
c5ecc04d
JB
500 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
501 this.chargingStationsByTemplate.get(parse(stationTemplateFile).name)!.lastIndex = max(
502 index,
503 this.chargingStationsByTemplate.get(parse(stationTemplateFile).name)?.lastIndex ?? -Infinity
504 )
717c1e56
JB
505 }
506
66a7748d 507 private gracefulShutdown (): void {
f130b8e6
JB
508 this.stop()
509 .then(() => {
5199f9fd 510 console.info(chalk.green('Graceful shutdown'))
66a7748d 511 this.uiServer?.stop()
36adaf06
JB
512 this.waitChargingStationsStopped()
513 .then(() => {
66a7748d 514 exit(exitCodes.succeeded)
36adaf06 515 })
5b2721db 516 .catch(() => {
66a7748d
JB
517 exit(exitCodes.gracefulShutdownError)
518 })
f130b8e6 519 })
a974c8e4 520 .catch(error => {
66a7748d
JB
521 console.error(chalk.red('Error while shutdowning charging stations simulator: '), error)
522 exit(exitCodes.gracefulShutdownError)
523 })
36adaf06 524 }
f130b8e6 525
66a7748d
JB
526 private readonly logPrefix = (): string => {
527 return logPrefix(' Bootstrap |')
528 }
ded13d97 529}