feat: TR-069 CWMP state machine hardening + exchange viewer improvements
- TR-069 CWMP state machine hardening (14 phases): shared event utility, multi-event routing, GPN circuit breaker, IdleState GPN guard, expanded GPN fault codes, CriticalReadsState, ConfigEval GPN guard, task queue race condition fix, explicit error handling, terminal states (OperationalState/PartialOperationalState), consolidated reclassify, crash pattern detection, removed enqueueImmediate, state flow wiring - Fix SOAP Fault not processed by state machine (soap-env:Fault detection) - Fix GPN circuit breaker not persisting after session drops - Fix CriticalReadsState GPV response parsing (xsi:type normalization) - Fix CriticalReadsState not processing GPV response values - Skip SPV when device is already compliant (ProvisioningCode check) - Add Device.DeviceInfo.ModelName to critical reads GPV - Fix TypeError on DeviceDetail parameters view (null guards) - Log empty 204 responses in exchange trace UI - TR-181 catalog: type corrections and new entries (+6640 lines) - SequenceViewer: replace session-nav buttons with compact dropdown select - SequenceViewer: fix CPE/ACS lifeline headers wrapping to 2 lines - Add 20 unit tests for state machine fixes (SOAP Fault, GPN circuit breaker, CriticalReads, stale session, ConfigEval/ComplianceUpdate skips) Generated with [Continue](https://continue.dev) Co-Authored-By: Continue <noreply@continue.dev>
This commit is contained in:
1 parent
dabe15839d
commit
aad93911e4
35 files changed
+8593
-4497
No files matched your search
@@ -3,12 +3,12 @@ import { buildSoapEnvelope } from '../protocol/soap.js'
|
||||
import { CWMPMessageBuilder } from '../protocol/common/cwmp_message_builder.js'
|
||||
import { StateMachineManager } from '../services/cwmp/state_machine_manager.js'
|
||||
import { Device, detectDataModelFromParams } from '../domain/device/device.js'
|
||||
import { reclassifyDeviceFromCatalog } from '../services/parameter_writability.js'
|
||||
import { DeviceId } from '../domain/device/device_id.js'
|
||||
import { DeviceParameter, DeviceProperty } from '../domain/device/device_parameter.js'
|
||||
import DeviceModel from '../models/device.js'
|
||||
import { getDeviceHashIdentifier } from '../utils/device_utils.js'
|
||||
import { getReferenceEngine } from '../services/references/reference_engine.js'
|
||||
import { normalizeEventCode } from '../utils/cwmp-events.js'
|
||||
import logger from '@adonisjs/core/services/logger'
|
||||
|
||||
export default class CwmpController {
|
||||
@@ -118,6 +118,12 @@ 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')
|
||||
} else {
|
||||
logger.info(
|
||||
{ deviceId: deviceId.toIdentifier(), gpnStatus: device.gpnStatus, circuitBreaker: device.gpnCircuitBreaker },
|
||||
'[handleInform] Device loaded from DB'
|
||||
)
|
||||
}
|
||||
device.updateLastSeen()
|
||||
|
||||
@@ -127,11 +133,6 @@ export default class CwmpController {
|
||||
device.updateFromValues(paramValues)
|
||||
}
|
||||
|
||||
// Reclassify properties → parameters using catalog when GPN is unsupported
|
||||
if (device.gpnStatus === 'unsupported') {
|
||||
reclassifyDeviceFromCatalog(device)
|
||||
}
|
||||
|
||||
device.setTmpParamValues(paramValues)
|
||||
|
||||
// Detect TR data model from Inform parameter names (Device.* = TR-181, InternetGatewayDevice.* = TR-098)
|
||||
@@ -157,6 +158,8 @@ export default class CwmpController {
|
||||
parameters: device.getParameters().map((p) => p.toSnapshot()),
|
||||
gpnStatus: device.gpnStatus,
|
||||
dataModel: device.dataModel,
|
||||
gpnCircuitBreaker: device.gpnCircuitBreaker,
|
||||
lastGpnSentDate: device.lastGpnSentDate,
|
||||
updatedAt: new Date(),
|
||||
})
|
||||
logger.info({ hashId }, 'Inform received')
|
||||
@@ -174,12 +177,23 @@ export default class CwmpController {
|
||||
logger.info({ events }, 'CWMP Inform events extracted')
|
||||
|
||||
// Track last periodic inform timestamp
|
||||
const isPeriodic = events.some((e) => String(e).toUpperCase().includes('PERIODIC') || String(e) === '2')
|
||||
const isBootstrap = events.some((e) => String(e).toUpperCase().includes('BOOTSTRAP') || String(e) === '0')
|
||||
const normalizedEvents = events.map((e) => normalizeEventCode(e))
|
||||
const isPeriodic = normalizedEvents.some((e) => e === 'PERIODIC')
|
||||
const isBootstrap = normalizedEvents.some((e) => e === 'BOOTSTRAP')
|
||||
if (isPeriodic || isBootstrap) {
|
||||
await DeviceModel.updateOne({ identifier: hashId }, { $set: { lastPeriodicInformAt: new Date() } }).catch(() => {})
|
||||
}
|
||||
|
||||
// Phase 12: Crash pattern detection
|
||||
const hasBoot = normalizedEvents.some((e) => e === 'BOOT')
|
||||
if (hasBoot && device.lastGpnSentDate) {
|
||||
const timeSinceGpn = Date.now() - device.lastGpnSentDate.getTime()
|
||||
if (timeSinceGpn < 60000) {
|
||||
device.recordCrashPattern()
|
||||
logger.warn({ hashId, timeSinceGpn }, 'Possible GPN-induced crash detected for device')
|
||||
}
|
||||
}
|
||||
|
||||
// Start state machine
|
||||
await manager.startStateMachine(device, session, { events })
|
||||
manager.bindRemoteToDevice(remoteKey, device.deviceId)
|
||||
@@ -204,25 +218,6 @@ export default class CwmpController {
|
||||
logger.error({ error: _e?.message, stack: _e?.stack }, '[References] Error during resolution')
|
||||
}
|
||||
|
||||
// After Inform for onboarding events, enqueue critical reads using the detected data model
|
||||
const onlyConnReq = events.length > 0 && events.every((e) => e === '6 CONNECTION REQUEST')
|
||||
if (!onlyConnReq) {
|
||||
try {
|
||||
const isTr181 = device.dataModel === 'tr-181'
|
||||
const root = isTr181 ? 'Device' : 'InternetGatewayDevice'
|
||||
const criticalPaths = [
|
||||
`${root}.DeviceInfo.HardwareVersion`,
|
||||
`${root}.DeviceInfo.SoftwareVersion`,
|
||||
`${root}.ManagementServer.ConnectionRequestURL`,
|
||||
...(isTr181 ? [] : [`${root}.DeviceInfo.SpecVersion`]),
|
||||
]
|
||||
logger.debug({ dataModel: device.dataModel, criticalPaths }, 'Enqueueing critical reads')
|
||||
await manager.enqueueImmediate(device, 'get_parameter_values', criticalPaths)
|
||||
} catch (_e) {
|
||||
// Non-critical
|
||||
}
|
||||
}
|
||||
|
||||
return { xml: CWMPMessageBuilder.informResponse(cwmpId), hashId }
|
||||
}
|
||||
|
||||
@@ -280,6 +275,19 @@ export default class CwmpController {
|
||||
device.setGpnStatus(record.gpnStatus ?? 'unknown')
|
||||
device.setDataModel(record.dataModel ?? 'unknown')
|
||||
|
||||
// Restore circuit breaker state
|
||||
if (record.gpnCircuitBreaker) {
|
||||
device.setGpnCircuitBreaker({
|
||||
failureCount: record.gpnCircuitBreaker.failureCount ?? 0,
|
||||
lastFailureDate: record.gpnCircuitBreaker.lastFailureDate ? new Date(record.gpnCircuitBreaker.lastFailureDate) : null,
|
||||
state: record.gpnCircuitBreaker.state ?? 'closed',
|
||||
cooldownUntil: record.gpnCircuitBreaker.cooldownUntil ? new Date(record.gpnCircuitBreaker.cooldownUntil) : null,
|
||||
})
|
||||
}
|
||||
if (record.lastGpnSentDate) {
|
||||
device.lastGpnSentDate = new Date(record.lastGpnSentDate)
|
||||
}
|
||||
|
||||
return device
|
||||
} catch (_error) {
|
||||
return null
|
||||
|
||||
Reference in new issue
Block a user