diff --git a/app/controllers/cwmp_controller.ts b/app/controllers/cwmp_controller.ts index 11371e2..03a2f48 100644 --- a/app/controllers/cwmp_controller.ts +++ b/app/controllers/cwmp_controller.ts @@ -61,7 +61,12 @@ export default class CwmpController { } logger.debug( - { sessionId, remoteKey, messageType: message?.type ?? 'none', cwmpId: envelope?.header?.id }, + { + sessionId, + remoteKey, + messageType: message?.type ?? 'none', + cwmpId: envelope?.header?.id, + }, 'CWMP message routing' ) @@ -76,7 +81,10 @@ export default class CwmpController { request.header('x-forwarded-for') || '' ) ctx.cwmpDeviceId = hashId - logger.debug({ sessionId, hashId, responseLength: xml.length }, 'CWMP sending InformResponse') + logger.debug( + { sessionId, hashId, responseLength: xml.length }, + 'CWMP sending InformResponse' + ) return this.sendSoap(response, xml) } @@ -140,10 +148,17 @@ export default class CwmpController { let device = await this.loadDevice(deviceId) if (!device) { device = new Device(deviceId) - logger.info({ deviceId: deviceId.toIdentifier() }, '[handleInform] Device not found in DB — creating new') + logger.info( + { deviceId: deviceId.toIdentifier() }, + '[handleInform] Device not found in DB — creating new' + ) } else { logger.info( - { deviceId: deviceId.toIdentifier(), gpnStatus: device.gpnStatus, circuitBreaker: device.gpnCircuitBreaker }, + { + deviceId: deviceId.toIdentifier(), + gpnStatus: device.gpnStatus, + circuitBreaker: device.gpnCircuitBreaker, + }, '[handleInform] Device loaded from DB' ) } @@ -197,7 +212,7 @@ export default class CwmpController { : [] const events: string[] = eventStructArray .map((e) => e?.EventCode) - .filter((c) => c != null) + .filter((c) => c !== null && c !== undefined) .map((c) => String(c)) logger.info({ events }, 'CWMP Inform events extracted') @@ -245,11 +260,17 @@ export default class CwmpController { { identifier: hashId }, { $set: { references, updatedAt: new Date() } } ).catch(() => {}) - logger.debug({ hashId, providers: references.map((r) => r.provider) }, '[References] Resolved') + logger.debug( + { hashId, providers: references.map((r) => r.provider) }, + '[References] Resolved' + ) } } catch (_e: unknown) { const err = _e as Error - logger.error({ error: err?.message, stack: err?.stack }, '[References] Error during resolution') + logger.error( + { error: err?.message, stack: err?.stack }, + '[References] Error during resolution' + ) } return { xml: CWMPMessageBuilder.informResponse(cwmpId), hashId } @@ -290,10 +311,14 @@ export default class CwmpController { ) record.properties?.forEach((p: { name: string; value: unknown; type: string }) => - device.setProperty(new DeviceProperty(p.name, p.value, (p.type as ParameterType) ?? ParameterType.UNKNOWN)) + device.setProperty( + new DeviceProperty(p.name, p.value, (p.type as ParameterType) ?? ParameterType.UNKNOWN) + ) ) record.parameters?.forEach((p: { name: string; value: unknown; type: string }) => - device.setParameter(new DeviceParameter(p.name, p.value, (p.type as ParameterType) ?? ParameterType.UNKNOWN)) + device.setParameter( + new DeviceParameter(p.name, p.value, (p.type as ParameterType) ?? ParameterType.UNKNOWN) + ) ) device.setGpnStatus(record.gpnStatus ?? 'unknown') device.setDataModel(record.dataModel ?? 'unknown') diff --git a/app/domain/device/device.ts b/app/domain/device/device.ts index 94530f8..c933951 100644 --- a/app/domain/device/device.ts +++ b/app/domain/device/device.ts @@ -1,5 +1,10 @@ -import { DeviceId } from './device_id.js' -import { DeviceParameter, DeviceProperty, ParameterSnapshot, PropertySnapshot } from './device_parameter.js' +import { type DeviceId } from './device_id.js' +import { + DeviceParameter, + DeviceProperty, + type ParameterSnapshot, + type PropertySnapshot, +} from './device_parameter.js' import { ParameterType } from '../shared/types.js' import { createHash } from 'node:crypto' import type { ParameterValueStruct, ParameterInfoStruct } from '../../protocol/tr069_types.js' @@ -271,12 +276,9 @@ export class Device { * - *.ParameterKey */ public reclassifyInformWritables(paramValues: ParameterValueStruct[]): void { - const WRITABLE_PATTERNS = [ - /(^|\.)ProvisioningCode$/, - /(^|\.)ParameterKey$/, - ] + const WRITABLE_PATTERNS = [/(^|\.)ProvisioningCode$/, /(^|\.)ParameterKey$/] for (const pv of paramValues || []) { - const name = typeof pv?.Name === 'string' ? pv.Name : (pv?.Name as { _?: string })?._ || null + const name = typeof pv?.Name === 'string' ? pv.Name : (pv?.Name as { _?: string })?._ || null if (!name) continue if (!WRITABLE_PATTERNS.some((re) => re.test(name))) continue // Only act if it ended up as a property (wrong classification) @@ -357,7 +359,7 @@ export class Device { const hasTextNode = valueObj && Object.prototype.hasOwnProperty.call(valueObj, '_') const valueObjRaw = valueObj as unknown as Record | undefined const hasTypeAttr = valueObjRaw && (valueObjRaw['$xsi:type'] || valueObj?.$?.Type) - const val = hasTextNode ? valueObj?._ : (hasTypeAttr ? '' : undefined) + const val = hasTextNode ? valueObj?._ : hasTypeAttr ? '' : undefined const typeStr = (valueObjRaw?.['$xsi:type'] as string | undefined) || valueObj?.$?.Type const mappedType = this.mapType(typeStr) @@ -370,7 +372,15 @@ export class Device { const snap = existing.toSnapshot() this._parameters.set( name, - new DeviceParameter(name, snap.value, mappedType, snap.createdAt, snap.updatedAt, snap.notification, snap.accessList) + new DeviceParameter( + name, + snap.value, + mappedType, + snap.createdAt, + snap.updatedAt, + snap.notification, + snap.accessList + ) ) } this.updateParameter(name, val) @@ -380,7 +390,15 @@ export class Device { const snap = existing.toSnapshot() this._properties.set( name, - new DeviceProperty(name, snap.value, mappedType, snap.createdAt, snap.updatedAt, snap.notification, snap.accessList) + new DeviceProperty( + name, + snap.value, + mappedType, + snap.createdAt, + snap.updatedAt, + snap.notification, + snap.accessList + ) ) } this.updateProperty(name, val) diff --git a/app/middleware/cwmp_middleware.ts b/app/middleware/cwmp_middleware.ts index 3b3943e..6824904 100644 --- a/app/middleware/cwmp_middleware.ts +++ b/app/middleware/cwmp_middleware.ts @@ -82,7 +82,10 @@ export default class CwmpMiddleware { cwmpVersion: envelope.cwmpVersion, cwmpId: envelope.header?.id, messageType: message?.type ?? 'unknown', - messageKeys: message?.data && typeof message.data === 'object' ? Object.keys(message.data as object) : [], + messageKeys: + message?.data && typeof message.data === 'object' + ? Object.keys(message.data as object) + : [], }, 'CWMP XML parsed' ) diff --git a/app/models/configuration_version.ts b/app/models/configuration_version.ts index 2d9fd2d..fc4768b 100644 --- a/app/models/configuration_version.ts +++ b/app/models/configuration_version.ts @@ -1,5 +1,5 @@ import { BaseModel } from '#database/base_model' -import { ConfigurationVersion } from '../domain/configuration/configuration_version.js' +import { type ConfigurationVersion } from '../domain/configuration/configuration_version.js' import { Sort } from 'mongodb' export default class ConfigurationVersionModel extends BaseModel { diff --git a/app/protocol/common/cwmp_message_builder.ts b/app/protocol/common/cwmp_message_builder.ts index 67b996c..514bf47 100644 --- a/app/protocol/common/cwmp_message_builder.ts +++ b/app/protocol/common/cwmp_message_builder.ts @@ -3,7 +3,7 @@ import { Cwmp_1_0, Download, FactoryReset, - FaultCodeType, + type FaultCodeType, FaultDetail, FaultStruct, Fault, @@ -26,10 +26,15 @@ import { CwmpID, NAMESPACES, SoapEnvelope, - SoapEnvelopeAttribute, + type SoapEnvelopeAttribute, SoapEnvelopeHeader, } from '../soap_envelope.js' -import type { DownloadTaskPayload, UploadTaskPayload, SetParameterValuesTaskPayload, SetParameterAttributesTaskPayload } from '../tr069_types.js' +import type { + DownloadTaskPayload, + UploadTaskPayload, + SetParameterValuesTaskPayload, + SetParameterAttributesTaskPayload, +} from '../tr069_types.js' function generateCwmpID(): string { return randomBytes(4).toString('hex') @@ -54,7 +59,11 @@ export class CWMPMessageBuilder { return enve.getXml() } - static getParameterNames(cwmpId: string | undefined, parameterPath: string, nextLevel: boolean = true): string { + static getParameterNames( + cwmpId: string | undefined, + parameterPath: string, + nextLevel: boolean = true + ): string { const enve = makeEnvelope(cwmpId) enve.Body.GetParameterNames = new GetParameterNames({ ParameterPath: parameterPath, @@ -89,10 +98,13 @@ export class CWMPMessageBuilder { return enve.getXml() } - static setParameterValuesFromPayload(cwmpId: string | undefined, payload: SetParameterValuesTaskPayload): string { + static setParameterValuesFromPayload( + cwmpId: string | undefined, + payload: SetParameterValuesTaskPayload + ): string { const params = (payload.parameterValueList || []).map((p) => ({ name: p.Name, - value: String(p.Value?._ ?? ''), + value: String(p.Value?._ ?? ''), type: p.Value?.$?.Type || 'xsd:string', })) return CWMPMessageBuilder.setParameterValues(cwmpId, params) @@ -140,7 +152,11 @@ export class CWMPMessageBuilder { return enve.getXml() } - static scheduleInform(cwmpId: string | undefined, delaySeconds: number, commandKey?: string): string { + static scheduleInform( + cwmpId: string | undefined, + delaySeconds: number, + commandKey?: string + ): string { const enve = makeEnvelope(cwmpId) enve.Body.ScheduleInform = new ScheduleInform({ DelaySeconds: delaySeconds, @@ -151,7 +167,9 @@ export class CWMPMessageBuilder { static getParameterAttributes(cwmpId: string | undefined, parameterNames: string[]): string { const enve = makeEnvelope(cwmpId) - enve.Body.GetParameterAttributes = new GetParameterAttributes({ ParameterNames: parameterNames }) + enve.Body.GetParameterAttributes = new GetParameterAttributes({ + ParameterNames: parameterNames, + }) return enve.getXml() } @@ -185,7 +203,9 @@ export class CWMPMessageBuilder { const fault = new Fault() fault.Faultcode = 'Server' fault.Faultstring = reason - fault.Detail = new FaultDetail({ Fault: new FaultStruct({ FaultCode: faultCode, FaultString: reason }) }) + fault.Detail = new FaultDetail({ + Fault: new FaultStruct({ FaultCode: faultCode, FaultString: reason }), + }) return fault.getXml() } diff --git a/app/protocol/soap.ts b/app/protocol/soap.ts index 9c09848..9964177 100644 --- a/app/protocol/soap.ts +++ b/app/protocol/soap.ts @@ -53,9 +53,7 @@ export function parseSoapXml(xml: string): SoapEnvelope { const idNode = header['cwmp:ID'] || header['cwmp:Id'] if (idNode) { soapHeader.id = - typeof idNode === 'string' - ? idNode - : (idNode as { _?: string })?._ || String(idNode) + typeof idNode === 'string' ? idNode : (idNode as { _?: string })?._ || String(idNode) } return { header: soapHeader, body, cwmpVersion } diff --git a/app/protocol/soap_envelope.ts b/app/protocol/soap_envelope.ts index d474827..f840bc1 100644 --- a/app/protocol/soap_envelope.ts +++ b/app/protocol/soap_envelope.ts @@ -119,11 +119,15 @@ export class SoapEnvelopeHeader { constructor(props: Record = {}) { this.ID = new CwmpID(props?.ID as any) this.NoMoreRequests = (props?.NoMoreRequests as number) || 0 - this.SessionTimeout = props?.SessionTimeout ? new SessionTimeout(props.SessionTimeout) : undefined + this.SessionTimeout = props?.SessionTimeout + ? new SessionTimeout(props.SessionTimeout) + : undefined this.SupportedCWMPVersions = props?.SupportedCWMPVersions ? new SupportedCWMPVersions(props.SupportedCWMPVersions) : undefined - this.UseCWMPVersion = props?.UseCWMPVersion ? new UseCWMPVersion(props.UseCWMPVersion) : undefined + this.UseCWMPVersion = props?.UseCWMPVersion + ? new UseCWMPVersion(props.UseCWMPVersion) + : undefined } } @@ -135,16 +139,28 @@ export class SoapEnvelope { constructor(props: Record = {}) { this.version = (props?.version as '1.0' | '1.1' | '1.2' | '1.3' | '1.4') || '1.0' - this.$ = props?.$ ? new SoapEnvelopeAttribute(props.$ as any) : new SoapEnvelopeAttribute(NAMESPACES[this.version]) + this.$ = props?.$ + ? new SoapEnvelopeAttribute(props.$ as any) + : new SoapEnvelopeAttribute(NAMESPACES[this.version]) this.Header = new SoapEnvelopeHeader((props.Header as Record) || {}) const bodyProps = (props.Body as Record) || {} switch (this.version) { - case '1.4': this.Body = new Cwmp_1_4(bodyProps); break - case '1.3': this.Body = new Cwmp_1_3(bodyProps); break - case '1.2': this.Body = new Cwmp_1_2(bodyProps); break - case '1.1': this.Body = new Cwmp_1_1(bodyProps); break - default: this.Body = new Cwmp_1_0(bodyProps); break + case '1.4': + this.Body = new Cwmp_1_4(bodyProps) + break + case '1.3': + this.Body = new Cwmp_1_3(bodyProps) + break + case '1.2': + this.Body = new Cwmp_1_2(bodyProps) + break + case '1.1': + this.Body = new Cwmp_1_1(bodyProps) + break + default: + this.Body = new Cwmp_1_0(bodyProps) + break } } diff --git a/app/protocol/tr069_types.ts b/app/protocol/tr069_types.ts index 682f64d..ee9ad1c 100644 --- a/app/protocol/tr069_types.ts +++ b/app/protocol/tr069_types.ts @@ -185,15 +185,15 @@ export interface RebootTaskPayload { } export type CWMPTaskPayload = - | string // get_parameter_names - | string[] // get_parameter_values / get_parameter_attributes + | string // get_parameter_names + | string[] // get_parameter_values / get_parameter_attributes | SetParameterValuesTaskPayload | SetParameterAttributesTaskPayload[] | ScheduleInformTaskPayload | DownloadTaskPayload | UploadTaskPayload | RebootTaskPayload - | Record // factory_reset + | Record // factory_reset /** Données de résultat retournées dans TaskResult.data selon le type de tâche */ export type CWMPTaskResultData = diff --git a/app/services/cwmp/configuration_evaluation_state.ts b/app/services/cwmp/configuration_evaluation_state.ts index b239e84..47262de 100644 --- a/app/services/cwmp/configuration_evaluation_state.ts +++ b/app/services/cwmp/configuration_evaluation_state.ts @@ -3,7 +3,10 @@ import type { EngineResult } from '../configuration_engine.js' import type { DeviceStateResult, DeviceContext } from './main_state_machine.js' import { PartialOperationalState } from './main_state_machine.js' import DeviceModel from '#models/device' -import type { GetParameterNamesResponse, GetParameterValuesResponse } from '../../protocol/tr069_types.js' +import type { + GetParameterNamesResponse, + GetParameterValuesResponse, +} from '../../protocol/tr069_types.js' import logger from '@adonisjs/core/services/logger' import { ComplianceStatus } from '../../domain/configuration/compliance.js' import { buildProvisioningCode } from '../../utils/provisioning_code.js' @@ -55,7 +58,8 @@ export class ConfigurationEvaluationState { { 'compliance.status': ComplianceStatus.IN_PROGRESS, 'compliance.lastEvaluatedAt': new Date(), - 'compliance.configurationIdentifier': this.engineResult.configuration?.identifier || null, + 'compliance.configurationIdentifier': + this.engineResult.configuration?.identifier || null, } ) @@ -72,7 +76,10 @@ export class ConfigurationEvaluationState { return this.processNextAction(context) } catch (error: any) { - logger.error({ identifier: context.device.identifier, error: error.message }, '[ConfigEval] Evaluation failed') + logger.error( + { identifier: context.device.identifier, error: error.message }, + '[ConfigEval] Evaluation failed' + ) await DeviceModel.updateOne( { identifier: context.device.identifier }, @@ -83,11 +90,13 @@ export class ConfigurationEvaluationState { } ).catch(() => {}) - return { nextState: new PartialOperationalState(context.device, { - reason: 'configuration_evaluation_failed', - blockingState: 'configuration-evaluation', - error: error.message, - }) } + return { + nextState: new PartialOperationalState(context.device, { + reason: 'configuration_evaluation_failed', + blockingState: 'configuration-evaluation', + error: error.message, + }), + } } } @@ -101,17 +110,27 @@ export class ConfigurationEvaluationState { const leafNames: string[] = infoList .map((info) => { const n = info?.Name - return typeof n === 'string' ? n : (n && typeof (n as { _?: string })._ === 'string' ? (n as { _: string })._ : null) + return typeof n === 'string' + ? n + : n && typeof (n as { _?: string })._ === 'string' + ? (n as { _: string })._ + : null }) .filter((name): name is string => !!name && !name.endsWith('.')) if (leafNames.length > 0) { this.pendingValuesQueue.push(...leafNames) } - logger.debug({ identifier: context.device.identifier, leafCount: leafNames.length }, '[ConfigEval] Inventory GPN response processed inline') + logger.debug( + { identifier: context.device.identifier, leafCount: leafNames.length }, + '[ConfigEval] Inventory GPN response processed inline' + ) } else if (context.message && !context.message.success) { // GPN failed during inventory — record failure context.device.recordGpnFailure() - logger.debug({ identifier: context.device.identifier }, '[ConfigEval] Inventory GPN failed — skipping remaining inventory') + logger.debug( + { identifier: context.device.identifier }, + '[ConfigEval] Inventory GPN failed — skipping remaining inventory' + ) this.inventoryQueue = [] } } else if (this.awaitingPhase === 'gpv_response') { @@ -122,19 +141,30 @@ export class ConfigurationEvaluationState { if (list.length > 0) { const normalized = list.map((pv) => ({ ...pv, - Name: typeof pv?.Name === 'string' ? pv.Name : (pv?.Name as { _?: string })?._ || pv?.Name, + Name: + typeof pv?.Name === 'string' ? pv.Name : (pv?.Name as { _?: string })?._ || pv?.Name, })) context.device.updateFromValues(normalized) - logger.debug({ identifier: context.device.identifier, count: normalized.length }, '[ConfigEval] Inventory GPV values saved to device') + logger.debug( + { identifier: context.device.identifier, count: normalized.length }, + '[ConfigEval] Inventory GPV values saved to device' + ) } } } // 1. Inventory phase 1: GetParameterNames per subtree path // Skip if device does not support GPN or circuit breaker is active - if (this.inventoryQueue.length > 0 && context.device.gpnStatus !== 'unsupported' && context.device.isGpnAllowed()) { + if ( + this.inventoryQueue.length > 0 && + context.device.gpnStatus !== 'unsupported' && + context.device.isGpnAllowed() + ) { const path = this.inventoryQueue.shift()! - logger.debug({ identifier: context.device.identifier, path }, '[ConfigEval] Inventory GetParameterNames') + logger.debug( + { identifier: context.device.identifier, path }, + '[ConfigEval] Inventory GetParameterNames' + ) const response = await context.services.deviceAction.getParameterNames( context.device, path, @@ -144,14 +174,20 @@ export class ConfigurationEvaluationState { return { response } } else if (this.inventoryQueue.length > 0) { // GPN unsupported or circuit breaker active — skip inventory - logger.debug({ identifier: context.device.identifier, skipped: this.inventoryQueue.length }, '[ConfigEval] Skipping inventory (GPN unsupported or circuit breaker active)') + logger.debug( + { identifier: context.device.identifier, skipped: this.inventoryQueue.length }, + '[ConfigEval] Skipping inventory (GPN unsupported or circuit breaker active)' + ) this.inventoryQueue = [] } // 2. Inventory phase 2: GetParameterValues for discovered leaf names if (this.pendingValuesQueue.length > 0) { const leaves = this.pendingValuesQueue.splice(0) - logger.debug({ identifier: context.device.identifier, count: leaves.length }, '[ConfigEval] Inventory GetParameterValues') + logger.debug( + { identifier: context.device.identifier, count: leaves.length }, + '[ConfigEval] Inventory GetParameterValues' + ) const response = await context.services.deviceAction.getParameterValues( context.device, leaves, @@ -164,7 +200,10 @@ export class ConfigurationEvaluationState { // 3. All parameter diffs in one SetParameterValues RPC if (this.paramDiffs.length > 0) { const diffs = this.paramDiffs.splice(0) - logger.debug({ identifier: context.device.identifier, count: diffs.length }, '[ConfigEval] Applying parameter diffs') + logger.debug( + { identifier: context.device.identifier, count: diffs.length }, + '[ConfigEval] Applying parameter diffs' + ) const params = diffs.map((d) => ({ parameterName: d.name, value: d.value, type: d.type })) const response = await context.services.deviceAction.applySetParameterValues( context.device, @@ -177,10 +216,19 @@ export class ConfigurationEvaluationState { // 4. Firmware upgrade (if needed and within schedule) if (this.engineResult!.firmwareMigrationPath.length > 0 && !this.engineResult!.skipFirmware) { logger.debug( - { identifier: context.device.identifier, steps: this.engineResult!.firmwareMigrationPath.length }, + { + identifier: context.device.identifier, + steps: this.engineResult!.firmwareMigrationPath.length, + }, '[ConfigEval] Transitioning to firmware upgrade' ) - return { nextState: new FirmwareUpgradeState(this.engineResult!.firmwareMigrationPath, 0, this.engineResult!) } + return { + nextState: new FirmwareUpgradeState( + this.engineResult!.firmwareMigrationPath, + 0, + this.engineResult! + ), + } } // 5. All done @@ -214,7 +262,11 @@ export class FirmwareUpgradeState { } logger.debug( - { identifier: context.device.identifier, step: this.currentStepIndex + 1, total: this.migrationSteps.length }, + { + identifier: context.device.identifier, + step: this.currentStepIndex + 1, + total: this.migrationSteps.length, + }, '[FirmwareUpgrade] Sending Download RPC' ) @@ -233,7 +285,10 @@ export class FirmwareUpgradeState { return { response } } catch (error: any) { - logger.error({ identifier: context.device.identifier, error: error.message }, '[FirmwareUpgrade] Download RPC failed') + logger.error( + { identifier: context.device.identifier, error: error.message }, + '[FirmwareUpgrade] Download RPC failed' + ) await DeviceModel.updateOne( { identifier: context.device.identifier }, @@ -279,7 +334,8 @@ export class ComplianceUpdateState { } if (this.engineResult.configuration) { - updateData['compliance.configurationIdentifier'] = this.engineResult.configuration.identifier + updateData['compliance.configurationIdentifier'] = + this.engineResult.configuration.identifier } // Always build and write provisioning code to device (functional for "once", informative for "maintain") @@ -292,18 +348,32 @@ export class ComplianceUpdateState { const currentPCValue = currentPC?.value ?? '' if (currentPCValue === provCode) { - logger.debug({ identifier: context.device.identifier, provCode }, '[ComplianceUpdate] Provisioning code already matches — skipping SPV') + logger.debug( + { identifier: context.device.identifier, provCode }, + '[ComplianceUpdate] Provisioning code already matches — skipping SPV' + ) updateData.provisioningCode = provCode - await DeviceModel.updateOne({ identifier: context.device.identifier }, updateData).catch(() => {}) + await DeviceModel.updateOne({ identifier: context.device.identifier }, updateData).catch( + () => {} + ) return { nextState: new ExecutingCommandsState(true) } } updateData.provisioningCode = provCode - await DeviceModel.updateOne({ identifier: context.device.identifier }, updateData).catch(() => {}) + await DeviceModel.updateOne({ identifier: context.device.identifier }, updateData).catch( + () => {} + ) context.device.tmpPC = provCode - logger.debug({ identifier: context.device.identifier, provCode, policy: this.engineResult.configuration.policy }, '[ComplianceUpdate] Writing provisioning code') + logger.debug( + { + identifier: context.device.identifier, + provCode, + policy: this.engineResult.configuration.policy, + }, + '[ComplianceUpdate] Writing provisioning code' + ) const response = await context.services.deviceAction.insertProvisioningCode( context.device, @@ -312,8 +382,13 @@ export class ComplianceUpdateState { return { response } } - await DeviceModel.updateOne({ identifier: context.device.identifier }, updateData).catch(() => {}) - logger.debug({ identifier: context.device.identifier }, '[ComplianceUpdate] Compliance finalized (no configuration)') + await DeviceModel.updateOne({ identifier: context.device.identifier }, updateData).catch( + () => {} + ) + logger.debug( + { identifier: context.device.identifier }, + '[ComplianceUpdate] Compliance finalized (no configuration)' + ) return { nextState: new ExecutingCommandsState(true) } } diff --git a/app/services/cwmp/device_action_executor.ts b/app/services/cwmp/device_action_executor.ts index c0713df..f8cec6e 100644 --- a/app/services/cwmp/device_action_executor.ts +++ b/app/services/cwmp/device_action_executor.ts @@ -1,7 +1,7 @@ -import { Device } from '../../domain/device/device.js' +import { type Device } from '../../domain/device/device.js' import { DeviceParameter } from '../../domain/device/device_parameter.js' import { ParameterType } from '../../domain/shared/types.js' -import { CWMPTaskQueue, ResultStatus, TaskResult } from './task_queue.js' +import { type CWMPTaskQueue, ResultStatus, type TaskResult } from './task_queue.js' import DeviceModel from '../../models/device.js' import logger from '@adonisjs/core/services/logger' import type { @@ -116,7 +116,12 @@ export class DeviceActionExecutor { return await callback(result) } - return this.taskQueue.addTask(device.deviceId, 'get_parameter_values', parameterPaths, wrappedCallback).xml + return this.taskQueue.addTask( + device.deviceId, + 'get_parameter_values', + parameterPaths, + wrappedCallback + ).xml } async getParameterAttributes( @@ -128,11 +133,14 @@ export class DeviceActionExecutor { if (result.status === ResultStatus.Success) { try { const resp = result.data as GetParameterAttributesResponse - const list: ParameterAttributeStruct[] = resp?.ParameterList?.ParameterAttributeStruct ?? [] + const list: ParameterAttributeStruct[] = + resp?.ParameterList?.ParameterAttributeStruct ?? [] if (list.length > 0) { for (const attr of list) { const name: string | undefined = - typeof attr?.Name === 'string' ? attr.Name : (attr?.Name as { _?: string } | undefined)?._ + typeof attr?.Name === 'string' + ? attr.Name + : (attr?.Name as { _?: string } | undefined)?._ if (!name) continue const notification: number | undefined = typeof attr?.Notification === 'number' ? attr.Notification : undefined @@ -157,7 +165,12 @@ export class DeviceActionExecutor { return await callback(result) } - return this.taskQueue.addTask(device.deviceId, 'get_parameter_attributes', parameterList, wrappedCallback).xml + return this.taskQueue.addTask( + device.deviceId, + 'get_parameter_attributes', + parameterList, + wrappedCallback + ).xml } async getParameterNames( @@ -180,13 +193,18 @@ export class DeviceActionExecutor { } } else { // Inspect fault code to distinguish "unsupported" from "transient error" - const faultData = result.data as { FaultCode?: number | string; faultCode?: number | string; Detail?: { Fault?: { FaultCode?: number | string } }; detail?: { Fault?: { FaultCode?: number | string } } } + const faultData = result.data as { + FaultCode?: number | string + faultCode?: number | string + Detail?: { Fault?: { FaultCode?: number | string } } + detail?: { Fault?: { FaultCode?: number | string } } + } const faultCode = faultData?.FaultCode ?? faultData?.faultCode ?? faultData?.Detail?.Fault?.FaultCode ?? faultData?.detail?.Fault?.FaultCode - const code = typeof faultCode === 'string' ? parseInt(faultCode, 10) : faultCode + const code = typeof faultCode === 'string' ? Number.parseInt(faultCode, 10) : faultCode const GPN_UNSUPPORTED_CODES = [9000, 9001, 9003, 9004, 9005] const GPN_TRANSIENT_CODES = [9002] @@ -195,13 +213,21 @@ export class DeviceActionExecutor { device.recordGpnFailure() await this.persistDevice(device) } else if (code !== undefined && GPN_TRANSIENT_CODES.includes(code)) { - logger.warn({ code, deviceId: device.deviceId?.serialNumber }, 'GPN transient error (9002) — will not mark unsupported') + logger.warn( + { code, deviceId: device.deviceId?.serialNumber }, + 'GPN transient error (9002) — will not mark unsupported' + ) } } return await callback(result) } - return this.taskQueue.addTask(device.deviceId, 'get_parameter_names', parameterName, wrappedCallback).xml + return this.taskQueue.addTask( + device.deviceId, + 'get_parameter_names', + parameterName, + wrappedCallback + ).xml } async applySetParameterValues( @@ -214,9 +240,12 @@ export class DeviceActionExecutor { const inferType = (val: string | number | boolean): string => { const t = typeof val switch (t) { - case 'boolean': return 'xsd:boolean' - case 'number': return Number.isInteger(val) ? 'xsd:int' : 'xsd:string' - default: return 'xsd:string' + case 'boolean': + return 'xsd:boolean' + case 'number': + return Number.isInteger(val) ? 'xsd:int' : 'xsd:string' + default: + return 'xsd:string' } } @@ -292,7 +321,8 @@ export class DeviceActionExecutor { parameter: SetScheduleInform, callback: (result: boolean) => Promise ): Promise { - const commandKey = parameter.commandKey || `ScheduleInform_${device.deviceId.serialNumber}_${Date.now()}` + const commandKey = + parameter.commandKey || `ScheduleInform_${device.deviceId.serialNumber}_${Date.now()}` return this.taskQueue.addTask( device.deviceId, 'schedule_inform', @@ -407,7 +437,9 @@ export class DeviceActionExecutor { } const parameterKey = `SetPValues_${device.deviceId.serialNumber}_${Date.now()}` - const pvStructs = [{ Name: provisioningPath, Value: { _: device.tmpPC as string, $: { Type: 'xsd:string' } } }] + const pvStructs = [ + { Name: provisioningPath, Value: { _: device.tmpPC as string, $: { Type: 'xsd:string' } } }, + ] return this.taskQueue.addTask( device.deviceId, @@ -416,7 +448,9 @@ export class DeviceActionExecutor { async (result: TaskResult) => { if (result.status === ResultStatus.Success) { device.removeProperty(provisioningPath) - device.setParameter(new DeviceParameter(provisioningPath, device.tmpPC, ParameterType.STRING)) + device.setParameter( + new DeviceParameter(provisioningPath, device.tmpPC, ParameterType.STRING) + ) await this.persistDevice(device) device.tmpPC = '' return await callback(true) diff --git a/app/services/cwmp/immediate_command_state_machine.ts b/app/services/cwmp/immediate_command_state_machine.ts index 0f95031..43c2026 100644 --- a/app/services/cwmp/immediate_command_state_machine.ts +++ b/app/services/cwmp/immediate_command_state_machine.ts @@ -1,7 +1,11 @@ -import { Device } from '../../domain/device/device.js' -import { CWMPTaskQueue } from './task_queue.js' +import { type Device } from '../../domain/device/device.js' +import { type CWMPTaskQueue } from './task_queue.js' import { DeviceActionExecutor } from './device_action_executor.js' -import { ExecutingCommandsState, DeviceContext, DeviceStateResult } from './main_state_machine.js' +import { + ExecutingCommandsState, + type DeviceContext, + type DeviceStateResult, +} from './main_state_machine.js' import type { CWMPTaskResultData } from '../../protocol/tr069_types.js' import logger from '@adonisjs/core/services/logger' @@ -27,7 +31,9 @@ export class ImmediateCommandStateMachine { } async start(): Promise { - logger.info(`Starting immediate-command state machine for device ${this.context.device.deviceId?.serialNumber}`) + logger.info( + `Starting immediate-command state machine for device ${this.context.device.deviceId?.serialNumber}` + ) return this.execute() } @@ -41,7 +47,11 @@ export class ImmediateCommandStateMachine { } isCompleted(): boolean { - return this.currentState.name === 'completed' || this.currentState.name === 'operational' || this.currentState.name === 'partial_operational' + return ( + this.currentState.name === 'completed' || + this.currentState.name === 'operational' || + this.currentState.name === 'partial_operational' + ) } private async execute(): Promise { diff --git a/app/services/cwmp/main_state_machine.ts b/app/services/cwmp/main_state_machine.ts index 9418ba5..db0765a 100644 --- a/app/services/cwmp/main_state_machine.ts +++ b/app/services/cwmp/main_state_machine.ts @@ -1,5 +1,5 @@ -import { Device } from '../../domain/device/device.js' -import { CWMPTaskQueue } from './task_queue.js' +import { type Device } from '../../domain/device/device.js' +import { type CWMPTaskQueue } from './task_queue.js' import { DeviceActionExecutor } from './device_action_executor.js' import { getDeviceHashIdentifier } from '../../utils/device_utils.js' import { CommandStatus, CommandType } from '../../domain/commands/command.js' @@ -21,7 +21,7 @@ import type { UploadParameter, RebootParameter, } from './device_action_executor.js' -import { ObjectId } from 'mongodb' +import { type ObjectId } from 'mongodb' // ─── Interfaces ──────────────────────────────────────────────────────────────── @@ -67,13 +67,15 @@ class IdleState extends DeviceState { if (device.gpnStatus === 'unknown') { // Never classified → all tmpParams are unclassified for (const pv of tmpParams) { - const name = typeof pv?.Name === 'string' ? pv.Name : (pv?.Name as { _?: string })?._ || null + const name = + typeof pv?.Name === 'string' ? pv.Name : (pv?.Name as { _?: string })?._ || null if (name) unclassified.push(name) } } else { // Already classified before → only params NOT in _parameters AND NOT in _properties are new for (const pv of tmpParams) { - const name = typeof pv?.Name === 'string' ? pv.Name : (pv?.Name as { _?: string })?._ || null + const name = + typeof pv?.Name === 'string' ? pv.Name : (pv?.Name as { _?: string })?._ || null if (!name) continue if (!device.getParameter(name) && !device.getProperty(name)) { unclassified.push(name) @@ -109,7 +111,11 @@ class IdleState extends DeviceState { // Case 1: GPN unsupported or circuit breaker open → classify unclassified via catalog if (device.gpnStatus === 'unsupported' || !device.isGpnAllowed()) { logger.info( - { gpnStatus: device.gpnStatus, unclassifiedCount: unclassified.length, deviceId: device.identifier }, + { + gpnStatus: device.gpnStatus, + unclassifiedCount: unclassified.length, + deviceId: device.identifier, + }, '[IdleState] GPN unavailable — classifying unclassified params via catalog' ) reclassifyDeviceFromCatalog(device) @@ -120,7 +126,12 @@ class IdleState extends DeviceState { // Case 3: GPN unknown → send GPN to test support + classify const paths = Array.from(parentPaths).sort() logger.info( - { paths, unclassifiedCount: unclassified.length, gpnStatus: device.gpnStatus, deviceId: device.identifier }, + { + paths, + unclassifiedCount: unclassified.length, + gpnStatus: device.gpnStatus, + deviceId: device.identifier, + }, '[IdleState] Classifying unclassified params via GPN' ) return { nextState: new DiscoveringParametersState(paths), response: '' } @@ -179,7 +190,11 @@ class DiscoveringParametersState extends DeviceState { updatedAt: new Date(), }) logger.info( - { hashId, gpnStatus: context.device.gpnStatus, circuitBreaker: context.device.gpnCircuitBreaker }, + { + hashId, + gpnStatus: context.device.gpnStatus, + circuitBreaker: context.device.gpnCircuitBreaker, + }, '[DiscoveringParameters] Persisted GPN failure + catalog classification to DB' ) } catch (e) { @@ -197,11 +212,17 @@ class DiscoveringParametersState extends DeviceState { context.device.organizeParameters(infoList, new Date()) context.device.setGpnStatus('supported') logger.info( - { deviceId: context.device.identifier, paramCount: context.device.getParameters().length }, + { + deviceId: context.device.identifier, + paramCount: context.device.getParameters().length, + }, '[DiscoveringParameters] GPN response processed' ) } catch (e: unknown) { - logger.error({ error: (e as Error)?.message }, '[DiscoveringParameters] Error processing GPN response') + logger.error( + { error: (e as Error)?.message }, + '[DiscoveringParameters] Error processing GPN response' + ) } } @@ -216,7 +237,9 @@ class DiscoveringParametersState extends DeviceState { private async processNext(context: DeviceContext): Promise { if (this.parameterQueue.length === 0) return '' const current = this.parameterQueue.shift()! - return context.services.deviceAction.getParameterNames(context.device, current, () => Promise.resolve('')) + return context.services.deviceAction.getParameterNames(context.device, current, () => + Promise.resolve('') + ) } } @@ -277,14 +300,21 @@ class CriticalReadsState extends DeviceState { // Normalize Name fields: XMLParser may return {_: "path", "$xsi:type": "..."} when Name has attributes const normalized = list.map((pv) => ({ ...pv, - Name: typeof pv?.Name === 'string' ? pv.Name : (pv?.Name as { _?: string })?._ || pv?.Name, + Name: + typeof pv?.Name === 'string' ? pv.Name : (pv?.Name as { _?: string })?._ || pv?.Name, })) context.device.setTmpParamValues(normalized) context.device.updateFromValues(normalized) - logger.info({ count: normalized.length, names: normalized.map((p) => p.Name) }, '[CriticalReads] GPV response values processed') + logger.info( + { count: normalized.length, names: normalized.map((p) => p.Name) }, + '[CriticalReads] GPV response values processed' + ) } } catch (e: unknown) { - logger.error({ error: (e as Error)?.message }, '[CriticalReads] Error processing GPV response values') + logger.error( + { error: (e as Error)?.message }, + '[CriticalReads] Error processing GPV response values' + ) } } @@ -316,15 +346,33 @@ export class ExecutingCommandsState extends DeviceState { }) const collection = CommandModel.getCollection() - const iWaiting = await collection.find({ deviceIdentifier: hashId, mode: ModeType.I, status: CommandStatus.WAITING }).sort({ priority: 1 }).toArray() - const iPending = await collection.find({ deviceIdentifier: hashId, mode: ModeType.I, status: CommandStatus.PENDING }).sort({ priority: 1 }).toArray() + const iWaiting = await collection + .find({ deviceIdentifier: hashId, mode: ModeType.I, status: CommandStatus.WAITING }) + .sort({ priority: 1 }) + .toArray() + const iPending = await collection + .find({ deviceIdentifier: hashId, mode: ModeType.I, status: CommandStatus.PENDING }) + .sort({ priority: 1 }) + .toArray() const niPending = this.includeNI - ? await collection.find({ deviceIdentifier: hashId, mode: ModeType.NI, status: CommandStatus.PENDING }).sort({ priority: 1 }).toArray() + ? await collection + .find({ deviceIdentifier: hashId, mode: ModeType.NI, status: CommandStatus.PENDING }) + .sort({ priority: 1 }) + .toArray() : [] this.commandsQueue = [...iWaiting, ...iPending, ...niPending] this.started = true - logger.debug({ hashId, iWaiting: iWaiting.length, iPending: iPending.length, niPending: niPending.length, includeNI: this.includeNI }, '[ExecutingCommands] Loaded commands queue') + logger.debug( + { + hashId, + iWaiting: iWaiting.length, + iPending: iPending.length, + niPending: niPending.length, + includeNI: this.includeNI, + }, + '[ExecutingCommands] Loaded commands queue' + ) if (this.commandsQueue.length === 0) { if (this.includeNI) { @@ -347,11 +395,17 @@ export class ExecutingCommandsState extends DeviceState { // Optimistic update for SPV if (this.currentCommandRecord.action === CommandType.SET_PARAMETER_VALUES) { - const rawList = Array.isArray(this.currentCommandRecord.parameters) ? this.currentCommandRecord.parameters as Array<{ name?: string; value?: unknown }> : [] + const rawList = Array.isArray(this.currentCommandRecord.parameters) + ? (this.currentCommandRecord.parameters as Array<{ name?: string; value?: unknown }>) + : [] const pvStructs: ParameterValueStruct[] = rawList.map((p) => ({ '@class': '.ParameterValueStruct', - Name: p?.name ?? '', - Value: { '@class': '.ValueObject', _: String(p?.value ?? ''), $: { '@class': '.ValueObjectAttribute', Type: 'xsd:string' } }, + 'Name': p?.name ?? '', + 'Value': { + '@class': '.ValueObject', + '_': String(p?.value ?? ''), + '$': { '@class': '.ValueObjectAttribute', 'Type': 'xsd:string' }, + }, })) if (pvStructs.length > 0) { context.device.updateFromValues(pvStructs) @@ -398,7 +452,11 @@ export class ExecutingCommandsState extends DeviceState { commandRecord.status = CommandStatus.SENT commandRecord.executedAt = now try { - await CommandModel.updateCommand(commandRecord._id as ObjectId, { status: CommandStatus.SENT, executedAt: now, updatedAt: now }) + await CommandModel.updateCommand(commandRecord._id as ObjectId, { + status: CommandStatus.SENT, + executedAt: now, + updatedAt: now, + }) } catch (err) { logger.error(`Failed to mark command as SENT: ${err}`) } @@ -410,73 +468,144 @@ export class ExecutingCommandsState extends DeviceState { const action = commandRecord.action as CommandType // params is typed as unknown from MongoDB — we cast to a flexible record for access - const params = commandRecord.parameters as Record | string | unknown[] | undefined + const params = commandRecord.parameters as + | Record + | string + | unknown[] + | undefined // Map CommandType to CWMP request switch (action) { case CommandType.GET_PARAMETER_VALUES: { const p = params as Record | string[] | undefined - const names: string[] = Array.isArray(p) ? (p as string[]) : ((p as Record)?.names as string[] || (p as Record)?.paths as string[] || (p as Record)?.parameterNames as string[] || []) - return context.services.deviceAction.getParameterValues(context.device, names, () => Promise.resolve('')) + const names: string[] = Array.isArray(p) + ? (p as string[]) + : ((p as Record)?.names as string[]) || + ((p as Record)?.paths as string[]) || + ((p as Record)?.parameterNames as string[]) || + [] + return context.services.deviceAction.getParameterValues(context.device, names, () => + Promise.resolve('') + ) } case CommandType.GET_PARAMETER_NAMES: { const p = params as Record | string | undefined - const name: string = typeof p === 'string' ? p : ((p as Record)?.name as string || (p as Record)?.path as string || (p as Record)?.parameterPath as string || '') - return context.services.deviceAction.getParameterNames(context.device, name, () => Promise.resolve('')) + const name: string = + typeof p === 'string' + ? p + : ((p as Record)?.name as string) || + ((p as Record)?.path as string) || + ((p as Record)?.parameterPath as string) || + '' + return context.services.deviceAction.getParameterNames(context.device, name, () => + Promise.resolve('') + ) } case CommandType.GET_PARAMETER_ATTRIBUTES: { const p = params as Record | string[] | undefined - const names: string[] = Array.isArray(p) ? (p as string[]) : ((p as Record)?.names as string[] || (p as Record)?.paths as string[] || (p as Record)?.parameterNames as string[] || []) - return context.services.deviceAction.getParameterAttributes(context.device, names, () => Promise.resolve('')) + const names: string[] = Array.isArray(p) + ? (p as string[]) + : ((p as Record)?.names as string[]) || + ((p as Record)?.paths as string[]) || + ((p as Record)?.parameterNames as string[]) || + [] + return context.services.deviceAction.getParameterAttributes(context.device, names, () => + Promise.resolve('') + ) } case CommandType.SET_PARAMETER_VALUES: { - const p = params as Record | Array<{ parameterName?: string; name?: string; value: unknown; type?: string }> | undefined - const rawParams: Array<{ parameterName?: string; name?: string; value: unknown; type?: string }> = Array.isArray(p) + const p = params as + | Record + | Array<{ parameterName?: string; name?: string; value: unknown; type?: string }> + | undefined + const rawParams: Array<{ + parameterName?: string + name?: string + value: unknown + type?: string + }> = Array.isArray(p) ? p - : ((p as Record)?.parameterValues as typeof rawParams || (p as Record)?.parameters as typeof rawParams || []) + : ((p as Record)?.parameterValues as typeof rawParams) || + ((p as Record)?.parameters as typeof rawParams) || + [] const spvParams = rawParams.map((pv) => ({ parameterName: pv.parameterName ?? pv.name ?? '', value: pv.value as string | number | boolean, type: pv.type, })) - return context.services.deviceAction.applySetParameterValues(context.device, spvParams, () => Promise.resolve('')) + return context.services.deviceAction.applySetParameterValues( + context.device, + spvParams, + () => Promise.resolve('') + ) } case CommandType.SET_PARAMETER_ATTRIBUTES: { const p = params as Record | unknown[] | undefined - const spaParams = Array.isArray(p) ? p : ((p as Record)?.attributes as unknown[] || (p as Record)?.parameterAttributes as unknown[] || []) - return context.services.deviceAction.applySetParameterAttributes(context.device, spaParams as SetParameterAttribute[], () => Promise.resolve('')) + const spaParams = Array.isArray(p) + ? p + : ((p as Record)?.attributes as unknown[]) || + ((p as Record)?.parameterAttributes as unknown[]) || + [] + return context.services.deviceAction.applySetParameterAttributes( + context.device, + spaParams as SetParameterAttribute[], + () => Promise.resolve('') + ) } case CommandType.DOWNLOAD: { const p = (params as Record) || {} - return context.services.deviceAction.applyDownload(context.device, p as unknown as DownloadParameter, () => Promise.resolve('')) + return context.services.deviceAction.applyDownload( + context.device, + p as unknown as DownloadParameter, + () => Promise.resolve('') + ) } case CommandType.UPLOAD: { const p = (params as Record) || {} - return context.services.deviceAction.applyUpload(context.device, p as unknown as UploadParameter, () => Promise.resolve('')) + return context.services.deviceAction.applyUpload( + context.device, + p as unknown as UploadParameter, + () => Promise.resolve('') + ) } case CommandType.REBOOT: { const p = (params as Record) || {} - return context.services.deviceAction.applyReboot(context.device, p as RebootParameter, () => Promise.resolve('')) + return context.services.deviceAction.applyReboot(context.device, p as RebootParameter, () => + Promise.resolve('') + ) } case CommandType.FACTORY_RESET: { - return context.services.deviceAction.applyFactoryReset(context.device, {}, () => Promise.resolve('')) + return context.services.deviceAction.applyFactoryReset(context.device, {}, () => + Promise.resolve('') + ) } case CommandType.SCHEDULE_INFORM: { const p = (params as Record) || {} return context.services.deviceAction.applyScheduleInform( context.device, - { delayInSeconds: (p?.delayInSeconds as number) || 300, commandKey: p?.commandKey as string | undefined }, + { + delayInSeconds: (p?.delayInSeconds as number) || 300, + commandKey: p?.commandKey as string | undefined, + }, () => Promise.resolve('') ) } case CommandType.REFRESH: { // Refresh = GetParameterValues on all known leaf parameters that have non-empty values const allNames = [ - ...context.device.getParameters().filter((p) => p.value != null && p.value !== '').map((p) => p.name), - ...context.device.getProperties().filter((p) => p.value != null && p.value !== '').map((p) => p.name), + ...context.device + .getParameters() + .filter((p) => p.value !== null && p.value !== undefined && p.value !== '') + .map((p) => p.name), + ...context.device + .getProperties() + .filter((p) => p.value !== null && p.value !== undefined && p.value !== '') + .map((p) => p.name), ].filter((name) => !!name && !name.endsWith('.')) if (allNames.length > 0) { - return context.services.deviceAction.getParameterValues(context.device, allNames, () => Promise.resolve('')) + return context.services.deviceAction.getParameterValues(context.device, allNames, () => + Promise.resolve('') + ) } return '' } @@ -484,7 +613,10 @@ export class ExecutingCommandsState extends DeviceState { logger.error(`Unknown command type: ${action}`) // Mark as failed commandRecord.status = CommandStatus.FAILED - await CommandModel.updateCommand(commandRecord._id as ObjectId, { status: CommandStatus.FAILED, updatedAt: new Date() }) + await CommandModel.updateCommand(commandRecord._id as ObjectId, { + status: CommandStatus.FAILED, + updatedAt: new Date(), + }) this.currentCommandRecord = null if (this.commandsQueue.length > 0) { return this.processNextCommand(context) @@ -526,7 +658,10 @@ export class OperationalState extends DeviceState { logger.error(`Failed to persist device on completion: ${e}`) } - logger.info({ deviceId: device.identifier, state: 'operational' }, '[StateMachine] Session completed successfully') + logger.info( + { deviceId: device.identifier, state: 'operational' }, + '[StateMachine] Session completed successfully' + ) return { isComplete: true, response: '' } } @@ -540,7 +675,10 @@ export class PartialOperationalState extends DeviceState { name = 'partial_operational' private failureContext: { reason: string; blockingState?: string; error?: any } - constructor(_device: Device, failureContext: { reason: string; blockingState?: string; error?: any }) { + constructor( + _device: Device, + failureContext: { reason: string; blockingState?: string; error?: any } + ) { super() this.failureContext = failureContext } @@ -570,12 +708,15 @@ export class PartialOperationalState extends DeviceState { logger.error(`Failed to persist device on partial completion: ${e}`) } - logger.error({ - deviceId: device.identifier, - reason: this.failureContext.reason, - blockingState: this.failureContext.blockingState, - state: 'partial_operational', - }, '[StateMachine] Session completed with degraded status') + logger.error( + { + deviceId: device.identifier, + reason: this.failureContext.reason, + blockingState: this.failureContext.blockingState, + state: 'partial_operational', + }, + '[StateMachine] Session completed with degraded status' + ) return { isComplete: true, response: '' } } @@ -610,7 +751,9 @@ export class MainStateMachine { } isCompleted(): boolean { - return this.currentState.name === 'operational' || this.currentState.name === 'partial_operational' + return ( + this.currentState.name === 'operational' || this.currentState.name === 'partial_operational' + ) } getCurrentStateName(): string { @@ -623,11 +766,17 @@ export class MainStateMachine { while (transitionCount++ < MAX_STATE_TRANSITIONS) { try { - logger.debug({ state: this.currentState.name, iteration: transitionCount }, '[StateMachine] Executing state') + logger.debug( + { state: this.currentState.name, iteration: transitionCount }, + '[StateMachine] Executing state' + ) const result = await this.currentState.handle(this.context) if (result.nextState) { - logger.debug({ from: this.currentState.name, to: result.nextState.name }, '[StateMachine] State transition') + logger.debug( + { from: this.currentState.name, to: result.nextState.name }, + '[StateMachine] State transition' + ) this.currentState = result.nextState } @@ -637,7 +786,10 @@ export class MainStateMachine { } if (result.response && result.response !== '') { - logger.debug({ state: this.currentState.name, responseLength: result.response.length }, '[StateMachine] Sending response to CPE') + logger.debug( + { state: this.currentState.name, responseLength: result.response.length }, + '[StateMachine] Sending response to CPE' + ) return result.response } @@ -658,11 +810,14 @@ export class MainStateMachine { } // Phase 9: Explicit error handling when max transitions exceeded - logger.error({ - deviceId: this.context.device.identifier, - lastState: this.currentState.name, - sessionId: this.context.session, - }, `State machine stuck in ${this.currentState.name} after ${MAX_STATE_TRANSITIONS} transitions`) + logger.error( + { + deviceId: this.context.device.identifier, + lastState: this.currentState.name, + sessionId: this.context.session, + }, + `State machine stuck in ${this.currentState.name} after ${MAX_STATE_TRANSITIONS} transitions` + ) this.currentState = new PartialOperationalState(this.context.device, { reason: 'max_transitions_exceeded', diff --git a/app/services/cwmp/task_queue.ts b/app/services/cwmp/task_queue.ts index a222c03..64cd962 100644 --- a/app/services/cwmp/task_queue.ts +++ b/app/services/cwmp/task_queue.ts @@ -1,4 +1,4 @@ -import { DeviceId } from '../../domain/device/device_id.js' +import { type DeviceId } from '../../domain/device/device_id.js' import { EventEmitter } from 'node:events' import { CWMPMessageBuilder } from '../../protocol/common/cwmp_message_builder.js' import logger from '@adonisjs/core/services/logger' @@ -168,7 +168,9 @@ export class CWMPTaskQueue extends EventEmitter { /** * Retrieve and mark a pending task as SENT for a device */ - getTaskForDevice(deviceId: DeviceId): { id: string; task: CWMPTaskType; props: CWMPTaskPayload } | undefined { + getTaskForDevice( + deviceId: DeviceId + ): { id: string; task: CWMPTaskType; props: CWMPTaskPayload } | undefined { const deviceKey = JSON.stringify(deviceId) const queue = this.deviceQueues.get(deviceKey) if (!queue || queue.length === 0) return undefined @@ -210,7 +212,11 @@ export class CWMPTaskQueue extends EventEmitter { this.emit('taskFailed', task) } - async executeTaskCallback(taskId: string, status: ResultStatus, data: CWMPTaskResultData): Promise { + async executeTaskCallback( + taskId: string, + status: ResultStatus, + data: CWMPTaskResultData + ): Promise { const callback = this.taskCallbacks.get(taskId) if (callback) { const result = new TaskResult()