500 lines
15 KiB
JavaScript
500 lines
15 KiB
JavaScript
/**
|
|
** ==============================
|
|
** O O O OOOO
|
|
** O O O O O O
|
|
** O O O O O O
|
|
** OOOO OOOO O OOO OOOO
|
|
** O O O O O O O
|
|
** O O O O O O O
|
|
** OOOO OOOO O O OOOO
|
|
** ==============================
|
|
** Dr. Stefan Bosse http://www.bsslab.de
|
|
**
|
|
** COPYRIGHT: THIS SOFTWARE, EXECUTABLE AND SOURCE CODE IS OWNED
|
|
** BY THE AUTHOR(S).
|
|
** THIS SOURCE CODE MAY NOT BE COPIED, EXTRACTED,
|
|
** MODIFIED, OR OTHERWISE USED IN A CONTEXT
|
|
** OUTSIDE OF THE SOFTWARE SYSTEM.
|
|
**
|
|
** $AUTHORS: Stefan Bosse
|
|
** $INITIAL: (C) 2006-2016 bLAB
|
|
** $CREATED: sbosse on 23-5-16.
|
|
** $VERSION: 1.3.5
|
|
**
|
|
** $INFO:
|
|
*
|
|
* Boot Service that is a monolithic application embeddeding all serviced programs.
|
|
*
|
|
** $ENDOFINFO
|
|
*/
|
|
var Io = Require('com/io');
|
|
var Comp = Require('com/compat');
|
|
var Sch = Require('dos/scheduler');
|
|
var Net = Require('dos/network');
|
|
var Conn = Require('dos/connection');
|
|
|
|
var My = {domain:'default',host:'none'};
|
|
|
|
var Operation = {
|
|
CLEAR:'clear',
|
|
DISABLE:'disable',
|
|
ENABLE:'enable',
|
|
EXECUTE:'exec',
|
|
INFO:'info',
|
|
INIT:'init',
|
|
MAKEAPP:'makeapp',
|
|
REFRESH:'refresh',
|
|
START:'start',
|
|
STOP:'stop'
|
|
}
|
|
|
|
var State = {
|
|
OFFLINE:'offline',
|
|
ONLINE:'online',
|
|
UNINITIALIZED:'uninitialized',
|
|
MAINTENANCE:'maintenance',
|
|
DISABLED:'disabled',
|
|
TEST:'test',
|
|
WAIT:'wait'
|
|
}
|
|
|
|
var options = {
|
|
app : '*',
|
|
args : [],
|
|
bip:'localhost',
|
|
boot_ops : [],
|
|
bootst : 'boot.state',
|
|
bootsf : 'boot.conf',
|
|
bport:3001,
|
|
broker: true,
|
|
dip : 'localhost',
|
|
http:false,
|
|
keepalive:true,
|
|
monitor:0,
|
|
prog : process.argv[1]||'?',
|
|
tcpnet:1,
|
|
verbose:0,
|
|
vm : process.argv[0]||'node'
|
|
}
|
|
|
|
Comp.args.parse(process.argv,[
|
|
[[
|
|
Operation.CLEAR,
|
|
Operation.DISABLE,
|
|
Operation.ENABLE,
|
|
Operation.INIT,
|
|
Operation.INFO,
|
|
Operation.MAKEAPP,
|
|
Operation.REFRESH,
|
|
Operation.START,
|
|
Operation.STOP
|
|
],
|
|
0,function (val) {
|
|
options.boot_ops.push(val)
|
|
}],
|
|
[Operation.EXECUTE,'*',function (vals) {
|
|
options.boot_ops.push(Operation.EXECUTE);
|
|
options.app=Comp.array.head(vals);
|
|
options.args=Comp.array.tail(vals);
|
|
}],
|
|
['-conf',1,function (val) {options.bootsf=val}],
|
|
['-state',1,function (val) {options.bootst=val}],
|
|
['-broker',1,function(val){
|
|
var tokens = Comp.string.split(':',val);
|
|
if (tokens.length==1)
|
|
options.bip=val;
|
|
else {
|
|
options.bip=tokens[0];
|
|
options.bport=Perv.int_of_string(tokens[1])
|
|
}
|
|
}],
|
|
['-dip',1,function(val){options.dip=val}],
|
|
['-D',1,function(val){options.dports.push(Perv.int_of_string(val))}],
|
|
['-L',2,function(val1,val2){options.links.push([Perv.int_of_string(val1),getip(val2),getipport(val2)])}],
|
|
['-nokeepalive',0,function(val){options.keepalive=false;}],
|
|
['-monitor',0,function() {options.monitor++;}],
|
|
[['-v','-verbose'],0,function() {options.verbose++;}],
|
|
['-T',0,function(val){options.tcpnet=1;options.http=false;}],
|
|
['-T2',0,function(val){options.tcpnet=2;options.http=false;}],
|
|
['-H',0,function(val){options.http=true;options.tcpnet=0;}],
|
|
function (val) {
|
|
options.app=val;
|
|
}
|
|
],2);
|
|
|
|
var verbose = !Comp.array.contains(options.boot_ops,Operation.EXECUTE);
|
|
var makeapp = Comp.array.contains(options.boot_ops,Operation.MAKEAPP);
|
|
var refresh = Comp.array.contains(options.boot_ops,Operation.REFRESH);
|
|
var start = Comp.array.contains(options.boot_ops,Operation.START);
|
|
options.privhostport = Net.uniqport();
|
|
options.pubhostport = Net.prv2pub(options.privhostport);
|
|
|
|
// console.log(process.argv)
|
|
|
|
if (Comp.array.empty(options.boot_ops)) {
|
|
Io.out('usage: boot [-verbose -v -monitor -H -T -T2 -nokeepalive] ');
|
|
Io.out(' [clear disable enable init makeapp refresh(reconf) start stop]');
|
|
Io.out(' [-conf <pathtoconffile>] (default '+options.bootsf+')');
|
|
Io.out(' [-state <pathtostatefile>] (default '+options.bootst+')');
|
|
Io.out(' [<service>]');
|
|
Io.out(' [exec <app> args...]');
|
|
process.exit(-1);
|
|
} else if (Comp.array.contains(options.boot_ops,Operation.EXECUTE)) {
|
|
Io.out('[BOOT] Executing '+options.app+' '+Comp.printf.list(options.args,_,' ')+' ..');
|
|
process.argv=Comp.array.concat(['node',options.app],options.args);
|
|
Require(options.app)
|
|
return;
|
|
}
|
|
|
|
/** Main watchdog service loop.
|
|
* 1. Poll servers and records status
|
|
* 2. Stops or restarts services
|
|
*
|
|
*/
|
|
var watchdog = function (serv) {
|
|
var self=this;
|
|
|
|
this.serv = serv;
|
|
this.cap = null;
|
|
this.down = false;
|
|
this.dying = false;
|
|
this.delay = serv.polltime||1000; // Poll delay
|
|
this.maxtimeout = 15000; // Maximal time between a successfull repsonse and failures
|
|
this.maxfailed = 5; // Maximal number of failures before state is changed to maintenance
|
|
this.maxrestart = 3; // Maximal number of failed restarts
|
|
|
|
this.init = function () {
|
|
Io.out('[BOOT] Polling service '+serv.app+' with PID='+serv.process.pid+' with '+serv.pollcap);
|
|
serv.status = Net.Status.STD_UNKNOWN;
|
|
serv.timestamp = Io.time();
|
|
serv.restart = 0;
|
|
serv.failed = 0;
|
|
if (Io.exists(serv.pollcap)) {
|
|
this.cap = Net.cap_of_file(serv.pollcap);
|
|
if (this.cap) {
|
|
Io.out('[BOOT] Using poll capability '+Net.Print.capability(this.cap)+' for '+serv.app);
|
|
serv.status = Net.Status.STD_OK;
|
|
} else Io.out('[BOOT] Invalid capability in '+serv.pollcap);
|
|
} else Io.out('[BOOT] Not found: '+serv.pollcap);
|
|
};
|
|
|
|
this.poll = function () {
|
|
var thr=this;
|
|
if (!this.cap) return;
|
|
switch (serv.state) {
|
|
case State.ONLINE:
|
|
case State.MAINTENANCE:
|
|
//print('pollcap: '+Net.Print.capability(this.cap));
|
|
Services.network.std.std_info(this.cap, function (stat) {
|
|
//print('... pollcap: '+Net.Print.capability(self.cap));
|
|
serv.status=stat;
|
|
if (serv.status==Net.Status.STD_OK) serv.timestamp=Io.time();
|
|
else {
|
|
serv.failed++;
|
|
Io.out('[BOOT] Polling failed for '+serv.app+': '+Net.Print.status(serv.status));
|
|
}
|
|
});
|
|
break;
|
|
}
|
|
};
|
|
|
|
this.service = function () {
|
|
var state=serv.state,t;
|
|
switch (serv.state) {
|
|
case State.ONLINE:
|
|
if ((Io.time()-serv.timestamp) > this.maxtimeout || serv.failed > this.maxfailed) {
|
|
serv.state=State.MAINTENANCE;
|
|
Io.out('[BOOT] '+serv.app+': '+state+' -> '+serv.state);
|
|
}
|
|
break;
|
|
case State.MAINTENANCE:
|
|
Io.out('[BOOT] Stopping '+serv.app);
|
|
serv.stop();
|
|
serv.state=State.OFFLINE;
|
|
this.cap=undefined;
|
|
break;
|
|
case State.OFFLINE:
|
|
if (serv.restart<this.maxrestart && serv.check()) {
|
|
Io.out('[BOOT] Starting '+serv.app);
|
|
serv.restart++;
|
|
serv.start();
|
|
Io.out('[BOOT] '+serv.app+': '+state+' -> '+serv.state);
|
|
serv.timestamp = Io.time();
|
|
serv.failed = 0;
|
|
} else if (serv.restart>=this.maxrestart) {
|
|
serv.state=State.DISABLED;
|
|
Io.out('[BOOT] '+serv.app+' too often failed: '+state+' -> '+serv.state);
|
|
}
|
|
break;
|
|
}
|
|
serv.status = Net.Status.STD_UNKNOWN;
|
|
};
|
|
|
|
this.sleep = function () {
|
|
Delay(this.delay);
|
|
if (!this.cap && serv.state==State.ONLINE && Io.exists(serv.pollcap)) {
|
|
this.cap = Net.cap_of_file(serv.pollcap);
|
|
if (this.cap) {
|
|
Io.out('[BOOT] Using poll capability '+Net.Print.capability(this.cap)+' for '+serv.app);
|
|
serv.status = Net.Status.STD_OK;
|
|
} else Io.out('[BOOT] Invalid capability in '+serv.pollcap);
|
|
};
|
|
};
|
|
this.terminate = function () {};
|
|
|
|
this.transitions = [
|
|
[undefined,this.init,function () {return serv.state==State.ONLINE}],
|
|
[this.init,this.sleep],
|
|
[this.poll,this.service, function (thr) {
|
|
return serv.status == Net.Status.RPC_FAILURE }],
|
|
[this.poll,this.sleep, function (thr) {
|
|
return serv.status != Net.Status.RPC_FAILURE }],
|
|
[this.service,this.sleep],
|
|
[this.sleep,this.poll, function () {
|
|
return !this.dying && serv.state!=State.OFFLINE}],
|
|
[this.sleep,this.service, function () {
|
|
return !this.dying && serv.state==State.OFFLINE}],
|
|
[this.sleep,this.terminate, function () {return this.dying}]
|
|
];
|
|
this.on = {
|
|
|
|
};
|
|
}
|
|
|
|
// console.log(boot_ops)
|
|
|
|
/***************
|
|
** SERVICES
|
|
***************/
|
|
|
|
var services = function (options) {
|
|
this.options=options;
|
|
this.services=[];
|
|
};
|
|
|
|
services.prototype.add = function (options) {
|
|
if (!options.trans || !options.app)
|
|
Io.fail('[BOOT] Invalid service object '+Io.inspect(options));
|
|
var serv=Service(options);
|
|
this.services.push(serv);
|
|
serv.services=this;
|
|
Io.out('[BOOT] Adding service '+serv.info());
|
|
}
|
|
|
|
|
|
services.prototype.exec = function (op) {
|
|
Comp.array.iter(this.services,function (s) {
|
|
var state=s.state,t,ready;
|
|
if ((Comp.string.contains(s.app,options.app) || options.app=='*')) {
|
|
switch (op) {
|
|
case Operation.INFO:
|
|
Io.out('[BOOT] '+s.app+' State='+s.state);
|
|
break;
|
|
case Operation.MAKEAPP:
|
|
s.state=State.TEST;
|
|
Io.out('[BOOT] '+s.app+' doing '+op);
|
|
t=s.start();
|
|
if (t && t.wait) Io.sleep(t.wait);
|
|
s.state=State.UNINITIALIZED;
|
|
break;
|
|
case Operation.INIT:
|
|
case Operation.START:
|
|
if (s.state==State.ONLINE) return;
|
|
if (op==Operation.START && s.state==State.MAINTENANCE) return;
|
|
if (op==Operation.INIT && s.state!=State.UNINITIALIZED) return;
|
|
// if (s.state==State.DISABLED) state=s.state=State.OFFLINE;
|
|
ready=s.check();
|
|
if (ready) {
|
|
Io.out('[BOOT] '+s.app+' doing '+op);
|
|
s.start();
|
|
} else {
|
|
Io.out('[BOOT] Warning: '+s.app+' has missing dependencie(s), cannot start!');
|
|
s.state==State.OFFLINE;
|
|
}
|
|
break;
|
|
case Operation.STOP:
|
|
case Operation.DISABLE:
|
|
if (s.state==State.ONLINE) {
|
|
Io.out('[BOOT] Stopping '+s.app);
|
|
s.stop();
|
|
}
|
|
if (op==Operation.DISABLE) {
|
|
s.state=State.DISABLED;
|
|
Io.out('[BOOT] '+s.app+' State '+state+' -> '+s.state);
|
|
}
|
|
break;
|
|
case Operation.ENABLE:
|
|
s.state=State.OFFLINE;
|
|
Io.out('[BOOT] '+s.app+' State '+state+' -> '+s.state);
|
|
break;
|
|
case Operation.CLEAR:
|
|
if (s.state==State.ONLINE) s.state=State.OFFLINE;
|
|
if (s.state==State.OFFLINE) s.state=State.UNINITIALIZED;
|
|
Io.out('[BOOT] '+s.app+' State '+state+' -> '+s.state);
|
|
break;
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
services.prototype.find = function (app) {
|
|
return Comp.array.find(this.services, function (s) {
|
|
return s.app==app;
|
|
});
|
|
}
|
|
|
|
services.prototype.read = function (file) {
|
|
try {
|
|
Io.out('[BOOT] Reading service file '+file+' ..');
|
|
var text=Io.read_file(file);
|
|
if (text==undefined) throw 'No such file';
|
|
services=eval(text);
|
|
if (Comp.obj.isArray(services))
|
|
Comp.array.iter(services,this.add.bind(this));
|
|
} catch (e) {
|
|
Io.out('[BOOT] Reading or compiling of configuration file '+file+' failed: '+e);
|
|
return;
|
|
}
|
|
};
|
|
|
|
/***************
|
|
** SERVICE
|
|
***************/
|
|
var service = function (options) {
|
|
this.state=options.state||State.UNINITIALIZED;
|
|
this.trans=options.trans;
|
|
this.app=options.app;
|
|
this.depends=options.depends;
|
|
this.poll=options.poll||true;
|
|
this.pollcap=options.pollcap;
|
|
this.polltime=options.polltime;
|
|
}
|
|
|
|
|
|
var Service = function (options) {
|
|
return new service(options);
|
|
}
|
|
|
|
/** Check dependencies. Return true if service is ready to be scheduled.
|
|
*
|
|
*/
|
|
service.prototype.check = function () {
|
|
var self=this,state=this.state,ready=true,S=this.services;
|
|
|
|
if (!this.depends) return true;
|
|
Comp.array.iter(this.depends,function (dep) {
|
|
var serv=S.find(dep);
|
|
if (serv) {
|
|
ready=ready && (serv.state==State.ONLINE);
|
|
} else {
|
|
Io.out('[BOOT] Warning: Service '+self.app+' has unknwon dependency: '+dep);
|
|
ready=false;
|
|
}
|
|
});
|
|
return ready;
|
|
}
|
|
|
|
service.prototype.info = function () {
|
|
return this.app+': '+this.state;
|
|
}
|
|
|
|
/** Start a service. Searches a matching transition based on current state.
|
|
*/
|
|
service.prototype.start = function () {
|
|
var self=this,_args=process.argv,trans;
|
|
Comp.array.iter_break(this.trans, function (t) {
|
|
var exit=false;
|
|
if (Comp.array.contains(t.states,self.state)) {
|
|
exit=true;
|
|
trans=t;
|
|
}
|
|
return exit;
|
|
});
|
|
if (!trans) return;
|
|
if (Comp.array.contains(options.boot_ops,Operation.MAKEAPP)) {
|
|
Io.out('[BOOT] Compiling '+this.app+' '+Comp.printf.list(trans.args,_,' ')+' ..');
|
|
process.argv=Comp.array.concat(['node',this.app],trans.args);
|
|
this.process=Require(this.app);
|
|
process.argv=_args;
|
|
} else {
|
|
this.process=Io.fork(options.prog,Comp.array.concat(['exec',this.app],trans.args));
|
|
Io.out('[BOOT] '+this.process.pid+': Started '+this.app+' '+Comp.printf.list(trans.args,_,' ')+' ..');
|
|
switch (this.state) {
|
|
case State.OFFLINE:
|
|
case State.UNINITIALIZED:
|
|
if (trans.wait) Io.sleep(trans.wait);
|
|
this.state=State.ONLINE; break;
|
|
}
|
|
}
|
|
}
|
|
|
|
service.prototype.stop = function () {
|
|
if (this.process) {
|
|
Io.out('[BOOT] '+this.process.pid+': Stopping '+this.app+' ..');
|
|
this.process.kill();
|
|
}
|
|
this.process=null;
|
|
this.state=State.OFFLINE;
|
|
}
|
|
|
|
services.prototype.watchdog = function () {
|
|
this.network = Conn.setup(options);
|
|
this.network.init(this.network.start.bind(this));
|
|
Comp.array.iter(this.services,function (s) {
|
|
if (s.state != State.TEST && s.pollcap) {
|
|
Io.out('[BOOT] Adding watchdog service for '+s.app);
|
|
Sch.NewTask('WATCHDDOG '+s.app,watchdog,s);
|
|
}
|
|
});
|
|
}
|
|
|
|
services.prototype.write = function (file) {
|
|
var services=[],
|
|
text;
|
|
Io.out('[BOOT] Writing service file '+file+' ..');
|
|
Comp.array.iter(this.services,function (s) {
|
|
var sc={app:s.app,trans:s.trans,state:s.state};
|
|
if (s.poll) sc.poll=s.poll;
|
|
if (s.pollcap) sc.pollcap=s.pollcap;
|
|
if (s.polltime) sc.pollcap=s.polltime;
|
|
if (s.depends) sc.depends=s.depends;
|
|
services.push(sc);
|
|
});
|
|
text=JSON.stringify(services);
|
|
Io.write_file(file,text);
|
|
}
|
|
|
|
My.host=Io.hostname();
|
|
|
|
var Services = new services({});
|
|
var scheduler = Sch.TaskScheduler();
|
|
scheduler.Init();
|
|
|
|
if (makeapp || refresh || !Io.exists(options.bootst))
|
|
Services.read(options.bootsf);
|
|
else
|
|
Services.read(options.bootst);
|
|
|
|
|
|
process.on('SIGINT', function () {
|
|
Io.out('Got SIGINT ..');
|
|
Io.out('[BOOT] Stopping all services ..');
|
|
Services.exec(Operation.STOP);
|
|
Services.write(options.bootst);
|
|
process.exit(2);
|
|
});
|
|
|
|
for (var cmd in options.boot_ops) {
|
|
Services.exec(options.boot_ops[cmd]);
|
|
}
|
|
|
|
Services.write(options.bootst);
|
|
|
|
if (start) {
|
|
Services.watchdog();
|
|
scheduler.Run();
|
|
} else if (!makeapp) {
|
|
Io.out('Exiting ..');
|
|
process.exit(0);
|
|
} else Io.out('Waiting ..');
|