Implement support for ChangeDUState method

This commit is contained in:
Zaid Abdulla committed 2021-08-02 13:12:12 -07:00
1 parent 6ce91df127
commit 716f2c6df3
3 files changed
+527 -6

No files matched your search

+333 -1
View File
@@ -57,6 +57,9 @@ import {
FactoryReset, FactoryReset,
AddObjectResponse, AddObjectResponse,
GetParameterValuesResponse, GetParameterValuesResponse,
ChangeDUStateOperation,
DownloadOperation,
DUStateChangeCompleteRequest,
} from "./types"; } from "./types";
import { getRequestOrigin } from "./forwarded"; import { getRequestOrigin } from "./forwarded";
import * as logger from "./logger"; import * as logger from "./logger";
@@ -284,6 +287,8 @@ export async function transferComplete(
sessionContext.deviceData.attributes.revision = revision; sessionContext.deviceData.attributes.revision = revision;
const commandKey = rpcReq.commandKey; const commandKey = rpcReq.commandKey;
const operation = sessionContext.operations[commandKey]; const operation = sessionContext.operations[commandKey];
if (operation.name !== "Download")
throw new Error("Unexpected operation name");
if (!operation) { if (!operation) {
return { return {
@@ -433,6 +438,129 @@ function revertDownloadParameters(
} }
} }
export async function duStateChangeComplete(
sessionContext: SessionContext,
rpcReq: DUStateChangeCompleteRequest
): Promise<{
acsResponse: AcsResponse;
operation: ChangeDUStateOperation;
fault: Fault;
}> {
const revision =
(sessionContext.revisions[sessionContext.revisions.length - 1] || 0) + 1;
sessionContext.deviceData.timestamps.revision = revision;
sessionContext.deviceData.attributes.revision = revision;
const commandKey = rpcReq.commandKey;
const operation = sessionContext.operations[commandKey];
if (operation.name !== "ChangeDUState")
throw new Error("Unexpected operation name");
if (!operation) {
return {
acsResponse: { name: "DUStateChangeCompleteResponse" },
operation: null,
fault: null,
};
}
delete sessionContext.operations[commandKey];
if (!sessionContext.operationsTouched) sessionContext.operationsTouched = {};
sessionContext.operationsTouched[commandKey] = 1;
let toClear = null;
const timestamp = sessionContext.timestamp + sessionContext.iteration + 1;
let fault: Fault;
for (const [i, res] of rpcReq.results.entries()) {
const op = operation.args.operations[i];
if (res.fault && res.fault.faultCode !== "0") {
revertChangeDuStateParameters(sessionContext, op.instance);
fault = {
code: `cwmp.${res.fault.faultCode}`,
message: res.fault.faultString,
timestamp: operation.timestamp,
detail: res.fault,
};
continue;
}
const toSet: Record<string, [string | number | boolean, string]> = {
LastChangeDUState: [operation.timestamp, "xsd:dateTime"],
LastOperationType: [op.operationType, "xsd:string"],
LastURL: [op.url, "xsd:string"],
LastUUID: [op.uuid, "xsd:string"],
LastExecutionEnvRef: [op.executionEnvRef, "xsd:string"],
StartTime: [+res.startTime, "xsd:dateTime"],
CompleteTime: [+res.completeTime, "xsd:dateTime"],
CurrentState: [res.currentState, "xsd:string"],
DeploymentUnitRef: [res.deploymentUnitRef, "xsd:string"],
Version: [res.version, "xsd:string"],
Resolved: [res.resolved, "xsd:string"],
ExecutionUnitRefList: [res.executionUnitRefList, "xsd:string"],
};
for (const [k, v] of Object.entries(toSet)) {
const p = sessionContext.deviceData.paths.add(
Path.parse(`DUStates.${op.instance}.${k}`)
);
toClear = device.set(
sessionContext.deviceData,
p,
timestamp,
{ value: [timestamp, v] },
toClear
);
}
}
if (toClear) {
for (const c of toClear)
device.clear(sessionContext.deviceData, c[0], c[1], c[2], c[3]);
}
return {
acsResponse: { name: "DUStateChangeCompleteResponse" },
operation: operation,
fault: fault,
};
}
function revertChangeDuStateParameters(
sessionContext: SessionContext,
instance
): void {
const timestamp = sessionContext.timestamp + sessionContext.iteration + 1;
let p;
p = sessionContext.deviceData.paths.add(
Path.parse(`DUStates.${instance}.LastChangeDUState`)
);
const lastChangeDuState = sessionContext.deviceData.attributes.get(p);
p = sessionContext.deviceData.paths.add(
Path.parse(`DUStates.${instance}.ChangeDUState`)
);
const toClear = device.set(sessionContext.deviceData, p, timestamp, {
value: [
timestamp,
[
lastChangeDuState && lastChangeDuState.value[1]
? lastChangeDuState.value[1][0]
: 0,
"xsd:dateTime",
],
],
});
if (toClear) {
for (const c of toClear)
device.clear(sessionContext.deviceData, c[0], c[1], c[2], c[3]);
}
}
export async function timeoutOperations( export async function timeoutOperations(
sessionContext: SessionContext sessionContext: SessionContext
): Promise<{ faults: Fault[]; operations: Operation[] }> { ): Promise<{ faults: Fault[]; operations: Operation[] }> {
@@ -794,6 +922,10 @@ function runDeclarations(
downloadsToCreate: new InstanceSet(), downloadsToCreate: new InstanceSet(),
downloadsValues: new Map(), downloadsValues: new Map(),
downloadsDownload: new Map(), downloadsDownload: new Map(),
duStatesToDelete: new Set(),
duStatesToCreate: new InstanceSet(),
duStatesValues: new Map(),
duStatesChangeDUState: new Map(),
reboot: 0, reboot: 0,
factoryReset: 0, factoryReset: 0,
}; };
@@ -1439,6 +1571,147 @@ export async function rpcRequest(
} }
} }
// DUState
index = null;
for (const instance of sessionContext.syncState.duStatesToCreate) {
if (index == null) {
index = 0;
for (const p of sessionContext.deviceData.paths.find(
Path.parse("DUState.*"),
false,
true
)) {
if (
+p.segments[1] > index &&
sessionContext.deviceData.attributes.has(p)
)
index = +p.segments[1];
}
}
++index;
toClear = device.set(
sessionContext.deviceData,
Path.parse("DUState"),
timestamp,
{ object: [timestamp, 1], writable: [timestamp, 1] },
toClear
);
toClear = device.set(
sessionContext.deviceData,
Path.parse(`DUState.${index}`),
timestamp,
{ object: [timestamp, 1], writable: [timestamp, 1] },
toClear
);
const params = {
URL: {
writable: 1,
value: [instance.URL || "", "xsd:string"],
},
UUID: {
writable: 1,
value: [instance.UUID || "", "xsd:string"],
},
ExecutionEnvRef: {
writable: 1,
value: [instance.ExecutionEnvRef || "", "xsd:string"],
},
Username: {
writable: 1,
value: [instance.Username || "", "xsd:string"],
},
Password: {
writable: 1,
value: [instance.Password || "", "xsd:string"],
},
ChangeDUState: {
writable: 1,
value: [instance.ChangeDUState || "", "xsd:dateTime"],
},
OperationType: {
writable: 1,
value: [instance.OperationType || "", "xsd:string"],
},
LastURL: { writable: 0, value: ["", "xsd:string"] },
LastUUID: { writable: 0, value: ["", "xsd:string"] },
LastExecutionEnvRef: { writable: 0, value: ["", "xsd:string"] },
LastChangeDUState: { writable: 0, value: [0, "xsd:dateTime"] },
LastOperationType: { writable: 0, value: [0, "xsd:string"] },
Version: { writable: 0, value: [0, "xsd:string"] },
CurrentState: { writable: 0, value: [0, "xsd:string"] },
ExecutionUnitRefList: { writable: 0, value: [0, "xsd:string"] },
DeploymentUnitRef: { writable: 0, value: [0, "xsd:string"] },
Resolved: { writable: 0, value: [0, "xsd:boolean"] },
StartTime: { writable: 0, value: [0, "xsd:dateTime"] },
CompleteTime: { writable: 0, value: [0, "xsd:dateTime"] },
};
for (const [k, v] of Object.entries(params)) {
toClear = device.set(
sessionContext.deviceData,
Path.parse(`DUState.${index}.${k}`),
timestamp,
{
object: [timestamp, 0],
writable: [timestamp, v.writable as 0 | 1],
value: [
timestamp,
v.value as [string | number | boolean, string],
],
},
toClear
);
}
toClear = device.set(
sessionContext.deviceData,
Path.parse(`DUState.${index}.*`),
timestamp,
null,
toClear
);
}
sessionContext.syncState.duStatesToCreate.clear();
for (const instance of sessionContext.syncState.duStatesToDelete) {
toClear = device.set(
sessionContext.deviceData,
instance,
timestamp,
null,
toClear
);
for (const p of sessionContext.syncState.duStatesValues.keys()) {
if (p.segments[1] === instance.segments[1])
sessionContext.syncState.duStatesValues.delete(p);
}
}
sessionContext.syncState.duStatesToDelete.clear();
for (const [p, v] of sessionContext.syncState.duStatesValues) {
const attrs = sessionContext.deviceData.attributes.get(p);
if (attrs) {
if (attrs.writable && attrs.writable[1] && attrs.value) {
const val = device.sanitizeParameterValue([v, attrs.value[1][1]]);
if (val[0] !== attrs.value[1][0]) {
toClear = device.set(
sessionContext.deviceData,
p,
timestamp,
{ value: [timestamp, val] },
toClear
);
}
}
}
}
if (toClear || sessionContext.deviceData.changes.has("prerequisite")) { if (toClear || sessionContext.deviceData.changes.has("prerequisite")) {
if (toClear) { if (toClear) {
for (const c of toClear) for (const c of toClear)
@@ -2200,6 +2473,26 @@ function processDeclarations(
} }
} }
break; break;
case "DUState":
if (
currentPath.length === 3 &&
currentPath.wildcard === 0 &&
declareAttributeValues &&
declareAttributeValues.value
) {
if (currentPath.segments[2] === "ChangeDUState") {
syncState.duStatesChangeDUState.set(
currentPath,
declareAttributeValues.value[0]
);
} else {
syncState.duStatesValues.set(
currentPath,
declareAttributeValues.value[0]
);
}
}
break;
case "VirtualParameters": case "VirtualParameters":
if (currentPath.length <= 2) { if (currentPath.length <= 2) {
let d; let d;
@@ -2370,6 +2663,10 @@ function processInstances(
if (parent.length !== 1) return; if (parent.length !== 1) return;
instancesToDelete = sessionContext.syncState.downloadsToDelete; instancesToDelete = sessionContext.syncState.downloadsToDelete;
instancesToCreate = sessionContext.syncState.downloadsToCreate; instancesToCreate = sessionContext.syncState.downloadsToCreate;
} else if (parent.segments[0] === "DUStates") {
if (parent.length !== 1) return;
instancesToDelete = sessionContext.syncState.duStatesToDelete;
instancesToCreate = sessionContext.syncState.duStatesToCreate;
} else { } else {
instancesToDelete = sessionContext.syncState.instancesToDelete.get(parent); instancesToDelete = sessionContext.syncState.instancesToDelete.get(parent);
if (instancesToDelete == null) { if (instancesToDelete == null) {
@@ -2658,6 +2955,7 @@ export async function rpcResponse(
"FactoryReset", "FactoryReset",
"VirtualParameters", "VirtualParameters",
"Downloads", "Downloads",
"DUState",
]) { ]) {
const p = sessionContext.deviceData.paths.get(Path.parse(n)); const p = sessionContext.deviceData.paths.get(Path.parse(n));
if (p && sessionContext.deviceData.attributes.has(p)) if (p && sessionContext.deviceData.attributes.has(p))
@@ -2857,7 +3155,7 @@ export async function rpcResponse(
toClear toClear
); );
} else { } else {
const operation = { const operation: DownloadOperation = {
name: "Download", name: "Download",
timestamp: sessionContext.timestamp, timestamp: sessionContext.timestamp,
provisions: sessionContext.provisions, provisions: sessionContext.provisions,
@@ -2881,6 +3179,40 @@ export async function rpcResponse(
sessionContext.operationsTouched = {}; sessionContext.operationsTouched = {};
sessionContext.operationsTouched[rpcReq.commandKey] = 1; sessionContext.operationsTouched[rpcReq.commandKey] = 1;
} }
} else if (rpcRes.name === "ChangeDUStateResponse") {
if (rpcReq.name !== "ChangeDUState")
return invalidResponse("Response name does not match request name");
for (const op of rpcReq.operations) {
toClear = device.set(
sessionContext.deviceData,
Path.parse(`DuStates.${op.instance}.ChangeDUState`),
timestamp + 1,
{ value: [timestamp + 1, [sessionContext.timestamp, "xsd:dateTime"]] },
toClear
);
}
const operation = {
name: "ChangeDUState",
timestamp: sessionContext.timestamp,
provisions: sessionContext.provisions,
channels: sessionContext.channels,
retries: {},
args: {
operations: rpcReq.operations,
},
};
for (const channel of Object.keys(sessionContext.channels)) {
if (sessionContext.retries[channel] != null)
operation.retries[channel] = sessionContext.retries[channel];
}
sessionContext.operations[rpcReq.commandKey] = operation as Operation;
if (!sessionContext.operationsTouched)
sessionContext.operationsTouched = {};
sessionContext.operationsTouched[rpcReq.commandKey] = 1;
} else { } else {
return invalidResponse("Response name not recognized"); return invalidResponse("Response name not recognized");
} }
+129
View File
@@ -48,6 +48,9 @@ import {
GetRPCMethodsRequest, GetRPCMethodsRequest,
RequestDownloadRequest, RequestDownloadRequest,
AcsResponse, AcsResponse,
ChangeDUState,
ChangeDUStateResponse,
DUStateChangeCompleteRequest,
} from "./types"; } from "./types";
import Path from "./common/path"; import Path from "./common/path";
@@ -573,6 +576,29 @@ function DownloadResponse(xml: Element): DownloadResponse {
}; };
} }
function ChangeDUState(methodRequest: ChangeDUState): string {
const operations = methodRequest.operations.map((op) => {
if (op.operationType === "install")
return `<Operations xsi:type="cwmp:InstallOpStruct"><UUID>${op.uuid}</UUID><URL>${op.url}</URL><ExecutionEnvRef>${op.executionEnvRef}</ExecutionEnvRef><Username>${op.username}</Username><Password>${op.password}</Password></Operations>`;
else if (op.operationType === "update")
return `<Operations xsi:type="cwmp:UpdateOpStruct"><UUID>${op.uuid}</UUID><URL>${op.url}</URL><Version>${op.version}</Version><Username>${op.username}</Username><Password>${op.password}</Password></Operations>`;
else if (op.operationType === "uninstall")
return `<Operations xsi:type="cwmp:UninstallOpStruct"><UUID>${op.uuid}</UUID><Version>${op.version}</Version><ExecutionEnvRef>${op.executionEnvRef}</ExecutionEnvRef></Operations>`;
return "";
});
return `<cwmp:ChangeDUState><CommandKey>${
methodRequest.commandKey || ""
}</CommandKey>${operations.join("")}</cwmp:ChangeDUState>`;
}
function ChangeDUStateResponse(): ChangeDUStateResponse {
return {
name: "ChangeDUStateResponse",
};
}
function Inform(xml: Element): InformRequest { function Inform(xml: Element): InformRequest {
let retryCount: number, evnt: string[]; let retryCount: number, evnt: string[];
let parameterList: [Path, string | number | boolean, string][]; let parameterList: [Path, string | number | boolean, string][];
@@ -719,6 +745,97 @@ function TransferCompleteResponse(): string {
return "<cwmp:TransferCompleteResponse></cwmp:TransferCompleteResponse>"; return "<cwmp:TransferCompleteResponse></cwmp:TransferCompleteResponse>";
} }
function DUStateChangeComplete(xml: Element): DUStateChangeCompleteRequest {
let commandKey: string,
results: DUStateChangeCompleteRequest["results"],
res: DUStateChangeCompleteRequest["results"][0];
for (const c of xml.children) {
switch (c.localName) {
case "CommandKey":
commandKey = c.text;
break;
case "Results":
if (!results) results = [];
res = {
uuid: null,
deploymentUnitRef: null,
version: null,
currentState: null,
resolved: true,
executionUnitRefList: null,
startTime: null,
completeTime: null,
fault: null,
};
for (const cc of c.children) {
switch (cc.localName) {
case "UUID":
res.uuid = cc.text;
break;
case "DeploymentUnitRef":
res.deploymentUnitRef = cc.text;
break;
case "Version":
res.version = cc.text;
break;
case "CurrentState":
res.currentState = cc.text;
break;
case "Resolved":
res.resolved = parseBool(cc.text);
break;
case "ExecutionUnitRefList":
res.executionUnitRefList = cc.text;
break;
case "StartTime":
res.startTime = Date.parse(c.text);
break;
case "CompleteTime":
res.completeTime = Date.parse(c.text);
break;
case "Fault":
res.fault = {
faultCode: cc.children.find((n) => n.localName === "FaultCode")
.text,
faultString: cc.children.find(
(n) => n.localName === "FaultString"
).text,
};
break;
}
results.push(res);
}
break;
}
}
if (commandKey == null) {
warnings.push({
message: "Missing or invalid XML node",
element: "CommandKey",
});
commandKey = "";
}
if (!results) {
warnings.push({
message: "Missing or invalid XML node",
element: "Results",
});
results = [];
}
return {
name: "DUStateChangeComplete",
commandKey: commandKey,
results: results,
};
}
function DUStateChangeCompleteResponse(): string {
return "<cwmp:DUStateChangeCompleteResponse></cwmp:DUStateChangeCompleteResponse>";
}
function RequestDownload(xml: Element): RequestDownloadRequest { function RequestDownload(xml: Element): RequestDownloadRequest {
return { return {
name: "RequestDownload", name: "RequestDownload",
@@ -923,6 +1040,9 @@ export function request(
case "TransferComplete": case "TransferComplete":
rpc.cpeRequest = TransferComplete(methodElement); rpc.cpeRequest = TransferComplete(methodElement);
break; break;
case "DUStateChangeComplete":
rpc.cpeRequest = DUStateChangeComplete(methodElement);
break;
case "RequestDownload": case "RequestDownload":
rpc.cpeRequest = RequestDownload(methodElement); rpc.cpeRequest = RequestDownload(methodElement);
break; break;
@@ -956,6 +1076,9 @@ export function request(
case "DownloadResponse": case "DownloadResponse":
rpc.cpeResponse = DownloadResponse(methodElement); rpc.cpeResponse = DownloadResponse(methodElement);
break; break;
case "ChangeDUStateResponse":
rpc.cpeResponse = ChangeDUStateResponse();
break;
case "Fault": case "Fault":
rpc.cpeFault = fault(methodElement); rpc.cpeFault = fault(methodElement);
break; break;
@@ -1009,6 +1132,9 @@ export function response(rpc: {
case "TransferCompleteResponse": case "TransferCompleteResponse":
body = TransferCompleteResponse(); body = TransferCompleteResponse();
break; break;
case "DUStateChangeCompleteResponse":
body = DUStateChangeCompleteResponse();
break;
case "RequestDownloadResponse": case "RequestDownloadResponse":
body = RequestDownloadResponse(); body = RequestDownloadResponse();
break; break;
@@ -1051,6 +1177,9 @@ export function response(rpc: {
case "Download": case "Download":
body = Download(rpc.acsRequest); body = Download(rpc.acsRequest);
break; break;
case "ChangeDUState":
body = ChangeDUState(rpc.acsRequest);
break;
default: default:
throw new Error( throw new Error(
`Unknown method request ${(rpc.acsRequest as AcsRequest).name}` `Unknown method request ${(rpc.acsRequest as AcsRequest).name}`
+65 -5
View File
@@ -121,6 +121,10 @@ export interface SyncState {
downloadsToCreate: InstanceSet; downloadsToCreate: InstanceSet;
downloadsValues: Map<Path, string | number>; downloadsValues: Map<Path, string | number>;
downloadsDownload: Map<Path, number>; downloadsDownload: Map<Path, number>;
duStatesToDelete: Set<Path>;
duStatesToCreate: InstanceSet;
duStatesValues: Map<Path, string | number>;
duStatesChangeDUState: Map<Path, number>;
reboot: number; reboot: number;
factoryReset: number; factoryReset: number;
} }
@@ -183,12 +187,17 @@ export interface Task {
provisions?: (string | number | boolean)[][]; provisions?: (string | number | boolean)[][];
} }
export interface Operation { interface OperationBase {
name: string; name: string;
timestamp: number; timestamp: number;
provisions: string[][]; provisions: string[][];
channels: { [channel: string]: number }; channels: { [channel: string]: number };
retries: { [channel: string]: number }; retries: { [channel: string]: number };
args: any;
}
export interface DownloadOperation extends OperationBase {
name: "Download";
args: { args: {
instance: string; instance: string;
fileType: string; fileType: string;
@@ -197,6 +206,15 @@ export interface Operation {
}; };
} }
export interface ChangeDUStateOperation extends OperationBase {
name: "ChangeDUState";
args: {
operations: ChangeDUState["operations"];
};
}
export type Operation = DownloadOperation | ChangeDUStateOperation;
export type AcsRequest = export type AcsRequest =
| GetParameterNames | GetParameterNames
| GetParameterValues | GetParameterValues
@@ -207,7 +225,8 @@ export type AcsRequest =
| DeleteObject | DeleteObject
| FactoryReset | FactoryReset
| Reboot | Reboot
| Download; | Download
| ChangeDUState;
export interface GetParameterNames { export interface GetParameterNames {
name: "GetParameterNames"; name: "GetParameterNames";
@@ -277,6 +296,21 @@ export interface Download {
failureUrl?: string; failureUrl?: string;
} }
export interface ChangeDUState {
name: "ChangeDUState";
commandKey: string;
operations: {
operationType: "install" | "update" | "uninstall";
instance: string;
url: string;
uuid: string;
version?: string;
executionEnvRef?: string;
username?: string;
password?: string;
}[];
}
export interface SpvFault { export interface SpvFault {
parameterName: string; parameterName: string;
faultCode: string; faultCode: string;
@@ -305,7 +339,8 @@ export type CpeResponse =
| DeleteObjectResponse | DeleteObjectResponse
| RebootResponse | RebootResponse
| FactoryResetResponse | FactoryResetResponse
| DownloadResponse; | DownloadResponse
| ChangeDUStateResponse;
export interface GetParameterNamesResponse { export interface GetParameterNamesResponse {
name: "GetParameterNamesResponse"; name: "GetParameterNamesResponse";
@@ -342,6 +377,10 @@ export interface DeleteObjectResponse {
status: number; status: number;
} }
export interface ChangeDUStateResponse {
name: "ChangeDUStateResponse";
}
export interface RebootResponse { export interface RebootResponse {
name: "RebootResponse"; name: "RebootResponse";
} }
@@ -361,7 +400,8 @@ export type CpeRequest =
| InformRequest | InformRequest
| TransferCompleteRequest | TransferCompleteRequest
| GetRPCMethodsRequest | GetRPCMethodsRequest
| RequestDownloadRequest; | RequestDownloadRequest
| DUStateChangeCompleteRequest;
export interface InformRequest { export interface InformRequest {
name: "Inform"; name: "Inform";
@@ -384,6 +424,22 @@ export interface TransferCompleteRequest {
completeTime?: number; completeTime?: number;
} }
export interface DUStateChangeCompleteRequest {
name: "DUStateChangeComplete";
commandKey: string;
results: {
uuid: string;
deploymentUnitRef: string;
version: string;
currentState: string;
resolved: boolean;
executionUnitRefList: string;
fault?: { faultCode: string; faultString: string };
startTime?: number;
completeTime?: number;
}[];
}
export interface GetRPCMethodsRequest { export interface GetRPCMethodsRequest {
name: "GetRPCMethods"; name: "GetRPCMethods";
} }
@@ -397,7 +453,8 @@ export type AcsResponse =
| InformResponse | InformResponse
| GetRPCMethodsResponse | GetRPCMethodsResponse
| TransferCompleteResponse | TransferCompleteResponse
| RequestDownloadResponse; | RequestDownloadResponse
| DUStateChangeCompleteResponse;
export interface InformResponse { export interface InformResponse {
name: "InformResponse"; name: "InformResponse";
@@ -415,6 +472,9 @@ export interface TransferCompleteResponse {
export interface RequestDownloadResponse { export interface RequestDownloadResponse {
name: "RequestDownloadResponse"; name: "RequestDownloadResponse";
} }
export interface DUStateChangeCompleteResponse {
name: "DUStateChangeCompleteResponse";
}
export interface QueryOptions { export interface QueryOptions {
projection?: any; projection?: any;