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