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.
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.
- publish the device's node app in a private github repository.
- install and configured git on each of the Edison devices.
- clone the github repository onto each of the Edison devices.
- install the forever module on each of the devices to keep the app up and allow easy restarting of the app.
- create a startup script that starts the app using forever.
- create and enabled a linux service that runs the startup script on boot.
- add code to the app to periodically ask the web service what the latest version of the app should be.
- 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.