Closer was born because I had trouble with killing up processes I set up remotely via SSH. That is, you want to run some SSH process in the background, and then you want to kill it, just like you would a local subprocess.
I couldn't find a good solution, so here's my take on it.
Closer has evolved to do more than just automatic remote process cleanup. Here are the main features:
- kill the remote process (either by choice, or automatically at the end of the calling process)
 - capture the remote process's output
 - live monitoring of remote process output
 - get a callback upon remote process' death
 
Do whatever you want with closer, but be kind enough to share your improvements with a pull request.
You must install closer both on your local machine and the remote machine:
$ pip install closer
- Again, 
closermust be installed on the remote machine for it to work. closeruses TCP communication with the remote process. Firewalls may blockcloser.
In this example we connect via SSH to a machine with IP 10.50.50.11 with a user called vagrant.
We run a bash shell that itself runs a sleep, not before echoing whatup to standard output.
After we quit the IPython process, the Remote object kills the remote process for us (because we specified cleanup=True.
$ ipython
Python 2.7.12+ (default, Sep 17 2016, 12:08:02) 
Type "copyright", "credits" or "license" for more information.
IPython 5.1.0 -- An enhanced Interactive Python.
?         -> Introduction and overview of IPython's features.
%quickref -> Quick reference.
help      -> Python's own help system.
object?   -> Details about 'object', use 'object??' for extra details.
In [1]: import closer.remote
In [2]: r = closer.remote.Remote( 'vagrant', '10.50.50.11', [ 'bash', '-c', 'echo whatup; sleep 1500;' ] )
In [3]: r.background(cleanup=True) # launches remote process in the background
whatup
In [4]: quit()  # remote process dies automatically - check it out on your own remote servercloser relies on atexit
If your process dies as a result of receiving SIGTERM, the atexit handler will not run.
closer provides a solution by allowing you to explicitly close all Remote processes:
    closer.remote.Remote.tidyUp()NOTE: tidyUp() will ONLY WORK for Remote objects that run with
.background(cleanup=True). If you did not specify cleanup=True it is false
by default.
To handle SIGTERM, e.g.:
import closer.remote
import signal
import sys
def handleSIGTERM( * args ):
    closer.remote.Remote.tidyUp()
    sys.exit( 1 )
signal.signal( signal.SIGTERM, handleSIGTERM )Use the .setCloserCommand(), e.g.
remoteObject = closer.remote.Remote( ... )
remoteObject.setCloserCommand( '/path/to/closer' )Here you go:
remoteObject.sshPort = SOME_OTHER_PORT
remoteObject.sshOptions( 'StrictHostKeyChecking=no' ) # this goes into the -o ssh flagThe Remote class also allows you to run processes synchronously, i.e. the following IPython session:
In [7]: r = closer.remote.Remote( 'vagrant', '10.50.50.11', [ 'ls', '-ltr', '/var' ] )
In [8]: r.foreground()
total 44
drwxrwsr-x  2 root staff  4096 Apr 10  2014 local
drwxr-xr-x  2 root root   4096 Apr 10  2014 backups
drwxr-xr-x  2 root root   4096 Feb  8 20:41 opt
drwxrwsr-x  2 root mail   4096 Feb  8 20:41 mail
lrwxrwxrwx  1 root root      4 Feb  8 20:41 run -> /run
lrwxrwxrwx  1 root root      9 Feb  8 20:41 lock -> /run/lock
drwxr-xr-x  5 root root   4096 Feb  8 20:42 spool
drwxrwxrwt  2 root root   4096 Feb  8 20:43 crash
drwxr-xr-x 11 root root   4096 Feb  8 21:35 cache
drwxr-xr-x 47 root root   4096 Feb  8 21:36 lib
drwxr-xr-x  3 root root   4096 Feb 12 20:22 chef
drwxrwxrwt  2 root root   4096 Feb 12 22:11 tmp
drwxrwxr-x 10 root syslog 4096 Feb 13 18:49 logAnd you can capture the output if you like:
In [6]: r = closer.remote.Remote( 'vagrant', '10.50.50.11', [ 'ls', '-ltr', '/var' ] )
In [7]: text = r.output()
In [8]: text.split('\n')
Out[8]: 
['total 44',
 'drwxrwsr-x  2 root staff  4096 Apr 10  2014 local',
 'drwxr-xr-x  2 root root   4096 Apr 10  2014 backups',
 'drwxr-xr-x  2 root root   4096 Feb  8 20:41 opt',
 'drwxrwsr-x  2 root mail   4096 Feb  8 20:41 mail',
 'lrwxrwxrwx  1 root root      4 Feb  8 20:41 run -> /run',
 'lrwxrwxrwx  1 root root      9 Feb  8 20:41 lock -> /run/lock',
 'drwxr-xr-x  5 root root   4096 Feb  8 20:42 spool',
 'drwxrwxrwt  2 root root   4096 Feb  8 20:43 crash',
 'drwxr-xr-x 11 root root   4096 Feb  8 21:35 cache',
 'drwxr-xr-x 47 root root   4096 Feb  8 21:36 lib',
 'drwxr-xr-x  3 root root   4096 Feb 12 20:22 chef',
 'drwxrwxrwt  2 root root   4096 Feb 12 22:11 tmp',
 'drwxrwxr-x 10 root syslog 4096 Feb 13 18:49 log',
 '']By default .foreground() will raise an exception if the process fails. You can disable this behaviour with .foreground( check = False ).
You can impose a timeout on the time it takes the remote process to end
remote = closer.remote.Remote( 'myUser', 'myHost', [ 'bash', '-c', 'echo hiThere; sleep 10;' ] )
remote.run( timeout = 3 )Since we sleep here 10 seconds, the timeout will go off, and run will raise an exception:
RemoteProcessTimeout: runtime exceeded 3 seconds for remote process: {'args': (['bash', '-c', 'echo hiThere; sleep 10;'],), 'kwargs': {}}
You can monitor a remote processes' output and death events using the liveMonitor method. Try this:
def onOutput( line ):
    print "got: {}".format( line )
def onProcessEnd( exitCode ):
    print "process died with exitCode={}".format( exitCode )
tested = closer.remote.Remote( 'my-user', 'my-host', "bash -c 'for i in 1 2 3 4 5 6 7 8 9 10; do echo $i; sleep 1; done; exit 7'", shell = True )
tested.liveMonitor( onOutput = onOutput, onProcessEnd = onProcessEnd, cleanup = True )
LET_PROCESS_DIE_NATURALLY = 12
time.sleep( LET_PROCESS_DIE_NATURALLY )The onOutput callback will be called for every line the remote process
produces on its standard output, and the onProcessEnd will be called when the
remote process exits.
closer works with Python 3 just fine, but there is a caveat. Assuming that the local host has the Python 3 closer installed:
- if the remote host has a Python 3 based closer - no problem
 - if the remote host has a Python 2 based closer, you must set the closer command like so:
 
remoteObject.setCloserCommand( '/path/to/closer' ) Otherwise, it will look for a local closer3 script and will not find one.
PYTHON 2 SUPPORT WILL BE DROPPED EVENTUALLY