5

I'm building an app for windows using Electron. To package and distribute it I'm using electron-builder. Electron-builder relies on many packages, and for auto-updates it uses Squirrel-windows.

I've been battling with auto-update on Windows for 3 days and at the end I've come up with a working solution that seems to give no problems.

I wont' go into the details of what I've tried, and failed. Instead, I'll post here the solution with which I've come up.

I'm sharing it with you guys, to see if you may point out to me any flaws that will make my system fail, or, if it truly is a solid solution, to help those who are struggling as I was. For this latter reason, I'm posting some more code than it would be necessary, hoping it will help others.

The logic is as follows:

  • if the sub-folder fullupdate inside the path of the current executable does not exists (see later, it will be clarified), we connect with an online server and check if there is an update by sending the current app version;
  • if there is no update, do nothing.
  • if there is an update, we instruct the server to return a json string that contains the url from which we can download of the .exe installer produced by electron-builder. NB: not the .nupkg (server code not provided :-)).
  • we download the file and save it inside a sub folder fullupdate in the local folder in which our app is currently saved. This should be "safe" as electron-builder saves the app in the current user folder AppData, so we should not have permissions issues.
  • at the end of the download, we create a new file update inside the folder fullupdate to be sure the download has finished successfully. We could also rename the file, but I prefer this way.
  • next time the app opens:
    • if the folder fullupdate exists we check if the file update exists. If it does not exists, the download was not finished, so we delete the folder fullupdate and call the remote server again to start all over again.
    • else, if the file update exists, we launch the .exe file we have downloaded, and return true. This will prevent the app from opening the main window. The cool thing is that the updater will delete the whole old version of the app saved in AppData (while leaving local user data) and replace it with the new version. In this way we will get rid also of the folder fullupdate.

Now the code:

// we want to run this only on windows
var handleStartupEvent = function() {
if (process.platform !== 'win32') {
    return false;
}

/////////////////
// MANUAL UPDATER
/////////////////

var appFolder = 'app-' + appVersion;
var pathApp = path.dirname(process.execPath);
var pathUpdate = pathApp + '\\fullupdate';
var checkupdateurl = 'https://api.mysite.com/getjson/' + appVersion.split('.').join('-');

function checkIfDownloaded(){
  if (!fs.existsSync(pathUpdate)) checkUpdate();
  else return checkIfInstallLocal();
}

function checkIfInstallLocal(){
  if(fileExists('fullupdate\\update')) return installLocal();
  else {
      deleteFolderRecursive(pathUpdate);
      checkUpdate();
  }
}

function installLocal(){
  cp.exec('fullupdate\\Update.exe', function( error, stdout, stderr){
     if ( error != null ) {
          console.log(stderr);
     }
 });
 return true;
}

// from http://www.geedew.com/remove-a-directory-that-is-not-empty-in-nodejs/
var deleteFolderRecursive = function(path) {
    if( fs.existsSync(path) ) {
        fs.readdirSync(path).forEach(function(file,index){
            var curPath = path + "/" + file;
            if(fs.lstatSync(curPath).isDirectory()) deleteFolderRecursive(curPath);
            else fs.unlinkSync(curPath);
        });
        fs.rmdirSync(path);
    }
};

// from http://stackoverflow.com/questions/4482686/check-synchronously-if-file-directory-exists-in-node-js
function fileExists(path) {
    try  {
        return fs.statSync(path).isFile();
    }
    catch (e) {
        if (e.code == 'ENOENT') { // no such file or directory. File really does not exist
            return false;
        }
        throw e; // something else went wrong, we don't have rights, ...
    }
}

function checkUpdate(){
  https.get('https://api.mysite.com/getjson/' + app.getVersion().split('.').join('-'), (res) => {
      res.setEncoding('utf8');
      res.on('data', function(chunk) {
          if(chunk) thereIsUpdate(chunk);
      });
  }).on('error', (e) => {
      console.log(e);
  });
}

function thereIsUpdate(chunk){
  var data = JSON.parse(chunk);
  if(data && data.url) getNewUpdate(data.urlsetup);
}

function getNewUpdate(url){
  fs.mkdirSync(pathUpdate);
  var file = fs.createWriteStream(pathUpdate + '/Update.exe');
  var responseSent = false; // flag to make sure that response is sent only once.
  var request = https.get(url, function(response) {
    response.pipe(file);
    file.on('finish', () =>{
      file.close(() => {
          if(responseSent)  return;
          responseSent = true;
      });
      fs.closeSync(fs.openSync(pathUpdate + '/update', 'w'));
    });

  });
}

if(checkIfDownloaded()) return true;

/////////////////////////
// SQUIRREL EVENTS HANDLER
//////////////////////////

    // see http://stackoverflow.com/questions/30105150/handle-squirrels-event-on-an-electron-app
};

// here we call the function. It is before the opening of the window, so that we prevent the opening if we are updating, or if there is a Squirrel event going on (see SO question, link above)
if (handleStartupEvent()) {
  return;
}
don
  • 4,113
  • 13
  • 45
  • 70
  • This seems like a reasonable approach. I like that the process is at least straight-forward / not overly complex. It would be nice though if you described the context of the code you supplied. For example, it appears to be a custom function that I'm guessing you call from `main.js` when the application starts. – jacobq May 20 '16 at 21:55

0 Answers0