diff --git a/libs/Task.js b/libs/Task.js index 94fac91..a0544f9 100644 --- a/libs/Task.js +++ b/libs/Task.js @@ -1,6 +1,7 @@ "use strict"; let assert = require('assert'); let fs = require('fs'); +let rmdir = require('rimraf'); let odmRunner = require('./odmRunner'); let statusCodes = require('./statusCodes'); @@ -13,9 +14,8 @@ module.exports = class Task{ this.uuid = uuid; this.name = name != "" ? name : "Task of " + (new Date()).toISOString(); this.dateCreated = new Date().getTime(); - this.status = { - code: statusCodes.QUEUED - }; + this.processingTime = -1; + this.setStatus(statusCodes.QUEUED); this.options = {}; this.output = []; this.runnerProcess = null; @@ -42,6 +42,11 @@ module.exports = class Task{ return `data/${this.uuid}`; } + // Deletes files and folders related to this task + cleanup(cb){ + rmdir(this.getProjectFolderPath(), cb); + } + setStatus(code, extra){ this.status = { code: code @@ -51,17 +56,53 @@ module.exports = class Task{ } } + updateProcessingTime(resetTime){ + this.processingTime = resetTime ? + -1 : + new Date().getTime() - this.dateCreated; + } + + startTrackingProcessingTime(){ + this.updateProcessingTime(); + if (!this._updateProcessingTimeInterval){ + this._updateProcessingTimeInterval = setInterval(() => { + this.updateProcessingTime(); + }, 1000); + } + } + + stopTrackingProcessingTime(resetTime){ + this.updateProcessingTime(resetTime); + if (this._updateProcessingTimeInterval){ + clearInterval(this._updateProcessingTimeInterval); + this._updateProcessingTimeInterval = null; + } + } + getStatus(){ return this.status.code; } + isCanceled(){ + return this.status.code === statusCodes.CANCELED; + } + // Cancels the current task (unless it's already canceled) cancel(cb){ if (this.status.code !== statusCodes.CANCELED){ + let wasRunning = this.status.code === statusCodes.RUNNING; this.setStatus(statusCodes.CANCELED); + + if (wasRunning && this.runnerProcess){ + // TODO: this does guarantee that + // the process will immediately terminate. + // In fact, often times ODM will continue running for a while + // This might need to be fixed on ODM's end. + this.runnerProcess.kill('SIGINT'); + this.runnerProcess = null; + } - console.log("Requested to cancel " + this.name); - // TODO + this.stopTrackingProcessingTime(true); cb(null); }else{ cb(new Error("Task already cancelled")); @@ -72,6 +113,7 @@ module.exports = class Task{ // This will spawn a new process. start(done){ if (this.status.code === statusCodes.QUEUED){ + this.startTrackingProcessingTime(); this.setStatus(statusCodes.RUNNING); this.runnerProcess = odmRunner.run({ projectPath: `${__dirname}/../${this.getProjectFolderPath()}` @@ -79,12 +121,16 @@ module.exports = class Task{ if (err){ this.setStatus(statusCodes.FAILED, {errorMessage: `Could not start process (${err.message})`}); }else{ - if (code === 0){ - this.setStatus(statusCodes.COMPLETED); - }else{ - this.setStatus(statusCodes.FAILED, {errorMessage: `Process exited with code ${code}`}); + // Don't evaluate if we caused the process to exit via SIGINT? + if (this.status.code !== statusCodes.CANCELED){ + if (code === 0){ + this.setStatus(statusCodes.COMPLETED); + }else{ + this.setStatus(statusCodes.FAILED, {errorMessage: `Process exited with code ${code}`}); + } } } + this.stopTrackingProcessingTime(); done(); }, output => { // Replace console colors @@ -103,10 +149,9 @@ module.exports = class Task{ restart(cb){ if (this.status.code === statusCodes.CANCELED || this.status.code === statusCodes.FAILED){ this.setStatus(statusCodes.QUEUED); - - console.log("Requested to restart " + this.name); - // TODO - + this.dateCreated = new Date().getTime(); + this.output = []; + this.stopTrackingProcessingTime(true); cb(null); }else{ cb(new Error("Task cannot be restarted")); @@ -119,6 +164,7 @@ module.exports = class Task{ uuid: this.uuid, name: this.name, dateCreated: this.dateCreated, + processingTime: this.processingTime, status: this.status, options: this.options, imagesCount: this.images.length diff --git a/libs/TaskManager.js b/libs/TaskManager.js index 28e771f..02a0493 100644 --- a/libs/TaskManager.js +++ b/libs/TaskManager.js @@ -64,10 +64,15 @@ module.exports = class TaskManager{ cancel(uuid, cb){ let task; if (task = this.find(uuid, cb)){ - task.cancel(err => { - this.removeFromRunningQueue(task); - cb(err); - }); + if (!task.isCanceled()){ + task.cancel(err => { + this.removeFromRunningQueue(task); + this.processNextTask(); + cb(err); + }); + }else{ + cb(null); // Nothing to be done + } } } @@ -76,9 +81,16 @@ module.exports = class TaskManager{ remove(uuid, cb){ this.cancel(uuid, err => { if (!err){ - delete(this.tasks[uuid]); - // TODO: other cleanup - cb(null); + let task; + if (task = this.find(uuid, cb)){ + task.cleanup(err => { + if (!err){ + delete(this.tasks[uuid]); + this.processNextTask(); + cb(null); + }else cb(err); + }); + }else; // cb is called by find on error }else cb(err); }); } @@ -89,7 +101,7 @@ module.exports = class TaskManager{ let task; if (task = this.find(uuid, cb)){ task.restart(err => { - this.processNextTask(); + if (!err) this.processNextTask(); cb(err); }); } diff --git a/package.json b/package.json index 8306055..17663f0 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,8 @@ "express": "^4.14.0", "morgan": "^1.7.0", "multer": "^1.1.0", - "node-uuid": "^1.4.7" + "node-uuid": "^1.4.7", + "rimraf": "^2.5.3" }, "devDependencies": { "nodemon": "^1.9.2" diff --git a/public/css/main.css b/public/css/main.css index 72dcc94..8b5d502 100644 --- a/public/css/main.css +++ b/public/css/main.css @@ -42,7 +42,7 @@ } @keyframes pulsateNegative { 0% {background-color: #fff;} - 50% {background-color: lightred;} + 50% {background-color: pink;} 100% {background-color: #fff;} } diff --git a/public/index.html b/public/index.html index 42d10a7..1497ea5 100644 --- a/public/index.html +++ b/public/index.html @@ -64,19 +64,24 @@
Name:
Images:
Status:
+
Time Elapsed:
Output: ViewHide
- - + +
diff --git a/public/js/main.js b/public/js/main.js index f7e8596..33575b4 100644 --- a/public/js/main.js +++ b/public/js/main.js @@ -1,4 +1,22 @@ $(function(){ + function hoursMinutesSecs(t){ + var ch = 60 * 60 * 1000, + cm = 60 * 1000, + h = Math.floor(t / ch), + m = Math.floor( (t - h * ch) / cm), + s = Math.round( (t - h * ch - m * cm) / 1000), + pad = function(n){ return n < 10 ? '0' + n : n; }; + if( s === 60 ){ + h++; + s = 0; + } + if( m === 60 ){ + h++; + m = 0; + } + return [pad(h), pad(m), pad(s)].join(':'); + } + function TaskList(){ var uuids = JSON.parse(localStorage.getItem("odmTaskList") || "[]"); if (Object.prototype.toString.call(uuids) !== "[object Array]") uuids = []; @@ -31,8 +49,17 @@ $(function(){ this.loading = ko.observable(true); this.info = ko.observable({}); this.viewingOutput = ko.observable(false); + this.output = ko.observableArray(); this.resetOutput(); + this.timeElapsed = ko.observable("00:00:00"); + var codes = { + QUEUED: 10, + RUNNING: 20, + FAILED: 30, + COMPLETED: 40, + CANCELED: 50 + }; var statusCodes = { 10: { descr: "Queued", @@ -40,7 +67,7 @@ $(function(){ }, 20: { descr: "Running", - icon: "glyphicon-refresh spinning" + icon: "glyphicon-cog spinning" }, 30: { descr: "Failed", @@ -70,8 +97,17 @@ $(function(){ }else return "glyphicon-question-sign"; }else return ""; }, this); - this.canceled = ko.pureComputed(function(){ - return this.info().status && this.info().status.code === 50; + this.showCancel = ko.pureComputed(function(){ + return this.info().status && + (this.info().status.code === codes.QUEUED || this.info().status.code === codes.RUNNING); + }, this); + this.showRestart = ko.pureComputed(function(){ + return this.info().status && + (this.info().status.code === codes.CANCELED); + }, this); + this.showRemove = ko.pureComputed(function(){ + return this.info().status && + (this.info().status.code === codes.FAILED || this.info().status.code === codes.COMPLETED || this.info().status.code === codes.CANCELED); }, this); this.startRefreshingInfo(); @@ -80,7 +116,14 @@ $(function(){ var self = this; var url = "/task/" + this.uuid + "/info"; $.get(url) - .done(self.info) + .done(function(json){ + // Track time + + if (json.processingTime && json.processingTime !== -1){ + self.timeElapsed(hoursMinutesSecs(json.processingTime)); + } + self.info(json); + }) .fail(function(){ self.info({error: url + " is unreachable."}); }) @@ -91,7 +134,7 @@ $(function(){ Task.prototype.resetOutput = function(){ this.viewOutputLine = 0; this.autoScrollOutput = true; - this.output = ko.observableArray(); + this.output.removeAll(); }; Task.prototype.viewOutput = function(){ var self = this; @@ -130,7 +173,7 @@ $(function(){ this.refreshInfo(); this.refreshInterval = setInterval(function(){ self.refreshInfo(); - }, 2000); + }, 500); // TODO: change to larger value }; Task.prototype.stopRefreshingInfo = function() { if (this.refreshInterval){ @@ -160,7 +203,7 @@ $(function(){ }); }; - function genApiCall(url){ + function genApiCall(url, onSuccess){ return function(){ var self = this; @@ -169,6 +212,7 @@ $(function(){ }) .done(function(json){ if (json.success){ + if (onSuccess !== undefined) onSuccess(self, json); self.startRefreshingInfo(); }else{ self.stopRefreshingInfo(); @@ -182,7 +226,9 @@ $(function(){ } }; Task.prototype.cancel = genApiCall("/task/cancel"); - Task.prototype.restart = genApiCall("/task/restart"); + Task.prototype.restart = genApiCall("/task/restart", function(task){ + task.resetOutput(); + }); var taskList = new TaskList(); ko.applyBindings(taskList);