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