var os    = require('os');
var fs    = require('fs');
var path  = require('path');
var spawn = require('child_process').spawn;
var process = require('process');

// REF: https://nodejs.org/docs/latest-v12.x/api/child_process.html#child_process_class_childprocess

class compose {
    static magicDirReplace( word, valIfEqual ) {
        if ([null, '.','cwd','{cwd}'].indexOf(word?word:null)>-1) word = process.cwd();
        else if (['{__dirname}', '{composedir}'].indexOf(word?word:null)>-1) word = __dirname;
        else if (([null, '='].indexOf(word?word:null)>-1)&&(typeof valIfEqual !== 'undefined')) word = valIfEqual;
        return word;
    }
    constructor(  config) {
        // ---2023-01-18 BEGIN commented out new block ..service starts fail in module-usecase
        // this.services = { };   // out: states/state (and 'working memory'), by service id (short name). There is actually child-process as subprocess in this.
        // this.config = config || { };  // in: config, contains incoming service definitions (input) in this.config.services
        // this.tracestate = { pathchecked: false};
        // this.running_count = 0;
        // this.config.root    = compose.magicDirReplace( this.config.root);
        // this.config.cwdroot = compose.magicDirReplace( this.config.cwdroot, this.config.root);

        // if (( config.preprocessed == null) || (config.preprocessed == false)) {
            // this.config = compose.preProcessServices( config);
        // }
        // this.config.verbose = this.config.verbose || false;
        // // fs.writeFileSync("compose_start_config_dump.json", JSON.stringify( this.config, null, 2));
        // this.uplink_cb = null; // in: uplink cb, for live notifications
        // if ( this.config.verbose === true) console.log("compose: constructor. Config has " + this.config.services.length + " services defined");
        // ---2023-01-18 END comment out new block above, restore old block below

        this.running_count = 0;
        this.services = { };   // out: states/state (and 'working memory'), by service id (short name). There is actually child-process as subprocess in this.
        this.config = config || { };  // in: config, contains incoming service definitions (input) in this.config.services
        this.tracestate = { pathchecked: false};

        this.all_spawned_ts = null; // 2024-11-06 Not yet all spawned to make a start-attempt (i.e. no big bang attempt yet)
        this.all_started_ok_seen = false; // 2024-11-06 new to prevent multi-notification about the same thing
        this.first_seen_all_spawned_0s = false; // 2024-05-08 detection of application services spawned for the first time
        this.first_all_ready_2s_tmr=null; // 2024-05-08 timer for all_ready_2s check
        this.first_all_ready_2s = false; // 2024-05-08 detection of application ready for the first time
        this.when_all_seen_running_ts = null; // 2024-11-06 latest time when all seen running. This is reset if some service fails (exits)
        this.max_all_ready_tmo_ms = ( (config != null) && config.max_all_ready_tmo_ms) ? config.max_all_ready_tmo_ms : 13000; // 2024-11-07
        this.boot_interval_tmr = null; // 2024-11-06 granular timer to allow optimized boot with [min..X..max]

        this.require_all_stable_ms =  (( (config != null) && config.all_ready_tmo_ms) ? config.all_ready_tmo_ms : 3000); // 2024-05-08 exact  ms
        if (( config!= null) &&(config.all_stable_for_2s_ms != null)) this.require_all_stable_ms = config.require_all_stable_ms;

        this.exit_if_not_all_ready = (( (config != null) && config.exit_if_not_all_ready) ? config.exit_if_not_all_ready : false);

        if ((this.config.root == null)||(this.config.root == '.')) {
            this.config.root = process.cwd();
        }
        if ((this.config.root == '{__dirname}')||(this.config.root == '{composedir}')) {
            this.config.root = __dirname;
        }
        if ((this.config.cwdroot == null) || ( this.config.cwdroot == '=') ){
            this.config.cwdroot =this.config.root;
        }
        if ((this.config.cwdroot == '{__dirname}')||(this.config.cwdroot == '{composedir}')) {
            this.config.cwdroot = __dirname;
        }

        if (( config.preprocessed == null) || (config.preprocessed == false)) {
            this.config = compose.preProcessServices( config);
        }
        this.config.verbose = this.config.verbose || false;
        // fs.writeFileSync("compose_start_config_dump.json", JSON.stringify( this.config, null, 2));
        this.uplink_cb = null; // in: uplink cb, for live notifications
        if ( this.config.verbose) console.log("compose: constructor. Config has " + this.config.services.length + " services defined");
    }
    GetTs(opts) { // {dt, utc, inflate, fmt} - "Swiss-time-knife" all-in-one function for getting and opt. formatting timestamps to text
        // REF: day of year: https://stackoverflow.com/questions/8619879/javascript-calculate-the-day-of-the-year-1-366
        var tnow = (opts.dt) ? new Date(opts.dt) : new Date();
        var ts = { tz:'#NA', tnow: tnow }; // about 'tz': slow query/OS-dependent, so it is optional w/ requesting it by { gettz:true}
        const zeroPad = (num,width) => (num.toString().padStart(width, "0"));
        ts.year  = opts.utc? tnow.getUTCFullYear() : tnow.getFullYear();
        ts.month = opts.utc?(tnow.getUTCMonth()+1) :(tnow.getMonth()+1);
        ts.day   = opts.utc? tnow.getUTCDate()     : tnow.getDate(); // day of month
        ts.hour  = opts.utc? tnow.getUTCHours()    : tnow.getHours();
        ts.min   = opts.utc? tnow.getUTCMinutes()  : tnow.getMinutes();
        ts.sec   = opts.utc? tnow.getUTCSeconds()  : tnow.getSeconds();
        ts.msec  = opts.utc? tnow.getUTCMilliseconds(): tnow.getMilliseconds();
        ts.wkdi  = opts.utc? tnow.getUTCDay()      : tnow.getDay(); //0=sun,1mon,6=sat
        ts.dayNr = ts.wkdi; // just compatibility-alias (day of week 0=sun,1mon,6=sat)
        ts.wkdn=['Sunday','Monday','Tuesday','Wednesday','Thursday','Friday','Saturday'][ts.wkdi];
        ts.wkd2=ts.wkdn.slice(0,2); ts.wkd3=ts.wkdn.slice(0,3);
        if (opts.gettz == true) ts.tz = (Intl? Intl.DateTimeFormat().resolvedOptions().timeZone : '');
        if ( opts.inflate || opts.fmt != null) ['month','day','hour','min','sec','msec'].forEach( tu => { ts[tu] = zeroPad(ts[tu], ((tu!='msec')?2:3)); } );
        if ( opts.fmt =='fullms') return  ts.year +'-'+ts.month+'-'+ts.day+' '+ts.hour+':'+ts.min+':'+ts.sec+'.'+ts.msec;
        if ( opts.fmt != null) { // YYYY MM DD HH mm SS MS  (!) only 'mm' is special, i.e. MM=month, mm=minutes.
            var tmp=opts.fmt;var dt='YYYY-MM-DD';var tm='HH:mm:SS';
            var quicks={year:'YYYY',full: dt+' '+tm,fullms: dt+' '+tm+'.MS',date:dt, time:tm,'time-of-day':tm,timems:tm+'.MS'};
            Object.keys(quicks).forEach((okey)=> { if (tmp==okey) tmp=quicks[okey]; });
            var fieldmap= {YYYY:'year',MM:'month',DD:'day',HH:'hour',mm:'min',SS:'sec',MS:'msec',TZ:'tz',WKDI:'wkdi',WKDN:'wkdn',WKD2:'wkd2',WKD3:'wkd3'};
            Object.keys(fieldmap).forEach((k) => { var loops=0;while ((tmp.indexOf(k)>-1)&&(loops<16)) {loops++;tmp=tmp.replace(k, ts[ fieldmap[k] ]); } });
            return tmp;
        }
        return ts;
    }
    log( logLine) {
        var ts = new Date();
        if (this.config.tracefile == null) return;
        var current_date = this.GetTs({ dt: ts, utc: true, inflate: true, fmt: 'date' });// time.GetDateUTC(ts);
        if ((this.tracestate.this_day != current_date)||(this.tracestate.pathchecked == false)||( this.tracestate.fileName == null)) {
            this.tracestate.this_day = current_date;
            var fileName = compose.pathForRunningOS( this.config, this._replaceTextDefs(compose.pathForRunningOS( this.config, this.config.tracefile)), null,'applyspecials' );
            if (fs.existsSync(path.dirname(fileName)) == false) fs.mkdirSync( path.dirname(fileName), { recursive: true });
            var t = this.GetTs({ dt: ts, utc: true, inflate: true }); // time.GetUTCTimeAsStruct( true );
            fileName = fileName.replace("#YYYY", t.year).replace("#yyyy", t.year).replace("#MM", t.month)
                       .replace("#mm", t.month).replace("#DD", t.day).replace("#dd", t.day);
            if (fs.existsSync(fileName) && fileName.indexOf(".log") == fileName.length - 4) {
                var index = 1;
                var tmpFileName;
                do { tmpFileName = fileName.substring(0, fileName.length - 4) + "_" + index + ".log";
                } while (fs.existsSync(tmpFileName) && ++index < 10);
                fileName = tmpFileName;
            }
            this.tracestate.fileName = fileName;
            this.tracestate.pathchecked = true;
        }
        fs.appendFile(this.tracestate.fileName, this.GetTs({dt: ts, utc: true, inflate: true, fmt:'fullms'}) + ' ' + logLine + "\r\n", function (err) { });
    }
    // (!) preProcessServices MUST HAPPEN before constructor is called via this static method.
    // the return value composeConfig must be provided to the constructor as config
    static preProcessServices( composeConfig, /*optional options (of compose main level and opt cmdline)*/config)  {
        // populate missing service fields from inherited/referenced images
        composeConfig.services = composeConfig.services.map( (svc,idx) => ( svc.img_id? Object.assign( { }, composeConfig.img_id[ svc.img_id], svc) : svc ));
        // 2024-12-07 this map below adds support for "svc.addparams:[]" array, that can add (but not replace) extra parameers to the base image's parameters
        // composeConfig.services = composeConfig.services.map((svc, idx) => {
        //     var new_svc = svc;
        //     if (svc.addparams != null) {
        //         new_svc = JSON.parse(JSON.stringify(svc));
        //         new_svc.params = new_svc.params.concat(svc.addparams);
        //         delete new_svc.addparams;
        //     }
        //     return new_svc;
        // });
        composeConfig.services = composeConfig.services.flatMap((svc, idx) => {
            var new_svcs = [svc];

            // helper function to make actual replacements
            function __mkOptReplaces(str, i) {
                function _replacer( str, search, replacement) {
                    if (str.indexOf(search)<-1) return str; // do nothing if search not found
                    return str.split(search).join(replacement);
                }
                if (typeof str !== 'string') return str;
                const replacements = {
                    '{iid}': i.toString(),
                    '{iid0}': i.toString(),
                    '{iid0-2}': i.toString().padStart(2, '0'),
                    '{iid1}': (i + 1).toString(),
                    '{iid1-2}': (i + 1).toString().padStart(2, '0'),
                };
                Object.entries(replacements).forEach(([key, value]) => {
                    str = _replacer(str, key, value);
                });
                return str;
            }

            if (svc.insts) {
                new_svcs = [];
                for(var inst=0; inst < svc.insts;inst++) {
                    var _tInst = JSON.parse(JSON.stringify(svc));// create new instance of a template
                    Object.entries(_tInst).forEach(([key, value]) => {
                        _tInst[key] = Array.isArray(value)
                            ? value.map(item => __mkOptReplaces(item, inst)) // Map over array elements
                            : __mkOptReplaces(value, inst); // Handle non-array values
                    });
                    new_svcs.push( _tInst);
                }
            }
            new_svcs.forEach( (_svc, idx) => {
                if (_svc.addparams != null) {
                    var _new_svc = JSON.parse(JSON.stringify(_svc));
                    new_svcs[idx].params = _new_svc.params.concat(_new_svc.addparams);
                    delete new_svcs[idx].addparams;
                }
            });
            return new_svcs;
        });
        //console.dir(composeConfig.services);process.exit(0);
        composeConfig.preprocessed = true; // mark preprocessed
        var show_selected = ((config != null) && (config.selected == true));

        if (( config && (config.verbose===true)) || (composeConfig.verbose ===true) || (show_selected == true)) {
            console.log("");console.log("Preprocessed full list: " + composeConfig.services.length + " (intermediate)");
            composeConfig.services.forEach( (svc, idx ) => {
                console.log( idx.toString().padStart(2,' ') + '  ' + JSON.stringify( svc) );
        });}
        // handle optional "startonly --start <csv_ids> filtering i.e. only start specified subset of services by svc.id
        if ( config && config.start) {
            var newList = composeConfig.services.filter( svcdef => (config.start.split(',').map(ss=>ss.trim()).indexOf(svcdef.id)>-1) );
            composeConfig.services = newList;
        }
        // handle optional "nostart"  --nostart <csv_ids> i.e. skip starting specified subset of services by svc.id
        if (config && config.nostart) {
            config.nostart.split(',').forEach( (nostart_svc) => {
                composeConfig.services = composeConfig.services.filter( svcdef => ( svcdef.id != nostart_svc.trim() ) );
        });}
        // handle optional --select [csv_selectors]    <->    svc {  ..., sels|selectors: [csv_selectors], ... }
        // if  svc.selectors HAS one of the --select selectors, then it will be part of the result process group
        var selectors = config? (config.select || config.sel || config.use || config.run || composeConfig.default_launch_selectors) : composeConfig.default_launch_selectors;
        if (selectors) {
            var sels = selectors.split(',');
            composeConfig.services = composeConfig.services.filter( (svc) => {
                       var svc_sels = svc.selectors || svc.sels || svc.uses || svc.use;
                       if (svc_sels == null) return false;
                       if ((svc.run != null) && (svc.run == 0)) return false;
                       var svs_selarr = svc_sels.split(',');
                       var has_selector = false;
                       sels.forEach( (sel) => { if (svs_selarr.indexOf(sel)>-1) has_selector = true; } );
                       return has_selector;
            });
        }
        if ((config && (config.verbose ===true)) || (composeConfig.verbose ===true) || (show_selected == true)) {
            console.log(""); console.log("Selector-etc-filtered list :" +  composeConfig.services.length + " (final)");
            composeConfig.services.forEach( (svc, idx ) => { console.log( idx.toString().padStart(2,' ') + '  ' + JSON.stringify( svc) );});
        }
        if ((composeConfig.services.length == 0)||(show_selected)) {
            console.log("Nothing to start, making exit.");
            process.exit(0);
        }
        // (support leader def on commandline too:) for those that are started, mark that there is a leader dependency
        if (config && config.leader) {
            composeConfig.services.forEach( ( svc) => { if (svc.id == this.config.leader) svc.dep = 'leader'; } );
        }
        return composeConfig;
    }
    replaceAll( str, search, replacement) {
        return str.split(search).join(replacement);
    }
    static getComposeDataFilePathTo( config, fname ) {
        return config.composecwd ? path.join( compose.pathForRunningOS( config, config.composecwd), compose.pathForRunningOS( config, fname) )
                  : compose.pathForRunningOS(config, fname);
    }
    static pathForRunningOS( config, filepath, specific_sep, optioncmd ) {
        var sep =  (specific_sep != null) ? specific_sep : path.sep;
        if (typeof filepath === "string") {
            if (( filepath.indexOf('\\') > -1) || ( filepath.indexOf('/') > -1)) {
                filepath = (sep == '/') ? filepath.split('\\').join('/') : filepath.split('/').join('\\');
            }
            if (typeof optioncmd !== 'undefined') {
                switch( optioncmd) {
                    case 'applyspecialscwd':
                    case 'applyspecials':
                        if (filepath == '.') {
                        } else if ( filepath.indexOf('{__dirname}') == 0) {
                            filepath = path.join( __dirname, filepath.substring(11));
                        } else if ( filepath.indexOf('{cr}') == 0) { // coderoot --root
                            filepath = path.join( config.root, filepath.substring(4));
                        } else if ( filepath.indexOf('{root}') == 0) { // coderoot --root
                            filepath = path.join( config.root, filepath.substring(6));
                        } else if ( filepath.indexOf('{dr}') == 0) { // dataroot --cwdroot
                            filepath = path.join( config.cwdroot, filepath.substring(4));
                        } else if ( filepath.indexOf('{cwdroot}') == 0) {
                            filepath = path.join( config.cwdroot, filepath.substring(9));
                        } else if ( [".\\","./","."+path.sep].indexOf(filepath.substring(0,2))>-1) {
                            filepath = ( optioncmd != 'applyspecialscwd') ? path.join( config.root, filepath.substring(2)) :
                                    path.join( config.cwdroot, filepath.substring(2));
                        }
                        break;
                    default: break;
                }
            }
        }
        return filepath;
    }
    _replaceTextDefs( text_in, svc) {
        var text = text_in;
        if ( this.config.useropts != null) { // // support smart useropts "variables" with dual ifdef ifundef values
            this.config.useropts.forEach( (useropt) => {
                var useropt_str = '{' + useropt.id + '}';
                if (text.indexOf( useropt_str) > -1) {
                    // commandline or param refers to useropt. Primary: ENV, secondary: CMDLINE, and last: Default
                    if (( process.env[useropt.id]) && ( process.env[useropt.id].length > 0) ) {
                        text = this.replaceAll( text, useropt_str, this.replaceAll(useropt.template, "{val}", process.env[useropt.id]));
                    } else if (this.config[useropt.id] != null) {
                        text = this.replaceAll( text, useropt_str, this.replaceAll(useropt.template, "{val}", this.config[useropt.id]));
                    } else {
                        text = this.replaceAll( text, useropt_str, (useropt.default == null ? "" : useropt.default) );
                    }
                }
            });
        }
        // process optional def(inition)s-section
        if ( this.config.defs) {
            Object.keys(this.config.defs).forEach( (defkey) => {
                text = this.replaceAll( text, '{'+defkey+'}', this.config.defs[defkey]);
            });
        }
        // opt passparams for the childprocess (only one set of params possible to pass, to 1-n child processes)
        text =  this.config.passparams ? this.replaceAll(text, '{passparams}', this.config.passparams) :  this.replaceAll(text, '{passparams}', '');
        // replace dynamic id
        if ( svc) {
            text = this.replaceAll(text, "{id}", svc.id);
            if (svc.img_id) {
                text = this.replaceAll(text, "{img_id}", svc.img_id);
                text = this.replaceAll(text, "{imgid}", this.replaceAll(svc.img_id,'_', ''));
            } else {
                text = this.replaceAll(text, "{img_id}", '');
                text = this.replaceAll(text, "{imgid}", '');
            }
        }
        return text;
    }
    optAdjustPath(  item, specialstype) {
        if ( ( (( item.indexOf('/') >-1) || (item.indexOf('\\') > -1)|| (item == '.'))
              && (item.indexOf("://") <0) && (item.indexOf('/') != 0) ) || ((item.indexOf('{')>-1)&&(item.indexOf('}')>-1)) ) {
            item = compose.pathForRunningOS(this.config, item, null, (specialstype? specialstype: "applyspecials") );
        }
        return item;
    }
    restartCore( svc_id) {
        var serv = this.services[svc_id];  // this.services[svc_id].svc_obj.dep = 'leader'
        if (( serv) && (serv.startParams)) {
            this.runCommand(
                serv.svc_obj.svc_obj,
                serv.startParams.svc_id, serv.startParams.is_shell, serv.startParams.batchcmd,
                serv.startParams.params, serv.startParams.cwd, serv.startParams.options_in,
                serv.startParams.start_cb, serv.startParams.stop_cb);
        }
    }
    runCommand( svc_obj, svc_id, is_shell, batchcmd, params, cwd, options_in, start_cb, stop_cb) {
        if ( this.services[svc_id] == null) { // Keep start options saved for easy restart with restartCore(-)
            this.services[svc_id] = {
                pid: 0, svc_obj: svc_obj,
                startParams: { svc_id: svc_id, is_shell: is_shell, batchcmd: batchcmd,
                    params: params, cwd: cwd, options_in: options_in, start_cb: start_cb, stop_cb: stop_cb}
            }
        }
        if (this.config.verbose === true) console.log("RUNCOMMAND( id=" + svc_id +", is_shell=" + is_shell + ", batchcmd=" + batchcmd
                        + ", params=" + (typeof params == 'string'? params :  ('['+params.join(',')+']')) // 20230106 Params can be array (!)
                        + ", cwd=" + cwd +  " @ " + process.cwd());
        var  _arrparams = [ ];
        var  _shadowparams = [];
        if ((os.type().toLowerCase().indexOf("windows") > -1) && ( is_shell == true) ) {
            _shadowparams.push( '/c');
            _arrparams.push( '/c' );
        }
        _shadowparams.push( batchcmd);
        _arrparams.push( this. optAdjustPath( batchcmd, 'applyspecials') );

        var appCb = this.uplink_cb;
        if (( params != null) && (params != "")) {
            var _arr = (( typeof params !== 'string') ? params : params.split(' ')); // 20230106 supp. input as params[]
            _arr.forEach( (param) => {
                _shadowparams.push( param);
                _arrparams.push(  this. optAdjustPath( param, 'applyspecialscwd') );
            });
        }

        var useComSpec = process.env.comspec;
        var child = null;
        var options = { stdio: ['ignore','ignore','ignore'] }; // stdin, stdout, stderr =>  ignore, pipe, pipe
        Object.assign(  options, options_in);
        if (this.config.verbose === true)  console.log("Merged options: " +  JSON.stringify( options ));
        if (cwd != null) {
            options.cwd = /*cwd;*/ this.optAdjustPath( cwd, null, "applyspecialscwd" );
        }
        if (this.config.showonly == 'relative') {
            console.log();
            console.log( ' ' + svc_id + ' ! ==> ' +  _shadowparams[0] + ' ' + _shadowparams.slice(1).join(' '));
            console.log( ' ' + svc_id + ' cwd-> ' + ( options.cwd ? cwd : '.') );
            appCb('child_exit', svc_id, 0);
            return;
        }

        if (this.config.verbose === true) console.log("RUNCOMMAND params after postprocessing: " + _arrparams.join(' '));
        if (this.config.showonly == true) {
            console.log();
            console.log( ' ' + svc_id + ' ! ==> ' +  _arrparams[0] + ' ' + _arrparams.slice(1).join(' '));
            console.log( ' ' + svc_id + ' cwd->  ' + ( options.cwd ? options.cwd : '.') );
            appCb('child_exit', svc_id, 0);
            return;
        }
        this.log("starting " + svc_id + " 1/3 comp-cwd  : " + process.cwd() + " is_shell: " + is_shell);
        this.log("starting " + svc_id + " 2/3 child-cwd : " + options.cwd);
        // this.log("starting " + svc_id + " 3/4 s-params  : " + _shadowparams.join(' '));
        this.log("starting " + svc_id + " 3/3 cli-params: " + _arrparams.join(' '));
        if ((['start','medium'].indexOf(this.config.verbose)>-1)||(this.config.monitor === true)) {
            console.log("[compose] =======STARTING service '" + svc_id + "'");
            console.log("[compose] " + _shadowparams.join(' '));
        }
        try {
            this.running_count++;
            if (os.type().toLowerCase() == "linux") {
                options.shell = is_shell;
                if (this.config.verbose === true) console.log("Execute_3a_of3: " + _arrparams[0] + _arrparams.slice(1));
                child = spawn(_arrparams[0], _arrparams.slice(1), options);
            } else {
                if (!is_shell) {
                    if (this.config.verbose === true) console.log("Execute_3B_of_3(normal): " + _arrparams[0] + " " + _arrparams.slice(1));
                    child = spawn(_arrparams[0], _arrparams.slice(1), options );
                } else {
                    if (this.config.verbose === true) console.log("Execute_3C_of_3(shell): " + _arrparams[1] + " " + _arrparams.slice(2));
                    child = spawn(useComSpec, _arrparams, options );
                }
            }
            if ( child == null) {
                if (this.config.verbose === true) console.log("ERROR: child process start failed for " + svc_id);
                this.services[svc_id] = { ...this.services[svc_id], ...{ pid:0, id: svc_id, subprocess: child, running: false, ts_start:null }}; // 2024-05-08 ts_start
                appCb('child_start', svc_id, "error");
                start_cb(err, { id: svc_id, "cb_reason": "start", "code": 1 });
            } else {
                //this.services[svc_id] = { pid: child.pid, id: svc_id, subprocess: child, running: true}; // SUCCESS STARTING
                this.services[svc_id] = { ...this.services[svc_id], ...{ pid: child.pid, id: svc_id, subprocess: child, running: true, ts_start: new Date()}}; // 2024-05-08 ts_start Date
                start_cb(null, { id: svc_id, "cb_reason": "start" });
                appCb('child_start', svc_id, "ok");
                child.__ber_svc_id = svc_id;
            }
        } catch(err) {
            console.error("ERROR: launching external command " + svc_id + " using spawn failed with " + err);
            child = null;  // this.services[svc_id] = {  pid:0, id: svc_id, subprocess: child, running: false };
            if (this.running_count> 0) this.running_count--;
            this.services[svc_id] = { ...this.services[svc_id], ...{  pid:0, id: svc_id, subprocess: child, running: false, ts_start:null }};
            start_cb(err, { pid: 0, id: svc_id, "cb_reason": "exit", "code": 1 });
        }
        if ( child != null) {
            var that = this;
            if (['start','medium'].indexOf(this.config.verbose)>-1) console.log("[compose] Service '" + svc_id + "' started");
            this.services[svc_id].pid = child.pid;
            if ( options.stdio[1] != 'ignore') child.stdout.on('data', function( data ) {
                    appCb('child_stdout', child.__ber_svc_id, data);
                    // if (this.config.verbose === true) console.log(child.__ber_svc_id + ": " + data)
                });
            if ( options.stdio[2] != 'ignore')  {
                child.stderr.on('data', function( data ) {
                    that.log("Compose registers STDERR data from " + child.__ber_svc_id);
                    that.log( data.toString() );
                    console.log(data.toString());
                    appCb('child_stderr', child.__ber_svc_id, data);
                    //if (this.config.verbose === true) console.log(child.__ber_svc_id + "-stderr: " + data.toString())
                });
            }
            child.on('error',       ( err )  => {
                   console.log(child.__ber_svc_id + ":.on(error): " + err);
                   that.log("ERROR: ", child.__ber_svc_id + ":.on(error): " + err);//2024-01-22 added
                   });
            child.on('exit', ( code ) => {
                    if (this.running_count> 0) this.running_count--;
                    if (['start','medium'].indexOf(this.config.verbose)>-1) console.log("[compose] Service '" + svc_id + "' stopped");
                    appCb('child_exit', child.__ber_svc_id, code);
                    //console.log(child.__ber_svc_id + ":.on(exit): code=" + code)
                    if (this.config.verbose === true) console.log("CHILD EXIT SEEN (" + this.running_count + " running)");
                    that.log("Compose registers PROCESS EXIT for " + child.__ber_svc_id + " with exit code: " + code);
                    var leader = false;
                    // console log..find service info and if this is dep:"leader", shut down all...
                    if ( this.services == null) {
                        console.error("child.on(exit) :  this.services == null for " + child.__ber_svc_id);
                    } else if (( this.services) && ( Object.keys(this.services).length > 0) && ( this.services[child.__ber_svc_id] )) {
                        var _svc = this.services[child.__ber_svc_id];
                        var serv_obj = _svc.svc_obj;
                        _svc.running = false;// 2024-05-08 mark stopped already at this layer (also in stop_cb in higher level, if asgd)
                        _svc.ts_start = null;// 2024-05-08 mark stopped already at this layer (also in stop_cb in higher level, if asgd)
                        if (this.config.verbose === true) console.log("EXIT CHILD INFO:" +  JSON.stringify( serv_obj));
                        if ( serv_obj.dep == 'leader') {
                            leader = true; // Leader made exit, so stop all
                            if (this.config.verbose === true) console.log("LEADER EXIT SEEN!");
                        }
                    }
                    if ( typeof stop_cb == 'function') {
                        stop_cb(null, { id: child.__ber_svc_id, is_leader: leader, cb_reason: "exit", code: code });  // OK default
                    }
                });
        }
    }
    start() {
        return this.startServices();
    }
    _serviceFinished( svc, err, val) {
        if ((this.config.showonly == true)||(this.config.showonly == 'relative')) {
            return;
        }
        if ( this.config.verbose === true) console.log( svc.id + " service finished with err, val:  " + err, val);
        if ( this.uplink_cb) {
            this.uplink_cb( 'child_stop', '', "compose: child process finished " + svc.id + "..");
        }
        if (  val.is_leader ) {
            var tmr = setTimeout( ( ) => {
                clearTimeout(tmr);
                if ( this.config.verbose === true) console.log("LEADER EXIT SEEN -> stopping ALL other services");
                this.stopServices();
                var tmr = setTimeout( ( ) => { clearTimeout(tmr); process.exit(0); }, 100);
            }, 100);
        }
    }
    // 2024-11-07 EFIX added condition to all_ok that count also matches the this.config.services.length
    _getSimpleCountStatus( ) {
        var _status = {
                running: Object.keys(this.services).filter( sk => this.services[sk].running == true).length,
                total: Object.keys(this.services).length,
                all_ok: false
            };
        _status.all_ok = (_status.running == _status.total) && (_status.running == this.config.services.length);
        return _status;
    }
    // 2024-11-06 New as a separate function
    _controlServiceStackStart( callSource ) {
        // if ((this.first_all_ready_0s == false) && (this._getSimpleCountStatus().all_ok)) {
        if (this.all_spawned_ts != null) {
            // console.log("==== ALL SERVICES LAUNCHED ===="); // .. but this does not mean services are ready
            if ( this._getSimpleCountStatus().all_ok) {
                if (this.when_all_seen_running_ts == null) {
                    this.when_all_seen_running_ts = Date.now();
                }
                if (this.all_started_ok_seen == false ) {
                    this.all_started_ok_seen = true;
                    if (this.uplink_cb) this.uplink_cb('all_start_0s_ok', '', "all launched ok"); // first indication of all seen running
                }
            } else {
                this.when_all_seen_running_ts = null;
            }
            // time since all launched the first time
            var cancel_interval_calls = false;
            var elapsed_ms = Date.now() - this.all_spawned_ts;
            var elapsed_all_stable_ms = (this.when_all_seen_running_ts == null) ? 0 /*ms*/: (Date.now()-this.when_all_seen_running_ts);
            var min_universe_dark_time_ms = 500 + (this.config.services.length*400/*ms/svc*/);
            // console.log("Control:____" + elapsed_ms);
            //      start of universe
            //      |       all_seen_running
            //              |
            // OK:            <-----elapsed_all_stable_ms-->| <--"all_start_2s_ok"
            // FAIL:| ......... ................................................| <--"all_start_2s_fail"
            if (elapsed_ms > min_universe_dark_time_ms) {
                // only after minimum time now we start looking at things, indicate OK as soon as seen, but only after "stability time" 2000ms
                if (elapsed_all_stable_ms > this.require_all_stable_ms /*ms*/) {
                    if (this.uplink_cb) this.uplink_cb('all_start_2s_ok', '', "all running stable for " + this.require_all_stable_ms + " ms");
                    cancel_interval_calls = true;
                }
                else if (elapsed_ms >=  this.max_all_ready_tmo_ms) {
                    // indicate failure
                    if (this.uplink_cb) this.uplink_cb('all_start_2s_fail', '', "all not ready in " + this.max_all_ready_tmo_ms + " ms");
                    cancel_interval_calls = true;
                }
            }
            if (cancel_interval_calls) {
                if (this.boot_interval_tmr != null) {
                    clearInterval(this.boot_interval_tmr);
                    this.boot_interval_tmr = null; // release
                }
            }
        }
    }
    _updateRunningState( svc, running, stopreason) {
        var found = false;
        Object.keys(this.services).forEach( (key) => {
            var rs = this.services[key];
            if ( rs.id == svc.id) {
                if ((running == false) && (this.boot_interval_tmr != null)) this.when_all_seen_running_ts = null;
                rs.running = running;
                rs.stopreason = stopreason;
                if (!running) rs.ts_start = null; // 2024-05-08 clear timestamp of start
                found = true;
            }
        });
        // After each state update, check the services status...
        // this._controlServiceStackStart( 'updateRunningState'); // 2024-11-06 moved this fragment to separate function

        if ( !found) {
            if (this.config.verbose === true) {
                console.error("====================================");
                console.error("_updateRunningState failed for " + svc.id );
                console.error(" Not found service in this.services: '" + svc.id +
                "'. This services has: " + Object.keys(this.services).length + " services");
            }
            if ( Object.keys(this.services).length > 0) {
                process.exit(0);
            }
        }
    }
    _getRunningSummary( ) {
        var result = { total: Object.keys(this.services).length, running:[], stopped:[], all_ok: false};// 2025-05-08 all_ok
        Object.keys(this.services).forEach( (skey) => {
            var rs = this.services[skey];
            if ( rs.running) result.running.push( rs);
            if (!rs.running) result.stopped.push( rs);
        });
        result.all_ok = (result.total == result.running.length); // 2024-05-08
        return result;
    }
    startStoppedServices( ){
        this.uplink_cb( 'debug', '', "compose: startStoppedServices " + os.type().toLowerCase() );
        Object.keys(this.services).forEach( (svckey, index) => {
            var svc = this.services[svckey];
            if ((svc.running == false) && (svc.restart !== false)) {
                this.uplink_cb( 'debug', '', "compose: startStoppedServices: restarting " + svckey + "..");
                svc.stopreason = '';
                this.restartCore( svc.id);
            }
        });
    }
    startServices( uplink_cb ) {
        if (this.boot_interval_tmr != null) {
            clearInterval( this.boot_interval_tmr );
            this.boot_interval_tmr = null;
        }
        this.uplink_cb = (uplink_cb ? uplink_cb : this.uplink_cb);
        // console.log("backendrunner: startServices " + os.type().toLowerCase() );
        this.log(" ");
        this.log(" ");
        this.log("----------------------------------------------------------");
        this.log("Compose registers START SERVICES request at starServices(" + this.config.services.length + ")");
        this.uplink_cb( 'debug', '', "compose: startServices " + os.type().toLowerCase() );
        this.config.services.forEach( (svc) => {
            var run = true;
            if ( run) {
                this.uplink_cb( 'debug', svc.id,  " -------------------- service " + svc.id + " start");
                Object.keys(svc).forEach( (key) => {
                    // console.log(" " + key + " : " + svc[key]  + "  <- " + typeof svc[key]);
                    this.uplink_cb( 'debug', svc.id, ' ' + key + ' : ' + svc[key]  + "  <- " + typeof svc[key] );
                    if (typeof svc[key] === 'string') {
                        svc[key] = this._replaceTextDefs(svc[key], svc)
                    } else if ( Array.isArray( svc[key] )) {                     // 20230106 params can be array
                        svc[key].forEach( (arritem, index) => {
                            svc[key][index] = this._replaceTextDefs(arritem, svc);//20230106 params arr replacements
                        });
                    }
                });
                svc.running = false;
                svc.stopreason = '';
                this.uplink_cb( 'debug', svc.id, JSON.stringify( svc) );
                this.runCommand(
                    svc, // svc_obj = original metainfo object (new 2021-02-13 1616)
                    svc.id,                                      // id
                    svc.shell == null? false : svc.shell,        // is_shell
                    svc.code,                                    // batchcmd
                    svc.params == null ? "": svc.params,         // params
                    svc.data,                                    // cwd (optional)
                    svc.options == null? {} : svc.options,       // options for spawn
                    ( err,val) => {                              // start cb
                        if ( err == null) {
                            svc.running = true;
                            svc.stopreason = '';
                        } else {
                            svc.running = false;
                            svc.stopreason = err;
                            this._serviceFinished(svc, err,val);
                        }
                        this._updateRunningState(svc, svc.running, svc.stopreason);
                    },
                    ( err, val) => {                             // stop cb
                        svc.running = false;
                        svc.ts_start = null; // 2024-05-08
                        svc.stopreason = err;
                        this._serviceFinished(svc, err,val);
                        this._updateRunningState(svc, svc.running, svc.stopreason);
                        if ((this.config.restart == false) && (this.running_count == 0)) {
                            var _msg = "NO restart requested with .restart: false and last child exit ==> Process.exit(0)";
                            this.log(_msg);
                            console.log(_msg);
                            process.exit(0);
                        }
                    }
                ); // runCommand
            } // if run
        }); // forEach(svc)
        this.all_spawned_ts = new Date(); // 2024-11-06 Now the big bang happened (just before this spawns were called for all)
        var that = this;
        this.boot_interval_tmr = setInterval( ( ) => {
                  that._controlServiceStackStart('boot_interval_tmr');
               },333);
        if ((this.config.showonly == true)||(this.config.showonly == 'relative')) {
            process.exit(0);
        }
    }
    stop() {
        return this.stopServices();
    }
    stopServices( ) {
        if (this.boot_interval_tmr != null) {
            clearInterval( this.boot_interval_tmr );
            this.boot_interval_tmr = null;
        }
        if ( this.config.verbose === true) console.log("compose: stopServices [" + Object.keys(this.services).length + "] Enter");
        this.log("Compose registers STOP SERVICES request at stopServices()");
        // try HTTP request first, if supported
        if ( this.config.verbose == true) console.log("compose: stopServices [1of2] - checking http shutdown option ");
        Object.keys(this.services).forEach( (svckey, index) => {
            var svc = this.services[svckey];
            if (svc.running == false) {
                if ( this.config.verbose === true) console.log("compose: stopServices " + svc.id + " already stopped");
            } else {
                var svcconfigs = this.config.services.filter(  svcconfig => svcconfig.id == svc.id );
                if (( svcconfigs) && ( svcconfigs[0].shutdown)) {
                    var shutdown = svcconfigs[0].shutdown;
                    shutdown = this._replaceTextDefs( shutdown, svcconfigs[0]);
                    if ( this.config.verbose === true) console.log("compose: stopServices - sending HTTP shutdown => " + svc.id + " pid " + svc.subprocess.pid + " : " +svcconfigs[0].shutdown);
                }
            }
        });
        // try killing harder...
        Object.keys(this.services).forEach( (svckey, index) => {
            var svc = this.services[svckey];
            if (svc.running == false) {
                if ( this.config.verbose === true) console.log("compose: stopServices [2of2] " + svc.id + " already stopped");
            } else {
                if ( this.config.verbose === true) console.log("compose: stopServices - sending SIGTERM => " + svc.id + " pid " + svc.subprocess.pid);
                try{
                    svc.subprocess.kill( ); // subprocess.kill([signal]) : no argument means "SIGTERM"
                } catch(err) {
                    if ( this.config.verbose === true) console.log("compose: stopServices - sent SIGTERM => " + svc.id + " pid " + svc.subprocess.pid + ", which failed: " + err);
                }
            }
        });
        this.all_spawned_ts = null; // 2024-11-06 allow new stack-start-control if startServices( ) would be called again after stopServices( )
        this.all_started_ok_seen = false; // 2024-11-06 the same as above
        if ( this.config.verbose === true) console.log("compose: stopServices Leave");
        this.services = { };
    }
}

class composeCmdline {
    constructor()  {
        this.config = {
            file : 'compose.json',
            interval_s_min : 5,  interval_s_max : 120,
            runtime_s : 0,       verbose : false,
            monitor : false,     interval_counter : 0, interval_s_print : 5,
            restart : true,       lastActivity : new Date(),
            passparams : null // whitespace separated string of opt. passed params
        }
        this.packageName = '<package name not specified in compose.json>';
        this.Composer = null;
        this.appCb = null;
    }
    // In CLI interface, processCommandLineOpts( ) is called next after contructor, and just before start( )
    processCommandLineOpts( ) {
        var key="";
        var passparams_seen = false;
        process.argv.slice(2).forEach( (arg) => {
            if ( (key.length == 0) && (['--','..','...'].indexOf(arg)>-1)) {
                passparams_seen = true;
            } else if ( !passparams_seen) {
                // collecting params for ourselves
                if (arg.indexOf('--') == 0) {
                    key = arg.slice(2).toLowerCase();
                    this.config[key] = true;
                } else  if ( key.length > 0) {
                    this.config[key] = arg.toString();// ((['root'].indexOf(key)>-1) ? arg.toString() : arg.toString().toLowerCase());
                    if (["true","TRUE","on"].indexOf( this.config[key])>-1) this.config[key] = true;
                    else if (["false","FALSE","off"].indexOf( this.config[key])>-1) this.config[key] = false;
                    key = "";
                }
            } else { // collect the params to pass to child process
                if (!this.config.passparams) this.config.passparams = '';
                if (this.config.passparams.length > 0) this.config.passparams += ' ';
                this.config.passparams += arg;
            }
        });

        if (this.config.verbose === true) console.log("NODE COMPOSE");
        if ( this.config.help) {
            console.log("Syntax:    node compose [options] [-- params to pass to svc.passparams:true child process]");
            console.log("[options]:");
            console.log(" no_options                 -- will start process group and if selectors used, with defined '.default_selectors'")
            console.log(" --help                     -- shows this help. No passed params supported for this.");
            console.log(" --selected (true)          -- shows services lists and exits (full and filtered list document).");
            console.log(" --showonly true/false*/relative -- If true or relative, only shows launch info, does not launch.");
            console.log("                               (!)relative is only 'for easy reading' output, not used internally.");
            console.log(" --verbose true/start/false* -- verbose output on.  start=show child process start command lines");
            console.log(" --root directory            -- optionally set (code) root dir for relative paths. Deflt: process.cwd())");
            console.log("                                Currently: " + process.cwd() + " i.e. defaults to current work dir.");
            console.log("                                You can refer to (code-)root in definitions section with {root}");
            console.log(" --cwdroot directory         -- optionally set cwd(data) root dir for relative paths. Deflt: process.cwd())");
            console.log("                                You can refer to cwdroot in definitions section with {cwdroot}");
            console.log("                                By default --cwdroot value equals --root value");
            console.log(" --file compose.json         -- use specific json-file instead of compose.json. See also --concat_file");
            console.log(" --restart true*/false       -- restart services if they stop (default true)");
            console.log(" --monitor true/false*       -- print service statuses with dynamic interval.");
            console.log(" --concat_file file.json     -- ADVANCED: specify opt. additional compose-json file to concat/overlay");
            console.log(" --nostart csv_svc_ids       -- ADVANCED: specify service(s) to (exceptionally) not start");
            console.log(" --start csv_svc_ids         -- ADVANCED: specify service(s) to (exceptionally) ONLY start");
            console.log(" --use|select csv_selectors  -- start only services, which have matching 'selectors' defined. I.e.");
            console.log("                                --select a,d -and- (svc).selectors: \"c,d\" -> starts svc with 'd'");
            console.log(" --leader svc_id             -- specify explicit service leader svc for the session.");
            console.log("                                '.leader' can be also be defined in compose.json svc options.");
            console.log("                                If service leader stops, then all other services are also stopped.")
            console.log(" --[useropt_key] val         -- instead of ENV, you can also provide useropt keys and values like this");
            console.log("                                Example: --hostip 10.161.0.21 to provide useroption 'hostip' with IP");
            console.log(" --require_all_stable_ms val -- define time in ms to require \"all running stable\" (3000)");
            console.log(" --max_all_ready_tmo_ms  val -- define max time to wait for \"all running stable\" to happen (13000)");
            console.log(" --exit_if_not_all_ready true");
            console.log("                             -- define if should exit if not all services ready @max_all_ready_tmo_ms (false)");
            console.log(" ");
            process.exit(0);
        }
        return this.config;
    }
    composeCallback( reason, child, data ) {
        this.config.lastActivity = new Date();
        if ( this.appCb ) {
            this.appCb( reason, child, data);
            return;
        }
        if (data) {
            if (( typeof data == 'object') && (Buffer.isBuffer(data)==false) ) data=JSON.stringify(data);
            var outstrs = data.toString( ).split('\n');
            outstrs.filter(row => (row.length>3)).forEach( row => {
                var rowp = row.replace('\r', '');
                if ( reason == 'child_stdout') {
                    console.log(rowp );
                }
                else if (reason == 'all_start_0s_ok') {
                    console.log(new Date().toISOString() + ' All launched initially. All ready tmo ms: ' + this.Composer.require_all_stable_ms);
                }
                else if (reason == 'all_start_2s_ok') {
                    console.log(new Date().toISOString() + ' All running OK near after launch');
                }
                else if (reason == 'all_start_2s_fail') {
                    if (this.config.verbose != false) console.log("");
                    if (this.config.verbose != false) console.log("##########################################################################");
                    console.log(new Date().toISOString() + ' W WARNING: Not all services started OK. Exit if = ' +  this.Composer.exit_if_not_all_ready);
                    console.log(data);
                    if ( this.Composer.exit_if_not_all_ready == true) {
                        this.Composer.stopServices();
                        if (process != null) {
                            process.exit(2); // exit with error code 2
                        }
                    }
                    if (this.config.verbose != false) console.log("##########################################################################");
                }
                else if (this.config.verbose === true) {
                    console.log( new Date().toISOString() + ' ' + ((reason!='debug') ? reason.padEnd(12,' ') : '') + child.padEnd(12,' ') + rowp );
                }
            });
        }
    }
    finalize( signal ) {
        if (this.config.verbose === true) console.log(this.packageName + " Compose services ending for " + signal);
        this.Composer.stop();
        process.exit(0);
    }
    stop(  ) {
        this.Composer.stop();
    }
    // start(  )
    // params:
    //    config   - optional override config object. See constructor this.config -assigments for available options.
    //               (!) null  means no-overlay (i.e. when you don't have config, but want to provide cb)
    //    cb       - optional application callback (goes to this.appCb) which is called for compose-notifications
    //              with (reason, child, data) parameters. Application callback disables local console.log for compose-callbacks.
    // returns:
    //    compose  -instance.  Note: if array of services to start is zero length, please exit
    start( config, cb ) {
        if ((typeof config !== 'undefined') && (config !== null)) {  // optionally add config overlay above default config
            this.config = {  ...this.config, ...config };
        }
        if (typeof cb !== 'undefined') this.appCb = cb;

        // Load the compose configuration...
        var confFile = compose.getComposeDataFilePathTo(this.config, this.config.file);
        if (fs.existsSync( confFile) == false) {
            var fjump = path.join(__dirname, "compose_jump.json");
            if ( fs.existsSync(fjump)) {
                var fjump = JSON.parse( fs.readFileSync( fjump, 'utf8') );
                if (fjump.file != null)       this.config.file    = fjump.file;
                if (fjump.composecwd != null) this.config.composecwd = fjump.composecwd;
                if (fjump.root != null)       this.config.root    = fjump.root;
                if (fjump.cwdroot != null)    this.config.cwdroot = fjump.cwdroot;
                this.config.root    = compose.magicDirReplace(this.config.root);
                this.config.cwdroot = compose.magicDirReplace(this.config.cwdroot, this.config.root);
                 // re-evaluate the config file based on optional new jump-info...
                confFile = compose.getComposeDataFilePathTo(this.config, this.config.file);
            }
        }
        var composeConfig = JSON.parse( fs.readFileSync(confFile, "utf-8"));
        this.packageName = composeConfig.packageName ? composeConfig.packageName : "(packageName not specified in compose.json)";
        if ((this.packageName) && (process.pid)) {
            var pidname="compose_pid_" + this.packageName.toLowerCase().split(' ').join('_').split("\\").join('_').split('/').join('_') + ".txt";
            fs.writeFileSync( compose.getComposeDataFilePathTo(this.config, pidname), process.pid.toString());
        }
        // Load/concat optional concat compose.json
        if ( this.config.concat_file) {
            var concatConfig = JSON.parse( fs.readFileSync(  compose.getComposeDataFilePathTo(this.config, this.config.concat_file), "utf-8"));
            if ( concatConfig.defs) composeConfig.defs  = { ...composeConfig.defs,  ...concatConfig.defs };
            composeConfig.services.concat( concatConfig.services ); // add extra services from extension config
        }

        // Call static method to commonly preprocess common filtering/selection related options and inherit svc img to svc inst.
        composeConfig = compose.preProcessServices(composeConfig, this.config);

        this.Composer = new compose( { ...this.config, ...composeConfig, ...{verbose: this.config.verbose, passparams: this.config.passparams} } );
        this.Composer.startServices(  this.composeCallback.bind(this) );
        this.Composer.log("Compose has started. Preprocessing complete");
        process.on('SIGTERM', () => {     this.finalize('SIGTERM'); });
        process.on('SIGINT',  () => {     this.finalize('SIGINT');  });
        process.on('exit',    (code) => { if ( this.config.verbose === true) console.log(this.packageName + " Compose services exit(" + code +")");});
        if (this.config.verbose === true) console.log(this.packageName + " Compose services START");

        setInterval( () => {
            this.config.runtime_s++;
            var now = new Date();
            var activity = false;
            if ((((now - this.config.lastActivity) > 900) && ((now - this.config.lastActivity) <2400)) && ( this.config.runtime_s > 4)) {
                activity = true;
                this.config.interval_s_print = this.config.interval_s_min;
            }
            if ((this.config.runtime_s < 4) || (++this.config.interval_counter >= this.config.interval_s_print)|| (activity)) {
                this.config.interval_counter = 0;
                var serviceStatus = this.Composer._getRunningSummary( );
                if (this.config.monitor) {
                    console.log(new Date().toISOString() + ' ' + this.packageName + " Compose IDLE  (cycle 1/"
                        + this.config.interval_s_print.toString().padStart(4,'0') + ") runtime ~ "
                        + this.config.runtime_s.toString().padStart(6,' ')
                        + " s. Status: " +  serviceStatus.running.length + " running, " + serviceStatus.stopped.length + " stopped");
                    if (serviceStatus.running) {
                        serviceStatus.running.forEach( (svc) => {
                            console.log( '   ' + svc.id.padEnd(10,' ') + ' pid:' + (svc.pid || 'no pid!').toString().padStart(5,' ') );
                        });
                    }
                }
                if (( serviceStatus.stopped.length > 0) &&  ([true,'true','on','restart'].indexOf(this.config.restart)>-1)) {
                    this.Composer.startStoppedServices(); // attempt restart...
                }
                if (this.config.interval_s_print < this.config.interval_s_max) this.config.interval_s_print += 10;
            }
        }, 1000);
        return this.Composer;
    }
}
// check commandline tool case
function finder( args) {
    return ["compose/compose","compose\\compose","compose","compose.js"]
        .find( (ending) => {
            var val1= args[1].indexOf(ending);
            var val2 = (args[1].length - ending.length );
            return ((val1 == val2) && (val1>-1));
        });
}
if ((process.argv.length >1) && finder(process.argv)!= null) {
    var composeHandler = new composeCmdline();
    composeHandler.processCommandLineOpts();
    composeHandler.start( );
}

// NodeJs
if (typeof exports !== 'undefined') {
    module.exports.compose = compose; // special direct access to the compose class
    module.exports.composeCmdLine = composeCmdline; // command line tool access
}
// Browser - (theoretical, will not work as such)  Ref: https://javascript.info/global-object
else {
    var rootObj = globalThis || global || window || self || this;
    rootObj.Satel = rootObj.Satel || { };
    rootObj.Satel.compose = compose; // special direct access to the compose-class
    rootObj.Satel.composeCmdline = composeCmdline;
}