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