Search code examples
javascriptnode.jsnpmspawn

Let NodeJS application update itself using NPM


Hej There,

I'm trying to add some non-conventional functionality to my NodeJS application but I'm having some trouble. What I'm trying to do is the following:

I want to update my server code from the client. (An auto-update functionality if you will.)

My first attempt was to utilize the NPM API and run:

 npm.commands.install([package], function(err, data)

But of course this results in an error telling me NPM can not install while the server is running.

My second attempt was spawning NPM update using the following code:

  spawnProcess('npm', ['update'], { cwd: projectPath }, done);

The spawnProcess function is a generic spawn function:

var projectPath = path.resolve(process.cwd());
var spawnProcess = function(command, args, options, callback) {
    var spawn = require('child_process').spawn;
    var process = spawn(command, args, options);
    var err = false;

    process.stdout.on('data', function(data) {
        console.log('stdout', data.toString());
    });

    process.stderr.on('data', function(data) {
        err = true;
        console.log('stderr', data.toString());
    });

    if (typeof callback === 'function') {
        process.on('exit', function() {
            if (!err) {
                return callback();
            }
        });
    }
};

But this gives me a stderr followed by a 'CreateProcessW: can not find file' error. I don't quite know what I'm doing wrong.

If all else fails I thought it might be possible to write a shellscript killing Node, updating the application and then rebooting it. Something like:

kill -9 45728
npm update
node server

But I don't know if this is a plausible solution and how I would go about executing it from my node server. I'd rather have the spawn function working of course.

Any help is welcome. Thanks in advance!


Solution

  • So I finally fixed this issue. If someone is interested how I did it, this is how:

    I built a function using the NPM api to check if the current version is up to date:

     exports.checkVersion = function(req, res) {
        npm.load([], function (err, npm) {
            npm.commands.search(["mediacenterjs"], function(err, data){
                if (err){
                    console.log('NPM search error ' + err);
                    return;
                } else{
                    var currentInfo = checkCurrentVersion();
                    for (var key in data) {
                        var obj = data[key];
                        if(obj.name === 'mediacenterjs' && obj.version > currentInfo.version){
                            var message = 'New version '+obj.version+' Available';
                            res.json(message);
                        }
                    }
                }
            });
        });
    }
    
    var checkCurrentVersion = function(){
        var info = {};
        var data = fs.readFileSync('./package.json' , 'utf8');
    
        try{
            info = JSON.parse(data);
        }catch(e){
            console.log('JSON Parse Error', e);
        }
    
        return info;
    };
    

    If the version is not up to date I initiate a download (in my case the github master repo url) using node-wget:

    var wget = require('wget');
    var download = wget.download(src, output, options);
    download.on('error', function(err) {
        console.log('Error', err);
        callback(output);
    });
    download.on('end', function(output) {
        console.log(output);
        callback(output);
    });
    download.on('progress', function(progress) {
        console.log(progress * 100);
    });
    

    The callback kicks off the unzip function bases on @John Munsch 'Self help' script but I added a check to see if there was a previous unzip attempt and if so, I delete the folder:

    if(fs.existsSync(dir) === false){
        fs.mkdirSync(dir);
    } else {
        rimraf(dir, function (err) { 
            if(err) {
                console.log('Error removing temp folder', err);
            } else {
                fileHandler.downloadFile(src, output, options, function(output){
                    console.log('Done', output);
                    unzip(req, res, output, dir);
                });
            }
        });
    }
    console.log("Unzipping New Version...");
    var AdmZip = require("adm-zip");
    var zip = new AdmZip(output);
    zip.extractAllTo(dir, true);
    
    fs.openSync('./configuration/update.js', 'w');
    

    The openSync function kicks off my 'NodeMon' based file (https://github.com/jansmolders86/mediacenterjs/blob/master/server.js) which kills the server because it is listing for changes to that specific file. Finally it restart and starts the following functions:

    function installUpdate(output, dir){
        console.log('Installing update...');
        var fsExtra = require("fs.extra");
        fsExtra.copy(dir+'/mediacenterjs-master', './', function (err) {
            if (err) {
                console.error('Error', err);
            } else {
                console.log("success!");
                cleanUp(output, dir);
            }
        });
    }
    
    function cleanUp(output, dir) {
        console.log('Cleanup...');
        var rimraf = require('rimraf');
        rimraf(dir, function (e) {
            if(e) {
                console.log('Error removing module', e .red);
            }
        });
    
        if(fs.existsSync(output) === true){
            fs.unlinkSync(output);
            console.log('Done, restarting server...')
            server.start();
        }
    }
    

    Thanks to everyone that helped me out!