├── setup.py ├── LICENSE ├── README.md ├── .gitignore └── spacebarheating.py /setup.py: -------------------------------------------------------------------------------- 1 | import setuptools 2 | import spacebarheating 3 | 4 | with open("README.md", "r") as f: 5 | long_description = f.read() 6 | 7 | setuptools.setup( 8 | name="spacebarheating", 9 | version=spacebarheating.VERSION, 10 | description="Heat up your CPU by holding the spacebar, inspired by https://xkcd.com/1172/", 11 | long_description=long_description, 12 | long_description_content_type="text/markdown", 13 | url="https://github.com/elesiuta/spacebarheating", 14 | license="MIT", 15 | py_modules=["spacebarheating"], 16 | entry_points={"console_scripts": ["spacebarheating = spacebarheating:cli"]}, 17 | install_requires=["keyboard"], 18 | classifiers=[ 19 | "Programming Language :: Python :: 3", 20 | "License :: OSI Approved :: MIT License", 21 | "Topic :: Utilities" 22 | ], 23 | ) 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Eric Lesiuta 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Spacebar Heating 2 | A stupid script inspired by https://xkcd.com/1172/ 3 | ![https://xkcd.com/1172/](https://imgs.xkcd.com/comics/workflow.png "There are probably children out there holding down spacebar to stay warm in the winter! YOUR UPDATE MURDERS CHILDREN.") 4 | I never thought I'd have an actual use case for this, but here I am, fixes the LCD on my laptop which displays vertical lines when the temperature is cold. 5 | - clone the repo and install with 6 | `python setup.py install --user` 7 | - you may need to add [userbase](https://docs.python.org/3/library/site.html#site.USER_BASE) to PATH on Windows (or just install without `--user`, or run via `python -m spacebarheating`) 8 | - uninstall with `pip uninstall spacebarheating` 9 | - then start with 10 | `spacebarheating start` 11 | - holding spacebar for 2.5 seconds will now max out your CPU usage until you release it 12 | - you can stop the background process with 13 | `spacebarheating stop` 14 | - or you can just run it once for 10 seconds then exit, no *spacebar* required 15 | `spacebarheating once` 16 | - you can also run the python script directly without installing via 17 | `python spacebarheating.py start|stop|restart|once|version` 18 | - depends on installing https://pypi.org/project/keyboard/ 19 | 20 | This software comes with ABSOLUTELY NO WARRANTY. 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # ---> Python 2 | # Byte-compiled / optimized / DLL files 3 | __pycache__/ 4 | *.py[cod] 5 | *$py.class 6 | 7 | # C extensions 8 | *.so 9 | 10 | # Distribution / packaging 11 | .Python 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | wheels/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | cover/ 54 | 55 | # Translations 56 | *.mo 57 | *.pot 58 | 59 | # Django stuff: 60 | *.log 61 | local_settings.py 62 | db.sqlite3 63 | db.sqlite3-journal 64 | 65 | # Flask stuff: 66 | instance/ 67 | .webassets-cache 68 | 69 | # Scrapy stuff: 70 | .scrapy 71 | 72 | # Sphinx documentation 73 | docs/_build/ 74 | 75 | # PyBuilder 76 | .pybuilder/ 77 | target/ 78 | 79 | # Jupyter Notebook 80 | .ipynb_checkpoints 81 | 82 | # IPython 83 | profile_default/ 84 | ipython_config.py 85 | 86 | # pyenv 87 | # For a library or package, you might want to ignore these files since the code is 88 | # intended to run in multiple environments; otherwise, check them in: 89 | # .python-version 90 | 91 | # pipenv 92 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 93 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 94 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 95 | # install all needed dependencies. 96 | #Pipfile.lock 97 | 98 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 99 | __pypackages__/ 100 | 101 | # Celery stuff 102 | celerybeat-schedule 103 | celerybeat.pid 104 | 105 | # SageMath parsed files 106 | *.sage.py 107 | 108 | # Environments 109 | .env 110 | .venv 111 | env/ 112 | venv/ 113 | ENV/ 114 | env.bak/ 115 | venv.bak/ 116 | 117 | # Spyder project settings 118 | .spyderproject 119 | .spyproject 120 | 121 | # Rope project settings 122 | .ropeproject 123 | 124 | # mkdocs documentation 125 | /site 126 | 127 | # mypy 128 | .mypy_cache/ 129 | .dmypy.json 130 | dmypy.json 131 | 132 | # Pyre type checker 133 | .pyre/ 134 | 135 | # pytype static type analyzer 136 | .pytype/ 137 | 138 | # Cython debug symbols 139 | cython_debug/ 140 | 141 | -------------------------------------------------------------------------------- /spacebarheating.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # spacebarheating inspired by https://xkcd.com/1172/ 3 | # never thought I'd have an actual use case for this, but here I am 4 | # fixes the lcd on my laptop which displays vertical lines when the temperature is cold 5 | # https://github.com/elesiuta/spacebarheating 6 | 7 | # Permission is hereby granted, free of charge, to any person obtaining a copy 8 | # of this software and associated documentation files (the "Software"), to deal 9 | # in the Software without restriction, including without limitation the rights 10 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | # copies of the Software, and to permit persons to whom the Software is 12 | # furnished to do so, subject to the following conditions: 13 | 14 | # The above copyright notice and this permission notice shall be included in all 15 | # copies or substantial portions of the Software. 16 | 17 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | # SOFTWARE. 24 | 25 | import importlib 26 | import multiprocessing 27 | import os 28 | import signal 29 | import subprocess 30 | import sys 31 | import threading 32 | import time 33 | 34 | import keyboard 35 | 36 | PIDFILE = os.path.join(os.path.expanduser("~"), ".config", "spacebarheating.pid") 37 | VERSION = "10.17.1" 38 | 39 | 40 | def heater(): 41 | """a very basic stress test""" 42 | signal.signal(signal.SIGTERM, lambda *args: sys.exit(0)) 43 | while multiprocessing.parent_process().is_alive() and (os.name == "nt" or keyboard.is_pressed("space")): 44 | for i in range(1, 1000): 45 | _ = 1/i**0.5 46 | sys.exit(0) 47 | 48 | 49 | def timed_heater(_): 50 | """a simpler heater, runs for 10 seconds then exits, no 'spacebar' required""" 51 | start_time = time.time() 52 | while time.time() < start_time + 10: 53 | for i in range(1, 1000): 54 | _ = 1/i**0.5 55 | 56 | 57 | def heater_hook(key_event: keyboard.KeyboardEvent) -> None: 58 | """hook to start and cleanup heaters""" 59 | if key_event.event_type == keyboard.KEY_DOWN: 60 | # make sure key is held for 2.5 seconds before starting 61 | for i in range(5): 62 | time.sleep(0.5) 63 | if keyboard.is_pressed("space"): 64 | continue 65 | return 66 | # stress cpu 67 | processes: list[multiprocessing.Process] = [] 68 | for i in range(multiprocessing.cpu_count()): 69 | processes.append(multiprocessing.Process(name="spacebarheater", target=heater, daemon=True)) 70 | processes[-1].start() 71 | while keyboard.is_pressed("space"): 72 | time.sleep(0.1) 73 | # cleanup 74 | for process in processes: 75 | process.terminate() 76 | process.join() 77 | process.close() 78 | return 79 | 80 | 81 | def start() -> int: 82 | """main function (registers hooks and waits)""" 83 | assert not os.path.exists(PIDFILE) 84 | os.makedirs(os.path.dirname(PIDFILE), exist_ok=True) 85 | with open(PIDFILE, "w") as f: 86 | f.write(str(os.getpid()) + "\n") 87 | forever = threading.Event() 88 | signal.signal(signal.SIGINT, lambda *args: forever.set()) 89 | signal.signal(signal.SIGTERM, lambda *args: forever.set()) 90 | keyboard.hook_key("space", heater_hook) 91 | forever.wait() 92 | keyboard.unhook_key("space") 93 | os.remove(PIDFILE) 94 | return 0 95 | 96 | 97 | def stop() -> int: 98 | """terminate running instance and make sure pidfile was removed""" 99 | try: 100 | with open(PIDFILE, "r") as f: 101 | pid = int(f.read().strip()) 102 | os.kill(pid, signal.SIGINT) 103 | time.sleep(0.1) 104 | os.kill(pid, signal.SIGTERM) 105 | time.sleep(0.1) 106 | os.kill(pid, signal.SIGKILL) 107 | assert not os.path.exists(PIDFILE) 108 | print("stopped spacebarheating") 109 | except FileNotFoundError: 110 | print(f"pidfile {PIDFILE} does not exist.\nspacebarheating is not currently running?", file=sys.stderr) 111 | return 1 112 | except ProcessLookupError: 113 | print(f"pidfile {PIDFILE} still exists but spacebarheating does not appear to be running, removing pidfile") 114 | os.remove(PIDFILE) 115 | except AssertionError: 116 | print(f"Error: pidfile {PIDFILE} still exists after attempting to stop process, process hanging or pid recycled?", file=sys.stderr) 117 | return 1 118 | except OSError: 119 | if os.name == "nt" and os.path.exists(PIDFILE): 120 | print("stopping spacebarheating...") 121 | os.remove(PIDFILE) 122 | else: 123 | print("I AM ERROR.", file=sys.stderr) 124 | return 0 125 | 126 | 127 | def cli() -> int: 128 | """command line interface and initialization""" 129 | # help/usage message for any invalid flags 130 | if len(sys.argv) <= 1 or sys.argv[1] not in ["start", "stop", "restart", "once", "version"]: 131 | print("usage: spacebarheating start|stop|restart|once|version\n\n" 132 | "This software comes with ABSOLUTELY NO WARRANTY. This is free software, and\n" 133 | "you are welcome to redistribute it. See the MIT License for details.") 134 | return 2 135 | # run once for 10 seconds and exit (before testing keyboard hooks so root not required either) 136 | if sys.argv[1] == "once": 137 | with multiprocessing.Pool(processes=multiprocessing.cpu_count()) as pool: 138 | _ = pool.map(timed_heater, range(multiprocessing.cpu_count())) 139 | return 0 140 | # test keyboard hooks (needs root on linux) 141 | try: 142 | keyboard.hook_key("space", lambda x: None) 143 | keyboard.unhook_key("space") 144 | except Exception as e: 145 | print(type(e).__name__ + str(e.args), file=sys.stderr) 146 | if os.name == "posix" and os.getuid() != 0: 147 | print("Attempting to re-run spacebarheating as root, requesting root privileges") 148 | if importlib.util.find_spec("spacebarheating"): 149 | args = ["sudo", "-E", "python3", "-m", "spacebarheating", sys.argv[1]] 150 | else: 151 | args = ["sudo", "-E", sys.executable] + sys.argv 152 | os.execvp("sudo", args) 153 | else: 154 | print("Error: could not register keyboard hooks", file=sys.stderr) 155 | return 1 156 | # command line interface 157 | if sys.argv[1] == "restart": 158 | stop() 159 | if sys.argv[1] in ["start", "restart"]: 160 | if os.path.exists(PIDFILE): 161 | print(f"pidfile {PIDFILE} already exists.\nspacebarheating is currently running?", file=sys.stderr) 162 | return 1 163 | elif sys.argv[1] == "stop": 164 | return stop() 165 | else: 166 | print(VERSION) 167 | return 0 168 | # detach from terminal and start 169 | print("starting spacebarheating") 170 | if os.name == "posix": 171 | if os.isatty(0): 172 | if importlib.util.find_spec("spacebarheating"): 173 | args = ["bash", "-c", "sudo -E nohup python3 -m spacebarheating %s > /dev/null 2>&1 &" % sys.argv[1]] 174 | else: 175 | args = ["bash", "-c", "sudo -E nohup %s %s > /dev/null 2>&1 &" % (sys.executable, " ".join(sys.argv))] 176 | os.execvp("bash", args) 177 | elif os.name == "nt": 178 | if "pythonw" not in sys.executable and "python.exe" in sys.executable: 179 | subprocess.Popen([sys.executable.replace("python.exe", "pythonw.exe"), "-m", "spacebarheating", sys.argv[1]]) 180 | return 0 181 | else: 182 | raise Exception("Unsupported Platform") 183 | return start() 184 | 185 | 186 | if __name__ == "__main__": 187 | sys.exit(cli()) 188 | --------------------------------------------------------------------------------