Self Updating Edison Apps


One of the challenges I have had in handing out prototype devices is keeping the software up to date. Solutions like resin.io do a nice job of deploying bundler images onto devices like the Edison. The primary issue I had was the overhead of pushing bundler images around when the only thing that was changing was the node app. It seemed easier to just use git and npm to handle the updates. I may go the resin.io route later, but this early in dev and testing the git route seems simpler.

/images/intel_edison.jpg

Overview of What I Did

The product I am prototyping has two parts, an Edison device (with additional sensors and actuators) running a node app, and a node web service for managing device configuration and data running in the cloud.

  1. publish the device's node app in a private github repository.
  2. install and configured git on each of the Edison devices.
  3. clone the github repository onto each of the Edison devices.
  4. install the forever module on each of the devices to keep the app up and allow easy restarting of the app.
  5. create a startup script that starts the app using forever.
  6. create and enabled a linux service that runs the startup script on boot.
  7. add code to the app to periodically ask the web service what the latest version of the app should be.
  8. if a new version is available the app will do a pull and use forever to do a restart using the new version.

The Details

Initial Cleanup

  • Remove any app from the default Edison node_app_slot directory so you don't accidentally start the app using the default Edison process.
    mv /node_app_slot /node_app_slotbk
    mv  ~/.node_app_slot/ ~/.node_app_slotbk

Install Git

Update /etc/opkg/base-feeds.conf with these 3 lines

    src all     http://iotdk.intel.com/repos/1.1/iotdk/all
    src x86 http://iotdk.intel.com/repos/1.1/iotdk/x86
    src i586    http://iotdk.intel.com/repos/1.1/iotdk/i586

Update opkg and install git

    opkg update
    opkg install git

Option 1: Modify Edison's default port

I wanted to use port 80 for the node app so I moved the default Edison config service to port 8080

Change default port in edison-cofig-server

Edit /~/usr/lib/edison_config_tools/edison-config-server.js/~ and change the last line to use a port other than 80.

    http.createServer(requestHandler).listen(8080);

Option 2: Disable the Edison config web service

    systemctl disable edison_config
    systemctl stop    edison_config

Setup to use github

Generate a key for use with github

 ssh-keygen -t rsa -b 4096 -C "me@my.email"

Follow the directions, easiest is just to hit return at the prompts. I chose to not do a passphrase for my small pilot.

Generate a deployment key for the github repository

  • Go to your app's github repository, choose settings, choose deployment keys
  • Click the Add Deployment Key button
  • Give it a title (the host name for the device works)
  • Back on the Edison run cat home/root.ssh/id_rsa.pub to get the text for the public key
  • copy the text for the public key to the Key input box back on github.

Clone your repo

Back on the Edison

    cd /
    git clone <your repo> AppDirName
    cd /AppDirName

Pull Latest code

Now any time you update code on master a simple git pull will update the latest code.

    git pull

Install forever module using npm

Forever will automatically restart a node app if it crashes. It also has some handy restart features.

    npm install -g forever

Create a startup script & service

Creating a startup service will allow your app to start automatically using forever.

Create startup.sh to start node app (server.js in this case)

    #!/bin/sh
    cd /AppDirName 
    forever start server.js

Make startup.sh executable

    chmod +x startup.sh

Create a startup service file

open a new file /lib/systemd/system/startup.service and add the configuration text from below.

    [Unit]
     Description=STARTUP
     [Service]                           
     Type=idle                           
     RemainAfterExit=true
     ExecStart=/AppDirName/startup.sh
     Environment="HOME=/home/root"    
     WorkingDirectory=/AppDirName/   
     [Install]                     
     WantedBy=multi-user.target 

Enable startup service

    systemctl enable /lib/systemd/system/startup.service

Enable your node app to update itself

By using a simple update function you can get your app to update itself and restart.

    var spawn = require('child_process').exec;
    var semver = require('semver');
    var bunyan = require('bunyan');
    var log = bunyan.createLogger({
        name: 'app',
        streams: [{
            type: 'rotating-file',
            path: '/var/log/app.log',
            period: '1d',
            count: 7        
        }]
    });
    var pjson = require('./package.json');

    var checkVersion = function(){
      var currentVersion = pjson.version;
      var options = {  hostname: 'www.myhost.com',
                                 port: 80,
                       path: 'http://www.myhost.com/device_version/',
                       method: 'GET',
                       headers: {'Content-Type': 'application/json'}
                    };
      var callback = function(response) {
        var dataStr = '';
        response.on('data', function (chunk) {
          dataStr += chunk;
        });

        response.on('end', function () {
          var versionInfo = JSON.parse(dataStr);
          var latestVersion = versionInfo.client_version || "0.0.0"; //don't update if missing version info
          log.info("current version: ", currentVersion);
          log.info("latest version: ", latestVersion);
          if (semver.gt(latestVersion, currentVersion)){
            log.info("pulling newer versions");
            spawn('git pull', function(error, stdout, stderr) {
              if (error){
                log.error("ERROR pulling latest: ", error);
              }
              else{
                log.info("updating packages");
                spawn('npm update', function(error, stdout, stderr){
                  if (error){
                    log.error("ERROR updating packages: ", error);
                  }
                  else {
                    log.info("restarting node");
                    spawn('forever restartall', function(error, stdout, stderr){
                      if (error){
                        log.error("ERROR restarting: ", error);
                      }
                      else {
                         log.info("restarted");
                      }
                    });
                  }           
                });
              }
            }); 
          }
        });
      };

    //check for updates at app startup
    checkVersion();

    //then check for updates every hour;
    setInterval(function() {
        checkVersion();                                                       
      }, 3600000);

On the server

You will need to add a route on your server to provide the version info. In this example the route was a GET request to the device_version route. For simplicity I just use an env_var on the service. I simply update the env_var when a new version is available. Then in the logic for the device_version route I pass back the version found in the env_var.

The logic for comparing versions is very basic and flawed, but will work in this simple case.

Improvements

Ideally instead of a straight git pull you can instead download a tagged version, and keep the current and next version info for each device in the web service db. This would allow rolling out upgrades to specific devices, etc. Another approach would be to pass back version info to the device so updates could roll out immediately if the device is in use. Finally more logic on the device to schedule an update when not active would be ideal. In that case maybe adding more than versionNumber of the latest version to the server response, maybe a priority value also.

This was a quick experiment it getting updates to percolate out to devices prototype devices, and so far it seems to be working well.

comments powered by Disqus