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";
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

Wyświetl plik

@ -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);
});
}

Wyświetl plik

@ -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"

Wyświetl plik

@ -42,7 +42,7 @@
}
@keyframes pulsateNegative {
0% {background-color: #fff;}
50% {background-color: lightred;}
50% {background-color: pink;}
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>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>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>
<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>
<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
</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
</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>

Wyświetl plik

@ -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);