refactor(ui): align text casing
[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 }
66a7748d 303 let elementsPerWorker: number | undefined
5199f9fd 304 switch (workerConfiguration.elementsPerWorker) {
487f0dfd
JB
305 case 'auto':
306 elementsPerWorker =
2f989136
JB
307 this.numberOfConfiguredChargingStations > availableParallelism()
308 ? Math.round(this.numberOfConfiguredChargingStations / (availableParallelism() * 1.5))
66a7748d
JB
309 : 1
310 break
c20d5d72 311 case 'all':
2f989136 312 elementsPerWorker = this.numberOfConfiguredChargingStations
66a7748d 313 break
8603c1ca 314 }
6d2b7d01
JB
315 this.workerImplementation = WorkerFactory.getWorkerImplementation<ChargingStationWorkerData>(
316 join(
317 dirname(fileURLToPath(import.meta.url)),
66a7748d 318 `ChargingStationWorker${extname(fileURLToPath(import.meta.url))}`
6d2b7d01 319 ),
66a7748d 320 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
6d2b7d01
JB
321 workerConfiguration.processType!,
322 {
323 workerStartDelay: workerConfiguration.startDelay,
324 elementStartDelay: workerConfiguration.elementStartDelay,
66a7748d 325 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
6d2b7d01 326 poolMaxSize: workerConfiguration.poolMaxSize!,
66a7748d 327 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
6d2b7d01
JB
328 poolMinSize: workerConfiguration.poolMinSize!,
329 elementsPerWorker: elementsPerWorker ?? (workerConfiguration.elementsPerWorker as number),
330 poolOptions: {
ba9a56a6 331 messageHandler: this.messageHandler.bind(this) as MessageHandler<Worker>,
66a7748d
JB
332 workerOptions: { resourceLimits: workerConfiguration.resourceLimits }
333 }
334 }
335 )
ded13d97 336 }
81797102 337
66a7748d
JB
338 private messageHandler (
339 msg: ChargingStationWorkerMessage<ChargingStationWorkerMessageData>
32de5a57
LM
340 ): void {
341 // logger.debug(
342 // `${this.logPrefix()} ${moduleName}.messageHandler: Worker channel message received: ${JSON.stringify(
343 // msg,
4ed03b6e 344 // undefined,
66a7748d
JB
345 // 2
346 // )}`
347 // )
32de5a57 348 try {
8cc482a9 349 switch (msg.event) {
244c1396 350 case ChargingStationWorkerMessageEvents.added:
44fccdf0 351 this.emit(ChargingStationWorkerMessageEvents.added, msg.data)
244c1396 352 break
721646e9 353 case ChargingStationWorkerMessageEvents.started:
44fccdf0 354 this.emit(ChargingStationWorkerMessageEvents.started, msg.data)
66a7748d 355 break
721646e9 356 case ChargingStationWorkerMessageEvents.stopped:
44fccdf0 357 this.emit(ChargingStationWorkerMessageEvents.stopped, msg.data)
66a7748d 358 break
721646e9 359 case ChargingStationWorkerMessageEvents.updated:
44fccdf0 360 this.emit(ChargingStationWorkerMessageEvents.updated, msg.data)
66a7748d 361 break
721646e9 362 case ChargingStationWorkerMessageEvents.performanceStatistics:
44fccdf0 363 this.emit(ChargingStationWorkerMessageEvents.performanceStatistics, msg.data)
66a7748d 364 break
e1a3f3c1 365 case ChargingStationWorkerMessageEvents.addedWorkerElement:
a492245c 366 this.emit(ChargingStationWorkerMessageEvents.addWorkerElement, msg.data)
e1a3f3c1 367 break
244c1396 368 case ChargingStationWorkerMessageEvents.workerElementError:
244c1396 369 this.emit(ChargingStationWorkerMessageEvents.workerElementError, msg.data)
66a7748d 370 break
32de5a57
LM
371 default:
372 throw new BaseError(
f93dda6a
JB
373 `Unknown charging station worker event: '${
374 msg.event
66a7748d
JB
375 }' received with data: ${JSON.stringify(msg.data, undefined, 2)}`
376 )
32de5a57
LM
377 }
378 } catch (error) {
379 logger.error(
380 `${this.logPrefix()} ${moduleName}.messageHandler: Error occurred while handling '${
8cc482a9 381 msg.event
32de5a57 382 }' event:`,
66a7748d
JB
383 error
384 )
32de5a57
LM
385 }
386 }
387
244c1396
JB
388 private readonly workerEventAdded = (data: ChargingStationData): void => {
389 this.uiServer?.chargingStations.set(data.stationInfo.hashId, data)
390 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
391 ++this.chargingStationsByTemplate.get(data.stationInfo.templateName)!.added
392 logger.info(
393 `${this.logPrefix()} ${moduleName}.workerEventAdded: Charging station ${
394 data.stationInfo.chargingStationId
395 } (hashId: ${data.stationInfo.hashId}) added (${
396 this.numberOfAddedChargingStations
397 } added from ${this.numberOfConfiguredChargingStations} configured charging station(s))`
398 )
399 }
400
66a7748d
JB
401 private readonly workerEventStarted = (data: ChargingStationData): void => {
402 this.uiServer?.chargingStations.set(data.stationInfo.hashId, data)
2f989136
JB
403 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
404 ++this.chargingStationsByTemplate.get(data.stationInfo.templateName)!.started
56eb297e 405 logger.info(
e6159ce8 406 `${this.logPrefix()} ${moduleName}.workerEventStarted: Charging station ${
56eb297e 407 data.stationInfo.chargingStationId
e6159ce8 408 } (hashId: ${data.stationInfo.hashId}) started (${
56eb297e 409 this.numberOfStartedChargingStations
244c1396 410 } started from ${this.numberOfAddedChargingStations} added charging station(s))`
66a7748d
JB
411 )
412 }
32de5a57 413
66a7748d
JB
414 private readonly workerEventStopped = (data: ChargingStationData): void => {
415 this.uiServer?.chargingStations.set(data.stationInfo.hashId, data)
2f989136
JB
416 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
417 --this.chargingStationsByTemplate.get(data.stationInfo.templateName)!.started
56eb297e 418 logger.info(
e6159ce8 419 `${this.logPrefix()} ${moduleName}.workerEventStopped: Charging station ${
56eb297e 420 data.stationInfo.chargingStationId
e6159ce8 421 } (hashId: ${data.stationInfo.hashId}) stopped (${
56eb297e 422 this.numberOfStartedChargingStations
244c1396 423 } started from ${this.numberOfAddedChargingStations} added charging station(s))`
66a7748d
JB
424 )
425 }
32de5a57 426
66a7748d
JB
427 private readonly workerEventUpdated = (data: ChargingStationData): void => {
428 this.uiServer?.chargingStations.set(data.stationInfo.hashId, data)
429 }
32de5a57 430
66a7748d 431 private readonly workerEventPerformanceStatistics = (data: Statistics): void => {
be0a4d4d
JB
432 // eslint-disable-next-line @typescript-eslint/unbound-method
433 if (isAsyncFunction(this.storage?.storePerformanceStatistics)) {
434 (
435 this.storage.storePerformanceStatistics as (
436 performanceStatistics: Statistics
437 ) => Promise<void>
438 )(data).catch(Constants.EMPTY_FUNCTION)
439 } else {
440 (this.storage?.storePerformanceStatistics as (performanceStatistics: Statistics) => void)(
441 data
442 )
443 }
66a7748d 444 }
32de5a57 445
66a7748d
JB
446 private initializeCounters (): void {
447 if (!this.initializedCounters) {
66a7748d
JB
448 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
449 const stationTemplateUrls = Configuration.getStationTemplateUrls()!
9bf0ef23 450 if (isNotEmptyArray(stationTemplateUrls)) {
7436ee0d 451 for (const stationTemplateUrl of stationTemplateUrls) {
2f989136
JB
452 const templateName = parse(stationTemplateUrl.file).name
453 this.chargingStationsByTemplate.set(templateName, {
454 configured: stationTemplateUrl.numberOfStations,
244c1396 455 added: 0,
c5ecc04d
JB
456 started: 0,
457 lastIndex: 0
2f989136
JB
458 })
459 this.uiServer?.chargingStationTemplates.add(templateName)
460 }
461 if (this.chargingStationsByTemplate.size !== stationTemplateUrls.length) {
462 console.error(
463 chalk.red(
464 "'stationTemplateUrls' contains duplicate entries, please check your configuration"
465 )
466 )
467 exit(exitCodes.duplicateChargingStationTemplateUrls)
7436ee0d 468 }
a596d200 469 } else {
2f989136
JB
470 console.error(
471 chalk.red("'stationTemplateUrls' not defined or empty, please check your configuration")
66a7748d
JB
472 )
473 exit(exitCodes.missingChargingStationsConfiguration)
a596d200 474 }
c5ecc04d
JB
475 if (
476 this.numberOfConfiguredChargingStations === 0 &&
477 Configuration.getConfigurationSection<UIServerConfiguration>(ConfigurationSection.uiServer)
baf34a77 478 .enabled !== true
c5ecc04d 479 ) {
2f989136
JB
480 console.error(
481 chalk.red(
c5ecc04d 482 "'stationTemplateUrls' has no charging station enabled and UI server is disabled, please check your configuration"
2f989136
JB
483 )
484 )
66a7748d 485 exit(exitCodes.noChargingStationTemplates)
a596d200 486 }
66a7748d 487 this.initializedCounters = true
846d2851 488 }
7c72977b
JB
489 }
490
71ac2bd7
JB
491 public async addChargingStation (
492 index: number,
493 stationTemplateFile: string,
494 options?: ChargingStationOptions
495 ): Promise<void> {
6ed3c845 496 await this.workerImplementation?.addElement({
717c1e56 497 index,
d972af76
JB
498 templateFile: join(
499 dirname(fileURLToPath(import.meta.url)),
e7aeea18
JB
500 'assets',
501 'station-templates',
c5ecc04d 502 stationTemplateFile
71ac2bd7
JB
503 ),
504 options
66a7748d 505 })
c5ecc04d
JB
506 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
507 this.chargingStationsByTemplate.get(parse(stationTemplateFile).name)!.lastIndex = max(
508 index,
509 this.chargingStationsByTemplate.get(parse(stationTemplateFile).name)?.lastIndex ?? -Infinity
510 )
717c1e56
JB
511 }
512
66a7748d 513 private gracefulShutdown (): void {
f130b8e6
JB
514 this.stop()
515 .then(() => {
5199f9fd 516 console.info(chalk.green('Graceful shutdown'))
66a7748d 517 this.uiServer?.stop()
36adaf06
JB
518 this.waitChargingStationsStopped()
519 .then(() => {
66a7748d 520 exit(exitCodes.succeeded)
36adaf06 521 })
5b2721db 522 .catch(() => {
66a7748d
JB
523 exit(exitCodes.gracefulShutdownError)
524 })
f130b8e6 525 })
a974c8e4 526 .catch(error => {
66a7748d
JB
527 console.error(chalk.red('Error while shutdowning charging stations simulator: '), error)
528 exit(exitCodes.gracefulShutdownError)
529 })
36adaf06 530 }
f130b8e6 531
66a7748d
JB
532 private readonly logPrefix = (): string => {
533 return logPrefix(' Bootstrap |')
534 }
ded13d97 535}