Graceful Router Power Down

{ EDIT: Updated the script to enable PCIe Link Power Management. The NVMe ‘Unsafe Shutdown’ counter no longer increments during a Power Off. }

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

smartctl --all /dev/nvme0

SMART/Health Information (NVMe Log 0x02, NSID 0xffffffff)
Critical Warning:                   0x00
Temperature:                        47 Celsius
Available Spare:                    100%
Available Spare Threshold:          10%
Percentage Used:                    0%
Data Units Read:                    1,080,727 [553 GB]
Data Units Written:                 109,464 [56.0 GB]
Host Read Commands:                 4,728,544
Host Write Commands:                618,993
Controller Busy Time:               0
Power Cycles:                       23
Power On Hours:                     16
Unsafe Shutdowns:                   9 <=<=<=<=<=< THIS
Media and Data Integrity Errors:    0
Error Information Log Entries:      0
Warning  Comp. Temperature Time:    0
Critical Comp. Temperature Time:    0
Temperature Sensor 1:               47 Celsius
Temperature Sensor 2:               62 Celsius

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 "Shutting down router... please wait..."
service log stop 2>/dev/null

ip link set eth0 down 2>/dev/null
ip link set eth1 down 2>/dev/null

if [ -d /sys/class/leds ]; then
    for led in /sys/class/leds/*; do
        echo none > "$led/trigger" 2>/dev/null
        echo 0 > "$led/brightness" 2>/dev/null
    done
fi

block umount 2>/dev/null
sync
sleep 5
sync
sleep 5

# Cut power to the software layer first
echo 1 > /sys/bus/pci/devices/0000:01:00.0/remove 2>/dev/null
sync
# Cut physical slot power
echo "auto" > /sys/bus/pci/devices/0000:00:00.0/power/control 2>/dev/null
sleep 2

if [ -f /sys/class/gpio/watchdog-enable/value ]; then
    echo 0 > /sys/class/gpio/watchdog-enable/value 2>/dev/null
fi

sleep 20
echo "When the router LEDs go dark, it is safe to pull the power."
while true; do
    sleep 1
done

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

Make that file executable:

chmod +x /etc/prep-unplug

That stops the ‘Unsafe Shutdowns’ counter from incrementing, because the NVMe drive is synced and powered down when power to the router is pulled.

Next:

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.

Initialize the changes:

source /etc/profile

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.

Make it executable:

chmod +x /usr/sbin/poweroff

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:

/usr/sbin/poweroff

Ensure the system sees it:

type poweroff

… which should return:

poweroff is an alias for /etc/prep-unplug
1 Like

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', {}, _('Please wait...')),
			E('p', { 'class': 'alert-message warning', 'style': 'margin-top: 10px;' }, _('When the router LEDs go dark, 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
});

Make it impervious to firmware updates: Under System >> Backup / Flash Firmware >> Configuration, enter:

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

… in the list, and click the ‘Save’ button.

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

1 Like

There was a change in ATF that caused this behaviour. To undo this change apply:

This commit to ATF