├── README.md └── lazy /README.md: -------------------------------------------------------------------------------- 1 | # Lazy, a tool for running things in idle time 2 | 3 | Mostly used to stop CUDA ML model training from making my desktop 4 | unusable. Simply monitors keyboard/mouse idleness using `xprintidle`, 5 | and pauses the given process using `SIGSTOP` whenever the machine is 6 | in use (defined as no activity within the last second). 7 | 8 | Invoke either with a command, 9 | 10 | lazy ./train_my_gpu_intensive_model.py 11 | 12 | or with a PID of something already running. 13 | 14 | lazy -p 1234 15 | 16 | (In PID mode, `lazy` is likely to leave the process paused after 17 | exiting, if cancelled with ^C. Resume it manually with `kill -SIGCONT 18 | $PID`, or `fg` in the shell you originally started it in.) -------------------------------------------------------------------------------- /lazy: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | from subprocess import Popen, PIPE, TimeoutExpired 3 | import multiprocessing 4 | import time 5 | import os, sys 6 | import threading 7 | import argparse 8 | import signal 9 | import functools 10 | 11 | verbose = False 12 | 13 | def getidle(): 14 | proc = Popen(['xprintidle'], stdout=PIPE) 15 | try: 16 | proc.wait(timeout=0.1) # seconds 17 | except TimeoutExpired: 18 | if verbose: 19 | print(f'xprintidle: [TimeoutExpired]', file=sys.stderr) 20 | return 0 21 | val = int(proc.stdout.read()) 22 | if verbose: 23 | print(f'xprintidle: {val}', file=sys.stderr) 24 | return val 25 | 26 | def control_loop(pid): 27 | os.kill(pid, signal.SIGCONT) 28 | state = 'RUNNING' 29 | while True: 30 | t = getidle() 31 | if state == 'RUNNING' and t < 500: 32 | if verbose: 33 | print(f'kill -SIGSTOP {pid}', file=sys.stderr) 34 | os.kill(pid, signal.SIGSTOP) 35 | state = 'STOPPED' 36 | elif state == 'STOPPED' and t > 1000: 37 | if verbose: 38 | print(f'kill -SIGCONT {pid}', file=sys.stderr) 39 | os.kill(pid, signal.SIGCONT) 40 | state = 'RUNNING' 41 | time.sleep(0.050) 42 | 43 | def start(): 44 | """Import this module from your application and call start() to begin 45 | the watchdog from within python. 46 | 47 | Note that like in PID mode, the first SIGSTOP will background the 48 | command in bash (as if you pressed ^Z), which might be an 49 | inconvenience. 50 | 51 | """ 52 | pid = os.getpid() 53 | proc = multiprocessing.Process(target=control_loop, args=[pid]) 54 | proc.start() 55 | 56 | def main(): 57 | parser = argparse.ArgumentParser(description='Run a command only when idle.') 58 | parser.add_argument('command', type=str, nargs='*', help='command to execute') 59 | parser.add_argument('-p', '--pid', type=int, default=None, help='process id instead of command') 60 | parser.add_argument('-v', '--verbose', default=False, action='store_true') 61 | args = parser.parse_args() 62 | globals()['verbose'] = args.verbose 63 | if args.pid is not None: 64 | control_loop(args.pid) 65 | else: 66 | proc = Popen(args.command) 67 | threading.Thread(target=functools.partial(control_loop, proc.pid), daemon=True).start() 68 | proc.wait() 69 | 70 | if __name__ == '__main__': 71 | main() --------------------------------------------------------------------------------