Friday, April 18, 2014

A dead man's switch for Linux, in bash

Last night, I was working on a (very) remote physical machine, over ssh.
After modifying Debian's /etc/network/interfaces to include the definition of a new bridge, br0, and playing around with its settings, I was about to bring it down and the back up again for a final check. And this is what I actually typed:

# ifdown eth0

Which did not bring br0 down, but eth0, instead. I had brought down the physical Ethernet interface, the very one I was actually using for SSHing into the machine. So now what? Left with an unreachable machine, the only option was to call a friend and ask him to drive over and press the magical Reset button.

How can one defend from this or similar cases, which are the equivalent of sawing off the branch your're sitting on? Something like a dead man's switch: if you don't do something within a specified time limit, the machine assumes your connection is gone, and reboots.

Ferm has an interactive mode for exactly this purpose: After applying new firewall rules, it waits for a few seconds for the user to confirm that everything is OK. If nothing happens, it assumes the new firewall rules have caused the user to lock themselves out, so it reverts them. Similarly, Windows or GNOME will wait for confirmation after changes to a monitor's resolution and revert to the previous settings after a few seconds.

Here's an approach in bash: Make sure a reboot is always scheduled within 60 minutes. If something happens, e.g., your connection is gone and you can no longer get it back, the system will reboot. Otherwise, you can cancel the current shutdown operation, and reschedule a new one.

Add this to /root/.bashrc:

alias dead_man_switch="shutdown -c; shutdown -r +60 & disown"

Using disown ensures you cannot bring the shutdown process in the foreground accidentally and kill it. Once you run dead_man_switch, you have to re-run it at least every 60 minutes to ensure the system does not reboot itself.

Hope someone finds this useful!

Wednesday, July 4, 2012

Pyrasite infestation

The best thing about EuroPython 2012 is the chance to learn about great Python hacks like pyrasite.

Pyrasite is a debugging tool which uses gdb to attach to a running Python process so it can inject code dynamically into it.

To give it a try, first install it inside a virtual environment:

$ virtualenv ~/venv
$ source ~/venv/bin/activate
$ pip install pyrasite

Then create a simple Python program, let's say a.py, and run it:

#!/usr/bin/env python

import time
import sys

def f():
    while True:
        print "kalhmera"
        time.sleep(1)

def main():
    f()

if __name__ == "__main__":
    sys.exit(main())

Use a command like ps -ef|grep a.py to get its PID, let's assume its 15210, then
in a new terminal run:

$ source ~/venv/bin/activate
$ pyrasite-shell 15210
Pyrasite Shell 2.0
Connected to 'python ./a.py'
Python 2.6.6 (r266:84292, Dec 26 2010, 22:31:48)
[GCC 4.4.5] on linux2
Type "help", "copyright", "credits" or "license" for more information.
(DistantInteractiveConsole)

>>> import __main__
>>> import traceback
>>> __main__.__file__
'./a.py'

The shell you're seeing is actually running code inside the context of the running process!
You can do all sorts of nice stuff, like import new modules, inspect stack frames, take a look at variables, e.g. globals(), a number of great things come to mind.

Let's get a list of all running threads and their stack frames:

>>> for thread, frame in sys._current_frames().items():
...     print('Thread 0x%x' % thread)
...     traceback.print_stack(frame)
...     print()
...
Thread 0x7ffdc79af700
  File "/usr/lib/python2.6/threading.py", line 504, in __bootstrap
    self.__bootstrap_inner()
  File "/usr/lib/python2.6/threading.py", line 532, in __bootstrap_inner
    self.run()
  File "<string>", line 167, in run
  File "/usr/lib/python2.6/code.py", line 243, in interact
    more = self.push(line)
  File "/usr/lib/python2.6/code.py", line 265, in push
    more = self.runsource(source, self.filename)
  File "/usr/lib/python2.6/code.py", line 87, in runsource
    self.runcode(code)
  File "/usr/lib/python2.6/code.py", line 103, in runcode
    exec code in self.locals
  File "<console>", line 3, in <module>
()
Thread 0x7ffdc9172700
  File "./a.py", line 16, in <module>
    sys.exit(main())
  File "./a.py", line 13, in main
    f()
  File "./a.py", line 10, in f
    time.sleep(1)
()

The first thread is the thread created by pyrasite to infest the running process and get it under our control.

Under the hood, pyrasite uses gdb to attach to the running CPython process and inject code to fire up this new thread. This thread receives user requests over a socket connection and executes them inside the process itself.

For more information and examples of code to inject take a look at the pyrasite documentation, or explore the code itself, at ~/venv/lib/python2.6/site-packages/pyrasite/reverse.py for all the intriguing details.

Sunday, March 11, 2012

Use Caps Lock to switch input languages in Windows

AutoHotKey is a great piece of software to use, if you're on Windows and getting jealous of Xorg's grp:caps_toggle option under Linux.

AutoHotKey scripts can do magic, but for now, let's use a script to switch between English and Greek using Caps Lock.

An initial approach would be to reprogram Caps Lock to send Left Alt + Shift, the default language switch combination in Windows. However, I've also got Spanish enabled on my keyboard, and there doesn't seem to be a way to exclude a language when switching.

So here's an alternative approach, which:
  1. allows selecting each language individually, based on an Left Alt + Shift + number combination,
  2. enables switching between only a subset of the supported input languages.
Start by setting a different Left Alt + Left Shift + number combination for each language under Control Panel / Region and Language / Keyboards and Languages / Change Keyboards / Advanced Key Settings, and disable global Alt-Shift switching. When you're done, your settings should be similar to the following:

Between input languages: (None)
To English (united States) - US: Left Alt + Shift + 0
To Greek (Greece) - Greek: Left Alt + Shift +9
To Spanish (Spain, International Sort) - Spanish: Left Alt + Shift + 8

Since there's no keys to switch languages, an AHK script will have to remember the current input language, and switch to the other one directly. Here's a sample script, using a simple boolean to remember the state:

ingreek = False
CapsLock::
ingreek := not ingreek
OutputDebug, ingreek has become %ingreek%
if ingreek {
    OutputDebug, Sending Alt-Shift-9
    SendInput, {Lalt Down}{LShift Down}9{LShift Up}{LAlt Up}
} else {
    OutputDebug, Sending Alt-Shift-0
    SendInput, {Lalt Down}{LShift Down}0{LShift Up}{LAlt Up}
}

It's my first ever script, so please forgive any style errors :)

So there you have it: you can use Caps Lock to switch between English and Greek, or Left Alt + Shift + 8 to get into Spanish mode directly.

A debugging tip: The OutputDebug call sends output to the system debugger. Download a copy of  SysInternals' DebugView, run it with elevated privileges and toggle Capture / Capture Global Win32, to view the debugging output of the script.

Saturday, March 10, 2012

Have Pentadactyl call gVim for editing textboxes

Pentadactyl is a must-have addon for Firefox, which enables browsing with vim-like bindings. The best part of all: You can actually press Ctrl-I to have Pentadactyl spawn a full gVim while in any textbox.

Which means using gVim with web-based gmail is now possible :)

If Pentadactyl complains it cannot find gVim and you're on Windows, make sure to add C:\Program Files (x86)\Vim\vim73 to your %PATH%, by going to Computer Properties / Advanced System Settings / Environmental variables... and adding this directory to your user path.

Wednesday, March 7, 2012

Moving the Profile folder to a different location under Windows 7 [UPDATE: Now for Windows 10! ]

UPDATE: 2019-12-24: Verified to work for Windows 10 Pro.
UPDATE: 2019-12-24: Added information on image indexes, and troubleshooting guide.
UPDATE: 2019-12-24: Add direct link to pre-built Unattend.xml.

There are many reasons why one may want to move C:\Users and C:\ProgramData to a new location, preferably on a different disk, the most important being separation of OS data from user-specific data.

The most common methods proposed all over the web are:
  • Messing with Registry settings (e.g., changing keys under HKEY_LOCAL_MACHINE\SOFTWARE\MICROSOFT\WINDOWS NT\CurrentVersion\ProfileList)
  • Using robocopy to copy everything over, then make an NTFS junction pointing to the new location (e.g. mklink /j C:\Users D:\Users)
  • using an answer file (unattend.xml) when installing Windows, to change the FolderLocations setting, see http://technet.microsoft.com/en-us/library/cc749305(v=ws.10).aspx
Method 3 seems to be the best-supported, given there's a nice article in Microsoft's KB (http://support.microsoft.com/kb/949977) about it, and that Setup creates the elaborate set of Junctions needed for backwards compatibility in the new location automatically.

Still, after numerous tries, Setup seemed to ignore the answer file I was providing in the root of a connected USB stick. It doesn't matter Microsoft's documentation says otherwise: http://technet.microsoft.com/en-us/library/cc749415(v=ws.10).aspx, no matter what I tried Setup ignored my unattend.xml.

The only way I could make it work was by placing it in the root of the newly installed %SYSTEMDRIVE%, i.e., C:, so here are detailed steps on how to do that:

  1. Download the Windows Automated Installation Kit (WAIK), and install it on a second computer. Or just download a pre-built Unattend.xml and jump directly to step 3
  2. Create an answer file using the Windows System Image Manager containing the necessary entries for the FolderLocations setting, in the oobeSystem phase (both for x86 and amd64 architectures)
  3. Create a bootable USB drive for installing Windows 7
    1. Use DISKPART to completely clear it, create a primary partition, format it as NTFS and mark it as active
    2. Copy the full contents of the Windows 7 installation DVD in the newly-created NTFS
    3. I also had to run BOOTSECT /nt60 J: at this point, although I'm not sure it's necessary
  4. Modify sources\install.wim on the USB stick so that it contains your unattend.xml file as well
    1. [UPDATED] Based on the version of Windows you wish to install, note its index in the install image:
      DISM /Get-ImageInfo /ImageFile:J:\sources\install.wim
    2. Mount the image on a local directory, but replace its index at the /index argument, based on the version you want to install, see step (1) above:
    3. DISM /Mount-Wim /WimFile:J:\sources\install.wim /index:1 /Mountdir:C:\wim 
    4. COPY unattend.xml C:\wim
    5. Unmount the directory
      DISM /Unmount-Wim /MountDir:C:\wim /commit 
If all goes well, you should be able to install Windows 7 from the USB stick ([UPDATED] remember to create a and format a second NTFS partition during Setup!) and see the fruits of your labor:
C:\>echo %userprofile%
D:\Users\vangelis


The setup logs under C:\Windows\Panther\UnattendGC\setupact.txt will confirm your victory:


oobeldr.exe] Status for unattend pass [oobeSystem] = 0x0
[oobeldr.exe] UnattendSearchExplicitPath: Found unattend file at [C:\unattend.xml]; examining for applicability.
[oobeldr.exe] UnattendSearchExplicitPath: Found usable unattend file for pass [oobeSystem] at [C:\unattend.xml].

...
[Shell Unattend] Running 'oobeSystem' pass
[Shell Unattend] FolderLocations: Moved 'C:\Users' to 'D:\Users'
[Shell Unattend] FolderLocations: 'ProfilesDirectory' set to 'D:\Users'
[Shell Unattend] FolderLocations: 'Public' set to 'D:\Users\Public'
[Shell Unattend] FolderLocations: 'Default' set to 'D:\Users\Default'
[Shell Unattend] FolderLocations: 'ProfileImagePath' set to 'D:\Users\Administrator'
[Shell Unattend] FolderLocations: Moved 'C:\ProgramData' to 'D:\ProgramData'
[Shell Unattend] FolderLocations: 'ProgramData' set to 'D:\ProgramData'



Enjoy your user data on a separate partition from the rest of the OS.

I can't get it to work!

The latest version of Windows I have verified this procedure with is Windows 10 1903.
If something breaks, please check the following:
  • Are you sure you have updated the right image in install.wim? Double check that the index you specified at the DISM command line corresponds exactly to the version you are deploying.
  • Are you sure a D: drive exists during installation? Press Shift-F10 during setup and use DIR to explore all drives.
  • It could be that your USB stick itself appears as D: instead of your second partition, which may be at E:. If this is the case, you need to remove your installation medium at the first reboot, right after Setup has completed the initial copy of all files, before it reboots into your hard disk.