0

I'm trying to start a windows service from a node script. This service has a bad habit of hanging and sometimes requires a retry to start successfully. I have a promise while loop setup (Please feel free to suggest a better way). The problem I'm having, is with each loop the sc.pollInterval output writes duplicate results in the console. Below is an example of the duplicate content I see in the console, this is after the second iteration in the loop, i'd like it to only display that content once.

sc \\abnf34873 start ColdFusion 10 Application Server

sc \\abnf34873 queryex ColdFusion 10 Application Server

SERVICE_NAME: ColdFusion 10 Application Server
        TYPE               : 10  WIN32_OWN_PROCESS
        STATE              : 2  START_PENDING
                                (NOT_STOPPABLE, NOT_PAUSABLE, IGNORES_SHUTDOWN)
        WIN32_EXIT_CODE    : 0  (0x0)
        SERVICE_EXIT_CODE  : 0  (0x0)
        CHECKPOINT         : 0x0
        WAIT_HINT          : 0x7d0
        PID                : 0
        FLAGS              :

SERVICE_NAME: ColdFusion 10 Application Server
        TYPE               : 10  WIN32_OWN_PROCESS
        STATE              : 2  START_PENDING
                                (NOT_STOPPABLE, NOT_PAUSABLE, IGNORES_SHUTDOWN)
        WIN32_EXIT_CODE    : 0  (0x0)
        SERVICE_EXIT_CODE  : 0  (0x0)
        CHECKPOINT         : 0x0
        WAIT_HINT          : 0x7d0
        PID                : 13772
        FLAGS              :

Here is the code I have. Basically, I'm going to try to start the service 3 times. If it doesn't, then I throw and error. One thing to note, when I attempt to start the service, but it's stuck in 'Start_pending' state, I kill the process and then try to start it again.

var retryCount = 0;

// Start the colfusion service
gulp.task('start-coldfusion-service', function(done) {
    var serviceStarted = false;
    console.log("Starting coldfusion service..");
    // This says we're going to ask where it's at every 30 seconds until it's in the desired state.
    sc.pollInterval(30);
    sc.timeout(60);
    retryCount = 0;

    tryServiceStart().then(function(result) {
          // process final result here
        done();
    }).catch(function(err) {
        // process error here
    });
});


function tryServiceStart() {
    return startService().then(function(serviceStarted) {
        if (serviceStarted == false) {
            console.log("Retry Count: " + retryCount);
            // Try again..
            return tryServiceStart();
        } else {
             return result;
        }
    });
}

function startService() {
    return new Promise(function(resolve, reject) {
        var started = true;
        // Make sure the coldfusion service exists on the target server
        sc.query(targetServer, { name: 'ColdFusion 10 Application Server'}).done(function(services) {
            // if the service exists and it is currentl stopped, then we're going to start it.
            if (services.length == 1) {
                var pid = services[0].pid;
                if (services[0].state.name == 'STOPPED') {
                    sc.start(targetServer, 'ColdFusion 10 Application Server')
                        .catch(function(error) {
                            started = false;
                            console.log("Problem starting Coldfusion service! error message: " + error.message);
                            console.log("retrying...");
                            retryCount++;
                            if (parseInt(retryCount) > 2) {
                                throw Error(error.message);
                            }
                       })
                       .done(function(displayName) {
                            if (started) {
                                console.log('Coldfusion service started successfully!');
                            }
                            resolve(started);
                       });
                } else if (services[0].state.name == 'START_PENDING') {
                    kill(pid, {force: true}).catch(function (err) {
                        console.log('Problem killing process..');
                    }).then(function() {
                        console.log('Killed hanging process..');
                        resolve(false);
                    });
                }
            } else {
                console.log("Could not find the service in a stopped state.");
                resolve(false);
            }
        });
   });
}
Jmh2013
  • 2,625
  • 2
  • 26
  • 42
  • The service-control-manager package claims that "all commands return a promise", however the documentation (which your code correctly adheres to) tells us that these so-called promises to have `.done()` and `.catch()` methods. It's far from clear whether `.done()` possesses the full power of a proper `.then()` or whether `.done()` is just a lame thing like jQuery's `.done()`. The examples suggest the latter and I have failed to find anything online that tells me otherwise. – Roamer-1888 Jan 18 '18 at 07:09
  • 1
    Thanks for the information @Roamer-1888. I believe you're right in that it's similar to JQuery's `.done()` method since whatever is inside it executes every time, whether the promise is rejected or resolved. Anyway, I've found a solution I"m happy with using a Promise-retry package. I'll post the new code as an answer soon. – Jmh2013 Jan 18 '18 at 12:36
  • Jmh2013, I played around with some ideas earlier today. I'll post them as an answer when I get back to my desktop. – Roamer-1888 Jan 18 '18 at 17:20

2 Answers2

1

Not too sure why you get duplicate results in the console, however below are some ideas on how the code might be better written, chiefly by promisifying at the lowest level.

Sticking fairly closely to the original concept, I ended up with this ...

Promisify sc commands

  • sc commands return something which is promise-like but with a .done() method that does not, in all probability, possess the full power of a genuine .then()
  • promisify each command as .xxxAsync()
  • by adopting each command's .done as .then, Promise.resolve() should be able to assimilate the promise-like thing returned by the command.
;(function() {
    commands.forEach(command => {
        sc[command].then = sc[command].done;
        sc[command + 'Async'] = function() {
            return Promise.resolve(sc[command](...arguments)); 
        };
    }).
}(['start', 'query'])); // add other commands as required

gulp.task()

  • promise chain follows its success path if service was opened, otherwise its error path
  • no need to test a result to detect error conditions in the success path.
gulp.task('start-coldfusion-service', function(done) {
    console.log('Starting coldfusion service..');
    // This says we're going to ask where it's at every 30 seconds until it's in the desired state.
    sc.pollInterval(30);
    sc.timeout(60);
    tryServiceStart(2) // tryServiceStart(maxRetries)
    .then(done) // success! The service was started.
    .catch(function(err) {
        // the only error to end up here should be 'Maximum tries reached'.
        console.err(err);
        // process error here if necessary
    });
});

tryServiceStart()

  • orchestrate retries here
function tryServiceStart(maxRetries) {
    return startService()
    // .then(() => {}) // success! No action required here, just stay on the success path.
    .catch((error) => {
        // all throws from startService() end up here
        console.error(error); // log intermediate/final error
        if(--maxRetries > 0) {
            return tryServiceStart();
        } else {
            throw new Error('Maximum tries reached');
        }
    });
}

startService()

  • form a fully capable promise chain by calling the promisified versions of sc.query() and sc.start()
  • console.log() purged in favour of throwing.
  • thrown errors will be caught and logged back in tryServiceStart()
function startService() {
    // Make sure the coldfusion service exists on the target server
    return sc.queryAsync(targetServer, { name: 'ColdFusion 10 Application Server'})
    .then((services) => {
        // if the service exists and it is currently stopped, then start it.
        if (services.length == 1) {
            switch(services[0].state.name) {
                case 'STOPPED':
                    return sc.startAsync(targetServer, 'ColdFusion 10 Application Server')
                    .catch((error) => {
                        throw new Error("Problem starting Coldfusion service! error message: " + error.message);
                    });
                break;
                case 'START_PENDING':
                    return kill(services[0].pid, { 'force': true })
                    .then(() => {
                        throw new Error('Killed hanging process..'); // successful kill but still an error as far as startService() is concerned.
                    })
                    .catch((err) => {
                        throw new Error('Problem killing process..');
                    });
                break;
                default:
                    throw new Error("Service not in a stopped state.");
            }
        } else {
            throw new Error('Could not find the service.');
        }
    });
}

Checked only for syntax error, so may well need debugging.

Offered FWIW. Feel free to adopt/raid as appropriate.

Roamer-1888
  • 19,138
  • 5
  • 33
  • 44
  • Sorry for the late reply, I was on a short vacation. I really like what you did here. I was still getting duplicate results in the console, though. The `promise-retry` package fixed that for me. So I've removed the `tryServiceStart()` function and am using the `Promise-retry` package to orchestrate the retries right in the 'start-coldfusion-service' task. I've adopted my code to closely match what you have here. I especially like how the errors will bubble up as they should now. – Jmh2013 Jan 22 '18 at 15:48
  • I'm not sure what was causing the duplicate results or why Promise-retry should fix them. There must be some subtle difference between the behaviour of my code and Promise-retry but for the life of me, I can't think what it might be. Anyways, the important thing is that you have a solution. Good luck with it. – Roamer-1888 Jan 22 '18 at 16:44
0

I have found another npm package called promise-retry that seems to have addressed the issue I was having. At the same time, I believe it made my code a little more clear as to what it's doing.

gulp.task('start-coldfusion-service', function(done) {
    var serviceStarted = false;
    console.log("Starting coldfusion service..");
    // Since starting a service on another server isn't exactly fast, we have to poll the status of it.
    // This says we're going to ask where it's at every 30 seconds until it's in the desired state.
    sc.pollInterval(30);
    sc.timeout(60);     

    promiseRetry({retries: 3}, function (retry, number) {
        console.log('attempt number', number);

        return startService()
        .catch(function (err) {
            console.log(err);
            if (err.code === 'ETIMEDOUT') {
                retry(err);
            } else if (err === 'killedProcess') {
                retry(err);
            }

            throw Error(err);
        });
    })
    .then(function (value) {
        done();
    }, function (err) {
        console.log("Unable to start the service after 3 tries!");
        process.exit();
    });

});

function startService() {
    var errorMsg = "";
    return new Promise(function(resolve, reject) {
        var started = true;
        // Make sure the coldfusion service exists on the target server
        sc.query(targetServer, { name: 'ColdFusion 10 Application Server'}).done(function(services) {
            // if the service exists and it is currentl stopped, then we're going to start it.
            if (services.length == 1) {
                var pid = services[0].pid;
                if (services[0].state.name == 'STOPPED') {
                    sc.start(targetServer, 'ColdFusion 10 Application Server')
                        .catch(function(error) {
                            started = false;
                            errorMsg = error;
                            console.log("Problem starting Coldfusion service! error message: " + error.message);
                            console.log("retrying...");
                       })
                       .done(function(displayName) {
                            if (started) {
                                console.log('Coldfusion service started successfully!');
                                resolve(started);
                            } else {
                                reject(errorMsg);
                            }
                       });
                } else if (services[0].state.name == 'START_PENDING') {
                    kill(pid, {force: true}).catch(function (err) {
                        console.log('Problem killing process..');
                    }).then(function() {
                        console.log('Killed hanging process..');
                        reject("killedProcess");
                    });
                } else {
                    // Must already be started..
                    resolve(true);
                }
            } else {
                console.log("Could not find the service in a stopped state.");
                resolve(false);
            }
        });

   });

}
Jmh2013
  • 2,625
  • 2
  • 26
  • 42
  • Be careful with `kill()` which returns (we assume) a Promise. If so then with `kill(...).catch(...).then(...);`, unless the `catch` callback throws/rethows, execution will drop through to the `then` callback. You will see in my solution that I reversed the order to read `kill(...).then(...).catch(...);`. And even with that reversal in place, make sure the catch callback calls `reject()`; logging isn't enough. – Roamer-1888 Jan 22 '18 at 03:08
  • Also, for uniformity always reject with (or throw) a proper `Error`, not a `String`. That way all downstream catches can be written always to receive an `Error` regardless of how/where it arose. – Roamer-1888 Jan 22 '18 at 03:08
  • Thanks for the tips. I've updated my code to resemble what you have in your answer. So this, I believe, is taken care of now. This is my first time using Node and working with promises so thank you very much for your help! This part of my script is working great now! – Jmh2013 Jan 22 '18 at 15:52