Graceful Router Power Down

You have hard drive(s) attached to your router. You’re not sure if just yanking power is going to corrupt them.

If you issue:

halt

… or:

poweroff

… the router just reboots.

So let’s set it up for a graceful shutdown.

First, create the shutdown script:

vi /etc/prep-unplug

… press i to enter editing mode, then paste this code in:

#!/bin/sh

echo "Isolating WAN traffic..."
ifdown wan 2>/dev/null
ifdown wan6 2>/dev/null

echo "Flushing filesystems..."
sleep 5
sync
sleep 5
sync

# Turn off the default green power light
if [ -d /sys/class/leds/green:status ]; then
    echo 0 > /sys/class/leds/green:status/brightness 2>/dev/null
fi

# Configure the red status LED to flash
if [ -d /sys/class/leds/red:status ]; then
    echo timer > /sys/class/leds/red:status/trigger 2>/dev/null
    echo 500 > /sys/class/leds/red:status/delay_on 2>/dev/null
    echo 500 > /sys/class/leds/red:status/delay_off 2>/dev/null

fi
echo "Stopping network services..."
service uhttpd stop 2>/dev/null
service dnsmasq stop 2>/dev/null

echo "Red LED flashing: You may power down the router."

# Enter a trap loop until the power is off.
while true; do
    sleep 1
done

Press Esc to exit editing mode, then :wq to save and exit.

vi /etc/profile

Press i to enter editing mode, then scroll to the bottom of that file and enter:

alias poweroff='/etc/prep-unplug'

Press Esc to exit editing mode, then :wq to save and exit.

The above catches terminal commands. If you issue poweroff from the terminal, it’ll run the /etc/prep-unplug script.

But, if you have automated internal tasks or third-party cron scripts calling /sbin/poweroff explicitly, an alias won’t catch that. You can fix this by dropping a symbolic wrapper script higher up in the system path execution priority (/usr/sbin/).

vi /usr/sbin/poweroff

Press i to enter editing mode, then enter:

#!/bin/sh
exec /etc/prep-unplug

Press Esc to exit editing mode, then :wq to save and exit.

When a service calls poweroff, the system searches /usr/sbin/. The /usr/sbin/poweroff script intercepts the hardware system call, and runs the /etc/prep-unplug script.

Make it all impervious to firmware updates. Under System >> Backup / Flash Firmware >> Configuration tab, add to the list:

/etc/profile
/etc/prep-unplug
/usr/sbin/poweroff

… then click the Save button at bottom.

Ensure the system sees it:

which poweroff

… which should return:

/sbin/poweroff

Ensure the system sees it:

type poweroff

… which should return:

poweroff is an alias for /etc/prep-unplug

Ok, the above completed, now comes the fun part. Let’s add a button under System >> Reboot to shut down the router gracefully from the LuCI interface.

First, if you don’t already have it, install luci-app-filemanager. It’s very nice. You can browse through the files on the router, and edit them, all in the LuCI interface.

Now, we’re going to go to:

/www/luci-static/resources/view/system/reboot.js

Record the contents of that file in a text editor, so if things go wrong, you can just paste the original code back in, save it, and everything is back to normal.

My original file contents (yours may be different… if so, I recommend knowing javascript so you can pick out where to inject the code to place the Power Off button.

‘use strict’;‘require view’;‘require rpc’;‘require ui’;‘require uci’;var callReboot=rpc.declare({object:‘system’,method:‘reboot’,expect:{result:0}});return view.extend({load:function(){return uci.changes();},render:function(changes){var body=E([E(‘h2’,(‘Reboot’)),E(‘p’,{},(‘Reboots the operating system of your device’))]);for(var config in(changes||{})){body.appendChild(E(‘p’,{‘class’:‘alert-message warning’},(‘Warning: There are unsaved changes that will get lost on reboot!’)));break;} body.appendChild(E(‘hr’));body.appendChild(E(‘button’,{‘class’:‘cbi-button cbi-button-action important’,‘click’:ui.createHandlerFn(this,‘handleReboot’)},(‘Perform reboot’)));return body;},handleReboot:function(ev){return callReboot().then(function(res){if(res!=0){L.ui.addNotification(null,E(‘p’,(‘The reboot command failed with code %d’).format(res)));L.raise(‘Error’,‘Reboot failed’);} L.ui.showModal((‘Rebooting…’),[E(‘p’,{‘class’:‘spinning’},(‘Waiting for device…’))]);window.setTimeout(function(){L.ui.showModal((‘Rebooting…’),[E(‘p’,{‘class’:‘spinning alert-message warning’},_(‘Device unreachable! Still waiting for device…’))]);},150000);L.ui.awaitReconnect();}).catch(function(e){L.ui.addNotification(null,E(‘p’,e.message))});},handleSaveApply:null,handleSave:null,handleReset:null});

The updated code to place the Power Off button:

'use strict';
'require view';
'require rpc';
'require ui';
'require uci';
'require fs';

var callReboot = rpc.declare({
	object: 'system',
	method: 'reboot',
	expect: { result: 0 }
});

return view.extend({
	load: function() {
		return uci.changes();
	},

	render: function(changes) {
		var body = E([
			E('h2', _('Reboot / Power Off')),
			E('p', {}, _('Reboots or gracefully halts the router operating system.'))
		]);

		for (var config in (changes || {})) {
			body.appendChild(E('p', { 'class': 'alert-message warning' }, _('Warning: There are unsaved changes that will get lost.')));
			break;
		}

		body.appendChild(E('hr'));

		body.appendChild(E('button', {
			'class': 'cbi-button cbi-button-action important',
			'style': 'margin-right: 10px;',
			'click': ui.createHandlerFn(this, 'handleReboot')
		}, _('Perform reboot')));

		body.appendChild(E('button', {
			'class': 'cbi-button cbi-button-reset important',
			'style': 'background-color: #cc0000; color: white;',
			'click': ui.createHandlerFn(this, 'handlePowerOff')
		}, _('Power Off')));

		return body;
	},

	handleReboot: function(ev) {
		return callReboot().then(function(res) {
			if (res != 0) {
				L.ui.addNotification(null, E('p', _('The reboot command failed with code %d').format(res)));
				L.raise('Error', 'Reboot failed');
			}
			L.ui.showModal(_('Rebooting...'), [ E('p', { 'class': 'spinning' }, _('Waiting for device...')) ]);
			window.setTimeout(function() {
				L.ui.showModal(_('Rebooting...'), [ E('p', { 'class': 'spinning alert-message warning' }, _('Device unreachable. Still waiting for device...')) ]);
			}, 150000);
			L.ui.awaitReconnect();
		}).catch(function(e) {
			L.ui.addNotification(null, E('p', e.message))
		});
	},

	// Runs prep-unplug and suppresses the expected network disconnection error
	handlePowerOff: function(ev) {
		L.ui.showModal(_('Shutting Down...'), [ 
			E('p', {}, _('Isolating WAN traffic and flushing filesystems...')),
			E('p', { 'class': 'alert-message warning', 'style': 'margin-top: 10px;' }, _('When the router green LED turns off and the red LED flashes, power down the router.'))
		]);
		
		// Remove the .catch() notification - the network link is supposed to break
		fs.exec('/etc/prep-unplug');
		return Promise.resolve();
	},

	handleSaveApply: null,
	handleSave: null,
	handleReset: null
});

Again, if your original code varies from the above, think twice about applying this change!