diff --git a/admin/index.html b/admin/index.html
index 1bc2077..e3d41ef 100644
--- a/admin/index.html
+++ b/admin/index.html
@@ -22,6 +22,14 @@
color: red;
}
+ .task-message.task-result-okay {
+ color: green;
+ }
+
+ .task-message.task-result-warning {
+ color: red;
+ }
+
.task-description {
max-width: 220px;
}
diff --git a/admin/js/main.js b/admin/js/main.js
index e4cee3d..3eb0c82 100644
--- a/admin/js/main.js
+++ b/admin/js/main.js
@@ -383,7 +383,8 @@ angular.module('ffffngAdmin').config(function(NgAdminConfigurationProvider, Rest
}
return 'task-' + field + ' ' +
(task.values.enabled ? 'task-enabled' : 'task-disabled') + ' '
- + 'task-state-' + task.values.state;
+ + 'task-state-' + task.values.state + ' '
+ + 'task-result-' + (task.values.result ? task.values.result : 'none');
};
}
@@ -403,6 +404,7 @@ angular.module('ffffngAdmin').config(function(NgAdminConfigurationProvider, Rest
nga.field('description').cssClasses(taskClasses('description')),
nga.field('schedule').cssClasses(taskClasses('schedule')),
nga.field('state').cssClasses(taskClasses('state')),
+ nga.field('message').cssClasses(taskClasses('message')),
nga.field('runningSince').map(formatMoment).cssClasses(taskClasses('runningSince')),
nga.field('lastRunStarted').map(formatMoment).cssClasses(taskClasses('lastRunStarted')),
nga.field('lastRunDuration').map(formatDuration).cssClasses(taskClasses('lastRunDuration'))
diff --git a/publish.sh b/publish.sh
index 3b7a3c1..8a2e7bd 100755
--- a/publish.sh
+++ b/publish.sh
@@ -46,4 +46,3 @@ if confirm "Continue publishing?"; then
cd dist
npm publish
fi
-
diff --git a/server/jobs/FixNodeFilenamesJob.ts b/server/jobs/FixNodeFilenamesJob.ts
index f5d80fd..ce5e4c1 100644
--- a/server/jobs/FixNodeFilenamesJob.ts
+++ b/server/jobs/FixNodeFilenamesJob.ts
@@ -1,8 +1,12 @@
import {fixNodeFilenames} from "../services/nodeService";
+import {jobResultOkay} from "./scheduler";
export default {
name: 'FixNodeFilenamesJob',
description: 'Makes sure node files (holding fastd key, name, etc.) are correctly named.',
- run: fixNodeFilenames
+ async run() {
+ await fixNodeFilenames();
+ return jobResultOkay();
+ },
}
diff --git a/server/jobs/MailQueueJob.ts b/server/jobs/MailQueueJob.ts
index 4f4742f..f7405aa 100644
--- a/server/jobs/MailQueueJob.ts
+++ b/server/jobs/MailQueueJob.ts
@@ -1,8 +1,12 @@
import * as MailService from "../services/mailService"
+import {jobResultOkay} from "./scheduler";
export default {
name: 'MailQueueJob',
description: 'Send pending emails (up to 5 attempts in case of failures).',
- run: MailService.sendPendingMails,
-}
+ async run() {
+ await MailService.sendPendingMails();
+ return jobResultOkay();
+ },
+};
diff --git a/server/jobs/MonitoringMailsSendingJob.ts b/server/jobs/MonitoringMailsSendingJob.ts
index 5600372..02720e6 100644
--- a/server/jobs/MonitoringMailsSendingJob.ts
+++ b/server/jobs/MonitoringMailsSendingJob.ts
@@ -1,8 +1,12 @@
import * as MonitoringService from "../services/monitoringService";
+import {jobResultOkay} from "./scheduler";
export default {
name: 'MonitoringMailsSendingJob',
description: 'Sends monitoring emails depending on the monitoring state of nodes retrieved by the NodeInformationRetrievalJob.',
- run: MonitoringService.sendMonitoringMails,
+ async run() {
+ await MonitoringService.sendMonitoringMails();
+ return jobResultOkay();
+ },
};
diff --git a/server/jobs/NodeInformationRetrievalJob.ts b/server/jobs/NodeInformationRetrievalJob.ts
index beeaa8f..5e65661 100644
--- a/server/jobs/NodeInformationRetrievalJob.ts
+++ b/server/jobs/NodeInformationRetrievalJob.ts
@@ -1,8 +1,20 @@
import * as MonitoringService from "../services/monitoringService";
+import {jobResultOkay, jobResultWarning} from "./scheduler";
export default {
name: 'NodeInformationRetrievalJob',
description: 'Fetches the nodes.json and calculates and stores the monitoring / online status for registered nodes.',
- run: MonitoringService.retrieveNodeInformation,
+ async run () {
+ const result = await MonitoringService.retrieveNodeInformation();
+ if (result.failedParsingNodesCount > 0) {
+ return jobResultWarning(
+ `Warning: ${result.failedParsingNodesCount} of ${result.totalNodesCount} nodes could not be processed.`
+ );
+ } else {
+ return jobResultOkay(
+ `${result.totalNodesCount} nodes have been processed.`
+ );
+ }
+ },
};
diff --git a/server/jobs/OfflineNodesDeletionJob.ts b/server/jobs/OfflineNodesDeletionJob.ts
index ed800f6..9b39d89 100644
--- a/server/jobs/OfflineNodesDeletionJob.ts
+++ b/server/jobs/OfflineNodesDeletionJob.ts
@@ -1,8 +1,12 @@
import * as MonitoringService from "../services/monitoringService";
+import {jobResultOkay} from "./scheduler";
export default {
name: 'OfflineNodesDeletionJob',
description: 'Delete nodes that are offline for more than 100 days.',
- run: MonitoringService.deleteOfflineNodes,
+ async run() {
+ await MonitoringService.deleteOfflineNodes();
+ return jobResultOkay();
+ },
};
diff --git a/server/jobs/scheduler.ts b/server/jobs/scheduler.ts
index 1c29370..1ebe7e1 100644
--- a/server/jobs/scheduler.ts
+++ b/server/jobs/scheduler.ts
@@ -10,11 +10,35 @@ import NodeInformationRetrievalJob from "./NodeInformationRetrievalJob";
import MonitoringMailsSendingJob from "./MonitoringMailsSendingJob";
import OfflineNodesDeletionJob from "./OfflineNodesDeletionJob";
+export enum JobResultState {
+ OKAY = "okay",
+ WARNING = "warning",
+}
+
+export type JobResult = {
+ state: JobResultState,
+ message?: string,
+};
+
+export function jobResultOkay(message?: string): JobResult {
+ return {
+ state: JobResultState.OKAY,
+ message
+ }
+}
+
+export function jobResultWarning(message?: string): JobResult {
+ return {
+ state: JobResultState.WARNING,
+ message
+ }
+}
+
export interface Job {
name: string,
description: string,
- run(): Promise,
+ run(): Promise,
}
export enum TaskState {
@@ -34,6 +58,7 @@ export class Task {
public lastRunStarted: moment.Moment | null,
public lastRunDuration: number | null,
public state: TaskState,
+ public result: JobResult | null,
public enabled: boolean,
) {}
@@ -47,7 +72,7 @@ export class Task {
this.lastRunStarted = this.runningSince;
this.state = TaskState.RUNNING;
- const done = (state: TaskState):void => {
+ const done = (state: TaskState, result: JobResult | null): void => {
const now = moment();
const duration = now.diff(this.runningSince || now);
Logger.tag('jobs').profile('[%sms]\t%s', duration, this.name);
@@ -55,13 +80,14 @@ export class Task {
this.runningSince = null;
this.lastRunDuration = duration;
this.state = state;
+ this.result = result;
};
- this.job.run().then(() => {
- done(TaskState.IDLE);
+ this.job.run().then(result => {
+ done(TaskState.IDLE, result);
}).catch((err: any) => {
Logger.tag('jobs').error("Job %s failed: %s", this.name, err);
- done(TaskState.FAILED);
+ done(TaskState.FAILED, null);
});
}
}
@@ -92,7 +118,8 @@ function schedule(expr: string, job: Job): void {
null,
null,
TaskState.IDLE,
- true
+ null,
+ true,
);
cron.schedule(expr, task.run);
diff --git a/server/resources/taskResource.ts b/server/resources/taskResource.ts
index c90507f..4668129 100644
--- a/server/resources/taskResource.ts
+++ b/server/resources/taskResource.ts
@@ -3,11 +3,11 @@ import _ from "lodash";
import CONSTRAINTS from "../validation/constraints";
import ErrorTypes from "../utils/errorTypes";
import * as Resources from "../utils/resources";
-import {getTasks, Task} from "../jobs/scheduler";
+import {Entity} from "../utils/resources";
+import {getTasks, Task, TaskState} from "../jobs/scheduler";
import {normalizeString} from "../utils/strings";
import {forConstraint} from "../validation/validator";
import {Request, Response} from "express";
-import {Entity} from "../utils/resources";
const isValidId = forConstraint(CONSTRAINTS.id, false);
@@ -20,6 +20,8 @@ interface ExternalTask {
lastRunStarted: number | null,
lastRunDuration: number | null,
state: string,
+ result: string | null,
+ message: string | null,
enabled: boolean,
}
@@ -33,6 +35,8 @@ function toExternalTask(task: Task): ExternalTask {
lastRunStarted: task.lastRunStarted && task.lastRunStarted.unix(),
lastRunDuration: task.lastRunDuration || null,
state: task.state,
+ result: task.state !== TaskState.RUNNING && task.result ? task.result.state : null,
+ message:task.state !== TaskState.RUNNING && task.result ? task.result.message || null : null,
enabled: task.enabled
};
}
diff --git a/server/services/monitoringService.test.ts b/server/services/monitoringService.test.ts
index 8224b02..1edf951 100644
--- a/server/services/monitoringService.test.ts
+++ b/server/services/monitoringService.test.ts
@@ -389,6 +389,8 @@ test('parseNodesJson() should succeed parsing no nodes', () => {
// then
expect(result.importTimestamp.isValid()).toBe(true);
expect(result.nodes).toEqual([]);
+ expect(result.failedNodesCount).toEqual(0);
+ expect(result.totalNodesCount).toEqual(0);
});
test('parseNodesJson() should skip parsing invalid nodes', () => {
@@ -423,6 +425,8 @@ test('parseNodesJson() should skip parsing invalid nodes', () => {
// then
expect(result.importTimestamp.isValid()).toBe(true);
expect(result.nodes).toEqual([]);
+ expect(result.failedNodesCount).toEqual(2);
+ expect(result.totalNodesCount).toEqual(2);
expect(mockedLogger.getMessages('error', 'monitoring', 'parsing-nodes-json').length).toEqual(2);
});
@@ -467,5 +471,7 @@ test('parseNodesJson() should parse valid nodes', () => {
expect(result.importTimestamp.isValid()).toBe(true);
expect(result.nodes).toEqual([expectedParsedNode]);
+ expect(result.failedNodesCount).toEqual(1);
+ expect(result.totalNodesCount).toEqual(2);
expect(mockedLogger.getMessages('error', 'monitoring', 'parsing-nodes-json').length).toEqual(1);
});
diff --git a/server/services/monitoringService.ts b/server/services/monitoringService.ts
index bb3b48d..cf89a4b 100644
--- a/server/services/monitoringService.ts
+++ b/server/services/monitoringService.ts
@@ -45,8 +45,15 @@ export type ParsedNode = {
export type NodesParsingResult = {
importTimestamp: Moment,
nodes: ParsedNode[],
+ failedNodesCount: number,
+ totalNodesCount: number,
}
+export type RetrieveNodeInformationResult = {
+ failedParsingNodesCount: number,
+ totalNodesCount: number,
+};
+
let previousImportTimestamp: Moment | null = null;
async function insertNodeInformation(nodeData: ParsedNode, node: Node): Promise {
@@ -228,7 +235,9 @@ export function parseNodesJson (body: string): NodesParsingResult {
const result: NodesParsingResult = {
importTimestamp: parseTimestamp(json.timestamp),
- nodes: []
+ nodes: [],
+ failedNodesCount: 0,
+ totalNodesCount: 0,
};
if (!result.importTimestamp.isValid()) {
@@ -240,12 +249,14 @@ export function parseNodesJson (body: string): NodesParsingResult {
}
for (const nodeData of json.nodes) {
+ result.totalNodesCount += 1;
try {
const parsedNode = parseNode(result.importTimestamp, nodeData);
Logger.tag('monitoring', 'parsing-nodes-json').debug(`Parsing node successful: ${parsedNode.mac}`);
result.nodes.push(parsedNode);
}
catch (error) {
+ result.failedNodesCount += 1;
Logger.tag('monitoring', 'parsing-nodes-json').error("Could not parse node.", error, nodeData);
}
}
@@ -434,11 +445,15 @@ async function withUrlsData(urls: string[]): Promise {
return results;
}
-async function retrieveNodeInformationForUrls(urls: string[]): Promise {
+async function retrieveNodeInformationForUrls(urls: string[]): Promise {
const datas = await withUrlsData(urls);
let maxTimestamp = datas[0].importTimestamp;
let minTimestamp = maxTimestamp;
+
+ let failedParsingNodesCount = 0;
+ let totalNodesCount = 0;
+
for (const data of datas) {
if (data.importTimestamp.isAfter(maxTimestamp)) {
maxTimestamp = data.importTimestamp;
@@ -446,6 +461,9 @@ async function retrieveNodeInformationForUrls(urls: string[]): Promise {
if (data.importTimestamp.isBefore(minTimestamp)) {
minTimestamp = data.importTimestamp;
}
+
+ failedParsingNodesCount += data.failedNodesCount;
+ totalNodesCount += data.totalNodesCount;
}
if (previousImportTimestamp !== null && !maxTimestamp.isAfter(previousImportTimestamp)) {
@@ -456,7 +474,10 @@ async function retrieveNodeInformationForUrls(urls: string[]): Promise {
maxTimestamp.format(),
previousImportTimestamp.format()
);
- return;
+ return {
+ failedParsingNodesCount,
+ totalNodesCount,
+ };
}
previousImportTimestamp = maxTimestamp;
@@ -502,6 +523,11 @@ async function retrieveNodeInformationForUrls(urls: string[]): Promise {
minTimestamp.unix()
]
);
+
+ return {
+ failedParsingNodesCount,
+ totalNodesCount,
+ }
}
export async function getAll(restParams: RestParams): Promise<{total: number, monitoringStates: any[]}> {
@@ -605,7 +631,7 @@ export async function disable(token: string): Promise {
return newNode;
}
-export async function retrieveNodeInformation(): Promise {
+export async function retrieveNodeInformation(): Promise {
const urls = config.server.map.nodesJsonUrl;
if (_.isEmpty(urls)) {
throw new Error('No nodes.json-URLs set. Please adjust config.json: server.map.nodesJsonUrl')