Task cancel, remove, time elapsed

pull/1/head
Piero Toffanin 2016-07-14 16:42:12 -05:00
rodzic 48e292a6cf
commit 2b3411dadc
6 zmienionych plików z 143 dodań i 33 usunięć

Wyświetl plik

@ -1,6 +1,7 @@
"use strict"; "use strict";
let assert = require('assert'); let assert = require('assert');
let fs = require('fs'); let fs = require('fs');
let rmdir = require('rimraf');
let odmRunner = require('./odmRunner'); let odmRunner = require('./odmRunner');
let statusCodes = require('./statusCodes'); let statusCodes = require('./statusCodes');
@ -13,9 +14,8 @@ module.exports = class Task{
this.uuid = uuid; this.uuid = uuid;
this.name = name != "" ? name : "Task of " + (new Date()).toISOString(); this.name = name != "" ? name : "Task of " + (new Date()).toISOString();
this.dateCreated = new Date().getTime(); this.dateCreated = new Date().getTime();
this.status = { this.processingTime = -1;
code: statusCodes.QUEUED this.setStatus(statusCodes.QUEUED);
};
this.options = {}; this.options = {};
this.output = []; this.output = [];
this.runnerProcess = null; this.runnerProcess = null;
@ -42,6 +42,11 @@ module.exports = class Task{
return `data/${this.uuid}`; return `data/${this.uuid}`;
} }
// Deletes files and folders related to this task
cleanup(cb){
rmdir(this.getProjectFolderPath(), cb);
}
setStatus(code, extra){ setStatus(code, extra){
this.status = { this.status = {
code: code 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(){ getStatus(){
return this.status.code; return this.status.code;
} }
isCanceled(){
return this.status.code === statusCodes.CANCELED;
}
// Cancels the current task (unless it's already canceled) // Cancels the current task (unless it's already canceled)
cancel(cb){ cancel(cb){
if (this.status.code !== statusCodes.CANCELED){ if (this.status.code !== statusCodes.CANCELED){
let wasRunning = this.status.code === statusCodes.RUNNING;
this.setStatus(statusCodes.CANCELED); 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); this.stopTrackingProcessingTime(true);
// TODO
cb(null); cb(null);
}else{ }else{
cb(new Error("Task already cancelled")); cb(new Error("Task already cancelled"));
@ -72,6 +113,7 @@ module.exports = class Task{
// This will spawn a new process. // This will spawn a new process.
start(done){ start(done){
if (this.status.code === statusCodes.QUEUED){ if (this.status.code === statusCodes.QUEUED){
this.startTrackingProcessingTime();
this.setStatus(statusCodes.RUNNING); this.setStatus(statusCodes.RUNNING);
this.runnerProcess = odmRunner.run({ this.runnerProcess = odmRunner.run({
projectPath: `${__dirname}/../${this.getProjectFolderPath()}` projectPath: `${__dirname}/../${this.getProjectFolderPath()}`
@ -79,12 +121,16 @@ module.exports = class Task{
if (err){ if (err){
this.setStatus(statusCodes.FAILED, {errorMessage: `Could not start process (${err.message})`}); this.setStatus(statusCodes.FAILED, {errorMessage: `Could not start process (${err.message})`});
}else{ }else{
if (code === 0){ // Don't evaluate if we caused the process to exit via SIGINT?
this.setStatus(statusCodes.COMPLETED); if (this.status.code !== statusCodes.CANCELED){
}else{ if (code === 0){
this.setStatus(statusCodes.FAILED, {errorMessage: `Process exited with code ${code}`}); this.setStatus(statusCodes.COMPLETED);
}else{
this.setStatus(statusCodes.FAILED, {errorMessage: `Process exited with code ${code}`});
}
} }
} }
this.stopTrackingProcessingTime();
done(); done();
}, output => { }, output => {
// Replace console colors // Replace console colors
@ -103,10 +149,9 @@ module.exports = class Task{
restart(cb){ restart(cb){
if (this.status.code === statusCodes.CANCELED || this.status.code === statusCodes.FAILED){ if (this.status.code === statusCodes.CANCELED || this.status.code === statusCodes.FAILED){
this.setStatus(statusCodes.QUEUED); this.setStatus(statusCodes.QUEUED);
this.dateCreated = new Date().getTime();
console.log("Requested to restart " + this.name); this.output = [];
// TODO this.stopTrackingProcessingTime(true);
cb(null); cb(null);
}else{ }else{
cb(new Error("Task cannot be restarted")); cb(new Error("Task cannot be restarted"));
@ -119,6 +164,7 @@ module.exports = class Task{
uuid: this.uuid, uuid: this.uuid,
name: this.name, name: this.name,
dateCreated: this.dateCreated, dateCreated: this.dateCreated,
processingTime: this.processingTime,
status: this.status, status: this.status,
options: this.options, options: this.options,
imagesCount: this.images.length imagesCount: this.images.length

Wyświetl plik

@ -64,10 +64,15 @@ module.exports = class TaskManager{
cancel(uuid, cb){ cancel(uuid, cb){
let task; let task;
if (task = this.find(uuid, cb)){ if (task = this.find(uuid, cb)){
task.cancel(err => { if (!task.isCanceled()){
this.removeFromRunningQueue(task); task.cancel(err => {
cb(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){ remove(uuid, cb){
this.cancel(uuid, err => { this.cancel(uuid, err => {
if (!err){ if (!err){
delete(this.tasks[uuid]); let task;
// TODO: other cleanup if (task = this.find(uuid, cb)){
cb(null); 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); }else cb(err);
}); });
} }
@ -89,7 +101,7 @@ module.exports = class TaskManager{
let task; let task;
if (task = this.find(uuid, cb)){ if (task = this.find(uuid, cb)){
task.restart(err => { task.restart(err => {
this.processNextTask(); if (!err) this.processNextTask();
cb(err); cb(err);
}); });
} }

Wyświetl plik

@ -25,7 +25,8 @@
"express": "^4.14.0", "express": "^4.14.0",
"morgan": "^1.7.0", "morgan": "^1.7.0",
"multer": "^1.1.0", "multer": "^1.1.0",
"node-uuid": "^1.4.7" "node-uuid": "^1.4.7",
"rimraf": "^2.5.3"
}, },
"devDependencies": { "devDependencies": {
"nodemon": "^1.9.2" "nodemon": "^1.9.2"

Wyświetl plik

@ -42,7 +42,7 @@
} }
@keyframes pulsateNegative { @keyframes pulsateNegative {
0% {background-color: #fff;} 0% {background-color: #fff;}
50% {background-color: lightred;} 50% {background-color: pink;}
100% {background-color: #fff;} 100% {background-color: #fff;}
} }

Wyświetl plik

@ -64,19 +64,24 @@
<div class="taskItem"><strong>Name:</strong> <span data-bind="text: info().name"></span></div> <div class="taskItem"><strong>Name:</strong> <span data-bind="text: info().name"></span></div>
<div class="taskItem"><strong>Images:</strong> <span data-bind="text: info().imagesCount"></span></div> <div class="taskItem"><strong>Images:</strong> <span data-bind="text: info().imagesCount"></span></div>
<div class="taskItem"><strong>Status:</strong> <span data-bind="text: statusDescr()"></span></div> <div class="taskItem"><strong>Status:</strong> <span data-bind="text: statusDescr()"></span></div>
<div class="taskItem"><strong>Time Elapsed:</strong> <span data-bind="text: timeElapsed()"></span></div>
<div class="taskItem"><strong>Output:</strong> <a href="javascript:void(0);" data-bind="click: viewOutput, visible: !viewingOutput()">View</a><a href="javascript:void(0);" data-bind="click: hideOutput, visible: viewingOutput()">Hide</a></div> <div class="taskItem"><strong>Output:</strong> <a href="javascript:void(0);" data-bind="click: viewOutput, visible: !viewingOutput()">View</a><a href="javascript:void(0);" data-bind="click: hideOutput, visible: viewingOutput()">Hide</a></div>
<textarea class="consoleOutput" data-bind="value: output().join(''), visible: viewingOutput(), event: {mouseover: consoleMouseOver, mouseout: consoleMouseOut}, attr: {id: 'console_' + uuid}"></textarea> <textarea class="consoleOutput" data-bind="value: output().join(''), visible: viewingOutput(), event: {mouseover: consoleMouseOver, mouseout: consoleMouseOut}, attr: {id: 'console_' + uuid}"></textarea>
<span data-bind="css: 'statusIcon glyphicon ' + icon()"></span> <span data-bind="css: 'statusIcon glyphicon ' + icon()"></span>
<div class="actionButtons"> <div class="actionButtons">
<button data-bind="click: cancel, visible: !canceled()" type="button" class="btn btn-primary btn-sm" > <button data-bind="click: cancel, visible: showCancel()" type="button" class="btn btn-primary btn-sm" >
<span class="glyphicon glyphicon-remove-circle"></span> Cancel <span class="glyphicon glyphicon-remove-circle"></span> Cancel
</button> </button>
<button data-bind="click: restart, visible: canceled()" type="button" class="btn btn-primary btn-sm" > <button data-bind="click: restart, visible: showRestart()" type="button" class="btn btn-primary btn-sm" >
<span class="glyphicon glyphicon-play"></span> Restart <span class="glyphicon glyphicon-play"></span> Restart
</button> </button>
<button data-bind="click: remove, visible: showRemove()" type="button" class="btn btn-primary btn-sm">
<span class="glyphicon glyphicon-remove-circle"></span> Remove
</button>
</div> </div>
</div> </div>

Wyświetl plik

@ -1,4 +1,22 @@
$(function(){ $(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(){ function TaskList(){
var uuids = JSON.parse(localStorage.getItem("odmTaskList") || "[]"); var uuids = JSON.parse(localStorage.getItem("odmTaskList") || "[]");
if (Object.prototype.toString.call(uuids) !== "[object Array]") uuids = []; if (Object.prototype.toString.call(uuids) !== "[object Array]") uuids = [];
@ -31,8 +49,17 @@ $(function(){
this.loading = ko.observable(true); this.loading = ko.observable(true);
this.info = ko.observable({}); this.info = ko.observable({});
this.viewingOutput = ko.observable(false); this.viewingOutput = ko.observable(false);
this.output = ko.observableArray();
this.resetOutput(); this.resetOutput();
this.timeElapsed = ko.observable("00:00:00");
var codes = {
QUEUED: 10,
RUNNING: 20,
FAILED: 30,
COMPLETED: 40,
CANCELED: 50
};
var statusCodes = { var statusCodes = {
10: { 10: {
descr: "Queued", descr: "Queued",
@ -40,7 +67,7 @@ $(function(){
}, },
20: { 20: {
descr: "Running", descr: "Running",
icon: "glyphicon-refresh spinning" icon: "glyphicon-cog spinning"
}, },
30: { 30: {
descr: "Failed", descr: "Failed",
@ -70,8 +97,17 @@ $(function(){
}else return "glyphicon-question-sign"; }else return "glyphicon-question-sign";
}else return ""; }else return "";
}, this); }, this);
this.canceled = ko.pureComputed(function(){ this.showCancel = ko.pureComputed(function(){
return this.info().status && this.info().status.code === 50; 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);
this.startRefreshingInfo(); this.startRefreshingInfo();
@ -80,7 +116,14 @@ $(function(){
var self = this; var self = this;
var url = "/task/" + this.uuid + "/info"; var url = "/task/" + this.uuid + "/info";
$.get(url) $.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(){ .fail(function(){
self.info({error: url + " is unreachable."}); self.info({error: url + " is unreachable."});
}) })
@ -91,7 +134,7 @@ $(function(){
Task.prototype.resetOutput = function(){ Task.prototype.resetOutput = function(){
this.viewOutputLine = 0; this.viewOutputLine = 0;
this.autoScrollOutput = true; this.autoScrollOutput = true;
this.output = ko.observableArray(); this.output.removeAll();
}; };
Task.prototype.viewOutput = function(){ Task.prototype.viewOutput = function(){
var self = this; var self = this;
@ -130,7 +173,7 @@ $(function(){
this.refreshInfo(); this.refreshInfo();
this.refreshInterval = setInterval(function(){ this.refreshInterval = setInterval(function(){
self.refreshInfo(); self.refreshInfo();
}, 2000); }, 500); // TODO: change to larger value
}; };
Task.prototype.stopRefreshingInfo = function() { Task.prototype.stopRefreshingInfo = function() {
if (this.refreshInterval){ if (this.refreshInterval){
@ -160,7 +203,7 @@ $(function(){
}); });
}; };
function genApiCall(url){ function genApiCall(url, onSuccess){
return function(){ return function(){
var self = this; var self = this;
@ -169,6 +212,7 @@ $(function(){
}) })
.done(function(json){ .done(function(json){
if (json.success){ if (json.success){
if (onSuccess !== undefined) onSuccess(self, json);
self.startRefreshingInfo(); self.startRefreshingInfo();
}else{ }else{
self.stopRefreshingInfo(); self.stopRefreshingInfo();
@ -182,7 +226,9 @@ $(function(){
} }
}; };
Task.prototype.cancel = genApiCall("/task/cancel"); 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(); var taskList = new TaskList();
ko.applyBindings(taskList); ko.applyBindings(taskList);