Read session lock to detect task completion
This commit is contained in:
1 parent
30bea8ac57
commit
fa1e40e370
4 files changed
+152
-102
No files matched your search
+35
-24
@@ -37,7 +37,7 @@ import { evaluate } from "./common/expression";
|
|||||||
export async function connectionRequest(
|
export async function connectionRequest(
|
||||||
deviceId: string,
|
deviceId: string,
|
||||||
device?: Record<string, { value?: [boolean | number | string, string] }>
|
device?: Record<string, { value?: [boolean | number | string, string] }>
|
||||||
): Promise<void> {
|
): Promise<string> {
|
||||||
if (!device) {
|
if (!device) {
|
||||||
const res = await db.devicesCollection.findOne({ _id: deviceId });
|
const res = await db.devicesCollection.findOne({ _id: deviceId });
|
||||||
if (!res) throw new Error("No such device");
|
if (!res) throw new Error("No such device");
|
||||||
@@ -156,8 +156,7 @@ export async function connectionRequest(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
const status = await httpConnectionRequest(
|
||||||
await httpConnectionRequest(
|
|
||||||
connectionRequestUrl,
|
connectionRequestUrl,
|
||||||
authExp,
|
authExp,
|
||||||
CONNECTION_REQUEST_ALLOW_BASIC_AUTH,
|
CONNECTION_REQUEST_ALLOW_BASIC_AUTH,
|
||||||
@@ -165,34 +164,46 @@ export async function connectionRequest(
|
|||||||
debug,
|
debug,
|
||||||
deviceId
|
deviceId
|
||||||
);
|
);
|
||||||
} catch (err) {
|
|
||||||
if (!udpProm) throw err;
|
if (udpProm) {
|
||||||
await udpProm;
|
await udpProm;
|
||||||
}
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function watchTask(
|
return status;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function awaitSessionStart(
|
||||||
deviceId: string,
|
deviceId: string,
|
||||||
taskId: string,
|
lastInform: number,
|
||||||
timeout: number
|
timeout: number
|
||||||
): Promise<string> {
|
): Promise<boolean> {
|
||||||
await new Promise((resolve) => setTimeout(resolve, 500));
|
const now = Date.now();
|
||||||
|
const device = await db.devicesCollection.findOne(
|
||||||
const task = await db.tasksCollection.findOne(
|
{ _id: deviceId },
|
||||||
{ _id: taskId },
|
{ projection: { _lastInform: 1 } }
|
||||||
{ projection: { _id: 1 } }
|
|
||||||
);
|
);
|
||||||
if (!task) return "completed";
|
const li = (device["_lastInform"] as Date).getTime();
|
||||||
|
if (li > lastInform) return true;
|
||||||
|
const token = await cache.get(`cwmp_session_${deviceId}`);
|
||||||
|
if (token) return true;
|
||||||
|
if (timeout < 500) return false;
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||||
|
timeout -= Date.now() - now;
|
||||||
|
return awaitSessionStart(deviceId, lastInform, timeout);
|
||||||
|
}
|
||||||
|
|
||||||
const q = { _id: `${deviceId}:task_${taskId}` };
|
export async function awaitSessionEnd(
|
||||||
const fault = await db.faultsCollection.findOne(q, {
|
deviceId: string,
|
||||||
projection: { _id: 1 },
|
timeout: number
|
||||||
});
|
): Promise<boolean> {
|
||||||
if (fault) return "fault";
|
const now = Date.now();
|
||||||
|
const token = await cache.get(`cwmp_session_${deviceId}`);
|
||||||
if ((timeout -= 500) <= 0) return "timeout";
|
if (!token) return true;
|
||||||
|
if (timeout < 500) return false;
|
||||||
return watchTask(deviceId, taskId, timeout);
|
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||||
|
timeout -= Date.now() - now;
|
||||||
|
return awaitSessionEnd(deviceId, timeout);
|
||||||
}
|
}
|
||||||
|
|
||||||
function sanitizeTask(task): void {
|
function sanitizeTask(task): void {
|
||||||
|
|||||||
@@ -97,10 +97,10 @@ export async function httpConnectionRequest(
|
|||||||
timeout: number,
|
timeout: number,
|
||||||
_debug: boolean,
|
_debug: boolean,
|
||||||
deviceId: string
|
deviceId: string
|
||||||
): Promise<void> {
|
): Promise<string> {
|
||||||
const options: http.RequestOptions = parse(address);
|
const options: http.RequestOptions = parse(address);
|
||||||
if (options.protocol !== "http:")
|
if (options.protocol !== "http:")
|
||||||
throw new Error("Invalid connection request URL or protocol");
|
return "Invalid connection request URL or protocol";
|
||||||
|
|
||||||
options.agent = new http.Agent({
|
options.agent = new http.Agent({
|
||||||
maxSockets: 1,
|
maxSockets: 1,
|
||||||
@@ -115,8 +115,7 @@ export async function httpConnectionRequest(
|
|||||||
let opts = options;
|
let opts = options;
|
||||||
if (authHeader) {
|
if (authHeader) {
|
||||||
if (authHeader["method"] === "Basic") {
|
if (authHeader["method"] === "Basic") {
|
||||||
if (!allowBasicAuth)
|
if (!allowBasicAuth) return "Basic HTTP authentication not allowed";
|
||||||
throw new Error("Basic HTTP authentication not allowed");
|
|
||||||
|
|
||||||
opts = Object.assign(
|
opts = Object.assign(
|
||||||
{
|
{
|
||||||
@@ -143,7 +142,7 @@ export async function httpConnectionRequest(
|
|||||||
options
|
options
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
throw new Error("Unrecognized auth method");
|
return "Unrecognized auth method";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -152,8 +151,8 @@ export async function httpConnectionRequest(
|
|||||||
// Workaround for some devices unexpectedly closing the connection
|
// Workaround for some devices unexpectedly closing the connection
|
||||||
if (res.statusCode === 0 && authHeader)
|
if (res.statusCode === 0 && authHeader)
|
||||||
res = await httpGet(opts, timeout, _debug, deviceId);
|
res = await httpGet(opts, timeout, _debug, deviceId);
|
||||||
if (res.statusCode === 0) throw new Error("Device is offline");
|
if (res.statusCode === 0) return "Device is offline";
|
||||||
if (res.statusCode === 200 || res.statusCode === 204) return;
|
if (res.statusCode === 200 || res.statusCode === 204) return "";
|
||||||
|
|
||||||
if (res.statusCode === 401 && res.headers["www-authenticate"]) {
|
if (res.statusCode === 401 && res.headers["www-authenticate"]) {
|
||||||
authHeader = auth.parseWwwAuthenticateHeader(
|
authHeader = auth.parseWwwAuthenticateHeader(
|
||||||
@@ -161,12 +160,10 @@ export async function httpConnectionRequest(
|
|||||||
);
|
);
|
||||||
[username, password, authExp] = await extractAuth(authExp, false);
|
[username, password, authExp] = await extractAuth(authExp, false);
|
||||||
} else {
|
} else {
|
||||||
throw new Error(
|
return `Unexpected response code from device: ${res.statusCode}`;
|
||||||
`Unexpected response code from device: ${res.statusCode}`
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
throw new Error("Incorrect connection request credentials");
|
return "Incorrect connection request credentials";
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function udpConnectionRequest(
|
export async function udpConnectionRequest(
|
||||||
|
|||||||
+75
-56
@@ -392,47 +392,91 @@ export function listener(
|
|||||||
task.device = deviceId;
|
task.device = deviceId;
|
||||||
apiFunctions
|
apiFunctions
|
||||||
.insertTasks(task)
|
.insertTasks(task)
|
||||||
.then(() => {
|
.then(async () => {
|
||||||
cache
|
const lastInform = Date.now();
|
||||||
.del(`${deviceId}_tasks_faults_operations`)
|
const notInSession = await apiFunctions.awaitSessionEnd(
|
||||||
.then(() => {
|
deviceId,
|
||||||
if (urlParts.query.connection_request != null) {
|
30000
|
||||||
apiFunctions
|
);
|
||||||
.connectionRequest(deviceId)
|
await cache.del(`${deviceId}_tasks_faults_operations`);
|
||||||
.then(() => {
|
if (urlParts.query.connection_request == null) {
|
||||||
const taskTimeout =
|
response.writeHead(202, {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
});
|
||||||
|
response.end(JSON.stringify(task));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!notInSession) {
|
||||||
|
response.writeHead(202, "Task queued but not processed", {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
});
|
||||||
|
response.end(JSON.stringify(task));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const status = await apiFunctions.connectionRequest(deviceId);
|
||||||
|
|
||||||
|
if (status) {
|
||||||
|
response.writeHead(202, status, {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
});
|
||||||
|
response.end(JSON.stringify(task));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const onlineThreshold =
|
||||||
(urlParts.query.timeout &&
|
(urlParts.query.timeout &&
|
||||||
parseInt(urlParts.query.timeout as string)) ||
|
parseInt(urlParts.query.timeout as string)) ||
|
||||||
(config.get(
|
(config.get("DEVICE_ONLINE_THRESHOLD", deviceId) as number);
|
||||||
"DEVICE_ONLINE_THRESHOLD",
|
|
||||||
deviceId
|
|
||||||
) as number);
|
|
||||||
|
|
||||||
apiFunctions
|
const sessionStarted = await apiFunctions.awaitSessionStart(
|
||||||
.watchTask(deviceId, task._id, taskTimeout)
|
deviceId,
|
||||||
.then((status) => {
|
lastInform,
|
||||||
if (status === "timeout") {
|
onlineThreshold
|
||||||
response.writeHead(
|
);
|
||||||
202,
|
if (!sessionStarted) {
|
||||||
"Task queued but not processed",
|
response.writeHead(202, "Task queued but not processed", {
|
||||||
{
|
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
|
});
|
||||||
|
response.end(JSON.stringify(task));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const sessionEnded = await apiFunctions.awaitSessionEnd(
|
||||||
|
deviceId,
|
||||||
|
120000
|
||||||
|
);
|
||||||
|
if (!sessionEnded) {
|
||||||
|
response.writeHead(202, "Task queued but not processed", {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
});
|
||||||
|
response.end(JSON.stringify(task));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const prom1 = collections.tasks.findOne(
|
||||||
|
{ _id: task._id },
|
||||||
|
{ projection: { _id: 1 } }
|
||||||
|
);
|
||||||
|
const prom2 = collections.faults.findOne(
|
||||||
|
{ _id: `${deviceId}:task_${task._id}` },
|
||||||
|
{
|
||||||
|
projection: { _id: 1 },
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
response.end(JSON.stringify(task));
|
|
||||||
} else if (status === "fault") {
|
|
||||||
collections.tasks.findOne(
|
|
||||||
{ _id: task._id },
|
|
||||||
(err, task2) => {
|
|
||||||
if (err)
|
|
||||||
return void throwError(err, response);
|
|
||||||
|
|
||||||
|
const [t, f] = await Promise.all([prom1, prom2]);
|
||||||
|
if (f) {
|
||||||
response.writeHead(202, "Task faulted", {
|
response.writeHead(202, "Task faulted", {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
});
|
});
|
||||||
response.end(JSON.stringify(task2));
|
response.end(JSON.stringify(t || task));
|
||||||
}
|
} else if (t) {
|
||||||
);
|
response.writeHead(202, "Task queued but not processed", {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
});
|
||||||
|
response.end(JSON.stringify(t));
|
||||||
} else {
|
} else {
|
||||||
response.writeHead(200, {
|
response.writeHead(200, {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
@@ -445,31 +489,6 @@ export function listener(
|
|||||||
throwError(err, response);
|
throwError(err, response);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
response.writeHead(202, err.message, {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
});
|
|
||||||
response.end(JSON.stringify(task));
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
response.writeHead(202, {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
});
|
|
||||||
response.end(JSON.stringify(task));
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
setTimeout(() => {
|
|
||||||
throwError(err, response);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
setTimeout(() => {
|
|
||||||
throwError(err, response);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
} else if (urlParts.query.connection_request != null) {
|
} else if (urlParts.query.connection_request != null) {
|
||||||
// No task, send connection request only
|
// No task, send connection request only
|
||||||
apiFunctions
|
apiFunctions
|
||||||
|
|||||||
+34
-11
@@ -24,7 +24,8 @@ import { getUsers } from "../local-cache";
|
|||||||
import { hashPassword } from "../auth";
|
import { hashPassword } from "../auth";
|
||||||
import {
|
import {
|
||||||
insertTasks,
|
insertTasks,
|
||||||
watchTask,
|
awaitSessionEnd,
|
||||||
|
awaitSessionStart,
|
||||||
connectionRequest,
|
connectionRequest,
|
||||||
deleteDevice,
|
deleteDevice,
|
||||||
} from "../api-functions";
|
} from "../api-functions";
|
||||||
@@ -104,23 +105,45 @@ export async function postTasks(
|
|||||||
const statuses = tasks.map((t) => {
|
const statuses = tasks.map((t) => {
|
||||||
return { _id: t._id, status: "pending" };
|
return { _id: t._id, status: "pending" };
|
||||||
});
|
});
|
||||||
|
const lastInform = Date.now();
|
||||||
await del(`${deviceId}_tasks_faults_operations`);
|
const notInSession = await awaitSessionEnd(deviceId, 30000);
|
||||||
|
if (!notInSession) {
|
||||||
try {
|
|
||||||
await connectionRequest(deviceId, device);
|
|
||||||
} catch (err) {
|
|
||||||
await Promise.all(statuses.map((t) => db.deleteTask(new ObjectID(t._id))));
|
await Promise.all(statuses.map((t) => db.deleteTask(new ObjectID(t._id))));
|
||||||
return {
|
return {
|
||||||
connectionRequest: err.message,
|
connectionRequest: "Session took too long to complete",
|
||||||
tasks: statuses,
|
tasks: statuses,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const sample = tasks[tasks.length - 1];
|
await del(`${deviceId}_tasks_faults_operations`);
|
||||||
|
|
||||||
// Waiting for session to finish or timeout
|
const status = await connectionRequest(deviceId, device);
|
||||||
await watchTask(deviceId, sample._id, timeout);
|
|
||||||
|
if (status) {
|
||||||
|
await Promise.all(statuses.map((t) => db.deleteTask(new ObjectID(t._id))));
|
||||||
|
return {
|
||||||
|
connectionRequest: status,
|
||||||
|
tasks: statuses,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const sessionStarted = await awaitSessionStart(deviceId, lastInform, timeout);
|
||||||
|
if (!sessionStarted) {
|
||||||
|
await Promise.all(statuses.map((t) => db.deleteTask(new ObjectID(t._id))));
|
||||||
|
return {
|
||||||
|
connectionRequest: "No contact from CPE",
|
||||||
|
tasks: statuses,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const sessionEnded = await awaitSessionEnd(deviceId, 120000);
|
||||||
|
if (!sessionEnded) {
|
||||||
|
await Promise.all(statuses.map((t) => db.deleteTask(new ObjectID(t._id))));
|
||||||
|
return {
|
||||||
|
connectionRequest: "Session took too long to complete",
|
||||||
|
tasks: statuses,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const promises = [];
|
const promises = [];
|
||||||
for (const s of statuses) {
|
for (const s of statuses) {
|
||||||
|
|||||||
Reference in new issue
Block a user