Created
July 11, 2019 14:33
-
-
Save dkw72n/949cd52950737c629e25e424a5ec4fa4 to your computer and use it in GitHub Desktop.
stop windows from performing automatic restarts after installing updates by constantly setting the active hours
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import ctypes | |
import ctypes.wintypes | |
import locale | |
import sys | |
import threading | |
import uuid | |
import os | |
RegisterWindowMessage = ctypes.windll.user32.RegisterWindowMessageA | |
LoadCursor = ctypes.windll.user32.LoadCursorA | |
LoadIcon = ctypes.windll.user32.LoadIconA | |
LoadImage = ctypes.windll.user32.LoadImageA | |
RegisterClass = ctypes.windll.user32.RegisterClassA | |
CreateWindowEx = ctypes.windll.user32.CreateWindowExA | |
UpdateWindow = ctypes.windll.user32.UpdateWindow | |
DefWindowProc = ctypes.windll.user32.DefWindowProcA | |
GetSystemMetrics = ctypes.windll.user32.GetSystemMetrics | |
InsertMenuItem = ctypes.windll.user32.InsertMenuItemA | |
PostMessage = ctypes.windll.user32.PostMessageA | |
PostQuitMessage = ctypes.windll.user32.PostQuitMessage | |
SetMenuDefaultItem = ctypes.windll.user32.SetMenuDefaultItem | |
GetCursorPos = ctypes.windll.user32.GetCursorPos | |
SetForegroundWindow = ctypes.windll.user32.SetForegroundWindow | |
TrackPopupMenu = ctypes.windll.user32.TrackPopupMenu | |
CreatePopupMenu = ctypes.windll.user32.CreatePopupMenu | |
CreateCompatibleDC = ctypes.windll.gdi32.CreateCompatibleDC | |
GetDC = ctypes.windll.user32.GetDC | |
CreateCompatibleBitmap = ctypes.windll.gdi32.CreateCompatibleBitmap | |
GetSysColorBrush = ctypes.windll.user32.GetSysColorBrush | |
FillRect = ctypes.windll.user32.FillRect | |
DrawIconEx = ctypes.windll.user32.DrawIconEx | |
SelectObject = ctypes.windll.gdi32.SelectObject | |
DeleteDC = ctypes.windll.gdi32.DeleteDC | |
DestroyWindow = ctypes.windll.user32.DestroyWindow | |
GetModuleHandle = ctypes.windll.kernel32.GetModuleHandleA | |
GetMessage = ctypes.windll.user32.GetMessageA | |
TranslateMessage = ctypes.windll.user32.TranslateMessage | |
DispatchMessage = ctypes.windll.user32.DispatchMessageA | |
Shell_NotifyIcon = ctypes.windll.shell32.Shell_NotifyIcon | |
DestroyIcon = ctypes.windll.user32.DestroyIcon | |
NIM_ADD = 0 | |
NIM_MODIFY = 1 | |
NIM_DELETE = 2 | |
NIF_ICON = 2 | |
NIF_MESSAGE = 1 | |
NIF_TIP = 4 | |
MIIM_ID = 2 | |
MIIM_SUBMENU = 4 | |
MIIM_STRING = 64 | |
MIIM_BITMAP = 128 | |
WM_DESTROY = 2 | |
WM_CLOSE = 16 | |
WM_COMMAND = 273 | |
WM_USER = 1024 | |
WM_LBUTTONDBLCLK = 515 | |
WM_RBUTTONUP = 517 | |
WM_LBUTTONUP = 514 | |
WM_NULL = 0 | |
CS_VREDRAW = 1 | |
CS_HREDRAW = 2 | |
IDC_ARROW = 32512 | |
COLOR_WINDOW = 5 | |
WS_OVERLAPPED = 0 | |
WS_SYSMENU = 524288 | |
CW_USEDEFAULT = -2147483648 | |
LR_LOADFROMFILE = 16 | |
LR_DEFAULTSIZE = 64 | |
IMAGE_ICON = 1 | |
IDI_APPLICATION = 32512 | |
TPM_LEFTALIGN = 0 | |
SM_CXSMICON = 49 | |
SM_CYSMICON = 50 | |
COLOR_MENU = 4 | |
DI_NORMAL = 3 | |
WPARAM = ctypes.wintypes.WPARAM | |
LPARAM = ctypes.wintypes.LPARAM | |
HANDLE = ctypes.wintypes.HANDLE | |
if ctypes.sizeof(ctypes.c_long) == ctypes.sizeof(ctypes.c_void_p): | |
LRESULT = ctypes.c_long | |
elif ctypes.sizeof(ctypes.c_longlong) == ctypes.sizeof(ctypes.c_void_p): | |
LRESULT = ctypes.c_longlong | |
SZTIP_MAX_LENGTH = 128 | |
LOCALE_ENCODING = locale.getpreferredencoding() | |
def encode_for_locale(s): | |
""" | |
Encode text items for system locale. If encoding fails, fall back to ASCII. | |
""" | |
try: | |
return s.encode(LOCALE_ENCODING, 'ignore') | |
except (AttributeError, UnicodeDecodeError): | |
return s.decode('ascii', 'ignore').encode(LOCALE_ENCODING) | |
POINT = ctypes.wintypes.POINT | |
RECT = ctypes.wintypes.RECT | |
MSG = ctypes.wintypes.MSG | |
LPFN_WNDPROC = ctypes.CFUNCTYPE(LRESULT, HANDLE, ctypes.c_uint, WPARAM, LPARAM) | |
class WNDCLASS(ctypes.Structure): | |
_fields_ = [("style", ctypes.c_uint), | |
("lpfnWndProc", LPFN_WNDPROC), | |
("cbClsExtra", ctypes.c_int), | |
("cbWndExtra", ctypes.c_int), | |
("hInstance", HANDLE), | |
("hIcon", HANDLE), | |
("hCursor", HANDLE), | |
("hbrBackground", HANDLE), | |
("lpszMenuName", ctypes.c_char_p), | |
("lpszClassName", ctypes.c_char_p), | |
] | |
class MENUITEMINFO(ctypes.Structure): | |
_fields_ = [("cbSize", ctypes.c_uint), | |
("fMask", ctypes.c_uint), | |
("fType", ctypes.c_uint), | |
("fState", ctypes.c_uint), | |
("wID", ctypes.c_uint), | |
("hSubMenu", HANDLE), | |
("hbmpChecked", HANDLE), | |
("hbmpUnchecked", HANDLE), | |
("dwItemData", ctypes.c_void_p), | |
("dwTypeData", ctypes.c_char_p), | |
("cch", ctypes.c_uint), | |
("hbmpItem", HANDLE), | |
] | |
class NOTIFYICONDATA(ctypes.Structure): | |
_fields_ = [("cbSize", ctypes.c_uint), | |
("hWnd", HANDLE), | |
("uID", ctypes.c_uint), | |
("uFlags", ctypes.c_uint), | |
("uCallbackMessage", ctypes.c_uint), | |
("hIcon", HANDLE), | |
("szTip", ctypes.c_char * SZTIP_MAX_LENGTH), | |
("dwState", ctypes.c_uint), | |
("dwStateMask", ctypes.c_uint), | |
("szInfo", ctypes.c_char * 256), | |
("uTimeout", ctypes.c_uint), | |
("szInfoTitle", ctypes.c_char * 64), | |
("dwInfoFlags", ctypes.c_uint), | |
("guidItem", ctypes.c_char * 16), | |
] | |
if sys.getwindowsversion().major >= 5: | |
_fields_.append(("hBalloonIcon", HANDLE)) | |
def PackMENUITEMINFO(text=None, hbmpItem=None, wID=None, hSubMenu=None): | |
res = MENUITEMINFO() | |
res.cbSize = ctypes.sizeof(res) | |
res.fMask = 0 | |
if hbmpItem is not None: | |
res.fMask |= MIIM_BITMAP | |
res.hbmpItem = hbmpItem | |
if wID is not None: | |
res.fMask |= MIIM_ID | |
res.wID = wID | |
if text is not None: | |
text = encode_for_locale(text) | |
res.fMask |= MIIM_STRING | |
res.dwTypeData = text | |
if hSubMenu is not None: | |
res.fMask |= MIIM_SUBMENU | |
res.hSubMenu = hSubMenu | |
return res | |
def LOWORD(w): | |
return w & 0xFFFF | |
def PumpMessages(): | |
msg = MSG() | |
while GetMessage(ctypes.byref(msg), None, 0, 0) > 0: | |
TranslateMessage(ctypes.byref(msg)) | |
DispatchMessage(ctypes.byref(msg)) | |
def NotifyData(hWnd=0, uID=0, uFlags=0, uCallbackMessage=0, hIcon=0, szTip=""): | |
szTip = encode_for_locale(szTip)[:SZTIP_MAX_LENGTH] | |
res = NOTIFYICONDATA() | |
res.cbSize = ctypes.sizeof(res) | |
res.hWnd = hWnd | |
res.uID = uID | |
res.uFlags = uFlags | |
res.uCallbackMessage = uCallbackMessage | |
res.hIcon = hIcon | |
res.szTip = szTip | |
return res | |
class SysTrayIcon(object): | |
""" | |
menu_options: tuple of tuples (menu text, menu icon path or None, function name) | |
menu text and tray hover text should be Unicode | |
hover_text length is limited to 128; longer text will be truncated | |
Can be used as context manager to enable automatic termination of tray | |
if parent thread is closed: | |
with SysTrayIcon(icon, hover_text) as systray: | |
for item in ['item1', 'item2', 'item3']: | |
systray.update(hover_text=item) | |
do_something(item) | |
""" | |
QUIT = 'QUIT' | |
SPECIAL_ACTIONS = [QUIT] | |
FIRST_ID = 1023 | |
def __init__(self, | |
icon, | |
hover_text, | |
menu_options=None, | |
on_quit=None, | |
default_menu_index=None, | |
window_class_name=None): | |
self._icon = icon | |
self._icon_shared = False | |
self._hover_text = hover_text | |
self._on_quit = on_quit | |
menu_options = menu_options or () | |
menu_options = menu_options + (('Quit', None, SysTrayIcon.QUIT),) | |
self._next_action_id = SysTrayIcon.FIRST_ID | |
self._menu_actions_by_id = set() | |
self._menu_options = self._add_ids_to_menu_options(list(menu_options)) | |
self._menu_actions_by_id = dict(self._menu_actions_by_id) | |
window_class_name = window_class_name or ("SysTrayIconPy-%s" % (str(uuid.uuid4()))) | |
self._default_menu_index = (default_menu_index or 0) | |
self._window_class_name = encode_for_locale(window_class_name) | |
self._message_dict = {RegisterWindowMessage("TaskbarCreated"): self._restart, | |
WM_DESTROY: self._destroy, | |
WM_CLOSE: self._destroy, | |
WM_COMMAND: self._command, | |
WM_USER+20: self._notify} | |
self._notify_id = None | |
self._message_loop_thread = None | |
self._hwnd = None | |
self._hicon = 0 | |
self._hinst = None | |
self._window_class = None | |
self._menu = None | |
self._register_class() | |
def __enter__(self): | |
"""Context manager so SysTray can automatically close""" | |
self.start() | |
return self | |
def __exit__(self, *args): | |
"""Context manager so SysTray can automatically close""" | |
self.shutdown() | |
def WndProc(self, hwnd, msg, wparam, lparam): | |
hwnd = HANDLE(hwnd) | |
wparam = WPARAM(wparam) | |
lparam = LPARAM(lparam) | |
if msg in self._message_dict: | |
self._message_dict[msg](hwnd, msg, wparam.value, lparam.value) | |
return DefWindowProc(hwnd, msg, wparam, lparam) | |
def _register_class(self): | |
# Register the Window class. | |
self._window_class = WNDCLASS() | |
self._hinst = self._window_class.hInstance = GetModuleHandle(None) | |
self._window_class.lpszClassName = self._window_class_name | |
self._window_class.style = CS_VREDRAW | CS_HREDRAW | |
self._window_class.hCursor = LoadCursor(0, IDC_ARROW) | |
self._window_class.hbrBackground = COLOR_WINDOW | |
self._window_class.lpfnWndProc = LPFN_WNDPROC(self.WndProc) | |
RegisterClass(ctypes.byref(self._window_class)) | |
def _create_window(self): | |
style = WS_OVERLAPPED | WS_SYSMENU | |
self._hwnd = CreateWindowEx(0, self._window_class_name, | |
self._window_class_name, | |
style, | |
0, | |
0, | |
CW_USEDEFAULT, | |
CW_USEDEFAULT, | |
0, | |
0, | |
self._hinst, | |
None) | |
UpdateWindow(self._hwnd) | |
self._refresh_icon() | |
def _message_loop_func(self): | |
self._create_window() | |
PumpMessages() | |
def start(self): | |
if self._hwnd: | |
return # already started | |
self._message_loop_thread = threading.Thread(target=self._message_loop_func) | |
self._message_loop_thread.start() | |
def shutdown(self): | |
if not self._hwnd: | |
return # not started | |
PostMessage(self._hwnd, WM_CLOSE, 0, 0) | |
self._message_loop_thread.join() | |
def update(self, icon=None, hover_text=None): | |
""" update icon image and/or hover text """ | |
if icon: | |
self._icon = icon | |
self._load_icon() | |
if hover_text: | |
self._hover_text = hover_text | |
self._refresh_icon() | |
def _add_ids_to_menu_options(self, menu_options): | |
result = [] | |
for menu_option in menu_options: | |
option_text, option_icon, option_action = menu_option | |
if callable(option_action) or option_action in SysTrayIcon.SPECIAL_ACTIONS: | |
self._menu_actions_by_id.add((self._next_action_id, option_action)) | |
result.append(menu_option + (self._next_action_id,)) | |
elif non_string_iterable(option_action): | |
result.append((option_text, | |
option_icon, | |
self._add_ids_to_menu_options(option_action), | |
self._next_action_id)) | |
else: | |
raise Exception('Unknown item', option_text, option_icon, option_action) | |
self._next_action_id += 1 | |
return result | |
def _load_icon(self): | |
# release previous icon, if a custom one was loaded | |
# note: it's important *not* to release the icon if we loaded the default system icon (with | |
# the LoadIcon function) - this is why we assign self._hicon only if it was loaded using LoadImage | |
if not self._icon_shared and self._hicon != 0: | |
DestroyIcon(self._hicon) | |
self._hicon = 0 | |
# Try and find a custom icon | |
hicon = 0 | |
if self._icon is not None and os.path.isfile(self._icon): | |
icon_flags = LR_LOADFROMFILE | LR_DEFAULTSIZE | |
icon = encode_for_locale(self._icon) | |
hicon = self._hicon = LoadImage(0, icon, IMAGE_ICON, 0, 0, icon_flags) | |
self._icon_shared = False | |
# Can't find icon file - using default shared icon | |
if hicon == 0: | |
self._hicon = LoadIcon(0, IDI_APPLICATION) | |
self._icon_shared = True | |
self._icon = None | |
def _refresh_icon(self): | |
if self._hwnd is None: | |
return | |
if self._hicon == 0: | |
self._load_icon() | |
if self._notify_id: | |
message = NIM_MODIFY | |
else: | |
message = NIM_ADD | |
self._notify_id = NotifyData(self._hwnd, | |
0, | |
NIF_ICON | NIF_MESSAGE | NIF_TIP, | |
WM_USER+20, | |
self._hicon, | |
self._hover_text) | |
Shell_NotifyIcon(message, ctypes.byref(self._notify_id)) | |
def _restart(self, hwnd, msg, wparam, lparam): | |
self._refresh_icon() | |
def _destroy(self, hwnd, msg, wparam, lparam): | |
if self._on_quit: | |
self._on_quit(self) | |
nid = NotifyData(self._hwnd, 0) | |
Shell_NotifyIcon(NIM_DELETE, ctypes.byref(nid)) | |
PostQuitMessage(0) # Terminate the app. | |
# TODO * release self._menu with DestroyMenu and reset the memeber | |
# * release self._hicon with DestoryIcon and reset the member | |
# * release loaded menu icons (loaded in _load_menu_icon) with DeleteObject | |
# (we don't keep those objects anywhere now) | |
self._hwnd = None | |
self._notify_id = None | |
def _notify(self, hwnd, msg, wparam, lparam): | |
if lparam == WM_LBUTTONDBLCLK: | |
self._execute_menu_option(self._default_menu_index + SysTrayIcon.FIRST_ID) | |
elif lparam == WM_RBUTTONUP: | |
self._show_menu() | |
elif lparam == WM_LBUTTONUP: | |
pass | |
return True | |
def _show_menu(self): | |
if self._menu is None: | |
self._menu = CreatePopupMenu() | |
self._create_menu(self._menu, self._menu_options) | |
#SetMenuDefaultItem(self._menu, 1000, 0) | |
pos = POINT() | |
GetCursorPos(ctypes.byref(pos)) | |
# See http://msdn.microsoft.com/library/default.asp?url=/library/en-us/winui/menus_0hdi.asp | |
SetForegroundWindow(self._hwnd) | |
TrackPopupMenu(self._menu, | |
TPM_LEFTALIGN, | |
pos.x, | |
pos.y, | |
0, | |
self._hwnd, | |
None) | |
PostMessage(self._hwnd, WM_NULL, 0, 0) | |
def _create_menu(self, menu, menu_options): | |
for option_text, option_icon, option_action, option_id in menu_options[::-1]: | |
if option_icon: | |
option_icon = self._prep_menu_icon(option_icon) | |
if option_id in self._menu_actions_by_id: | |
item = PackMENUITEMINFO(text=option_text, | |
hbmpItem=option_icon, | |
wID=option_id) | |
InsertMenuItem(menu, 0, 1, ctypes.byref(item)) | |
else: | |
submenu = CreatePopupMenu() | |
self._create_menu(submenu, option_action) | |
item = PackMENUITEMINFO(text=option_text, | |
hbmpItem=option_icon, | |
hSubMenu=submenu) | |
InsertMenuItem(menu, 0, 1, ctypes.byref(item)) | |
def _prep_menu_icon(self, icon): | |
icon = encode_for_locale(icon) | |
# First load the icon. | |
ico_x = GetSystemMetrics(SM_CXSMICON) | |
ico_y = GetSystemMetrics(SM_CYSMICON) | |
hicon = LoadImage(0, icon, IMAGE_ICON, ico_x, ico_y, LR_LOADFROMFILE) | |
hdcBitmap = CreateCompatibleDC(None) | |
hdcScreen = GetDC(None) | |
hbm = CreateCompatibleBitmap(hdcScreen, ico_x, ico_y) | |
hbmOld = SelectObject(hdcBitmap, hbm) | |
# Fill the background. | |
brush = GetSysColorBrush(COLOR_MENU) | |
FillRect(hdcBitmap, ctypes.byref(RECT(0, 0, 16, 16)), brush) | |
# draw the icon | |
DrawIconEx(hdcBitmap, 0, 0, hicon, ico_x, ico_y, 0, 0, DI_NORMAL) | |
SelectObject(hdcBitmap, hbmOld) | |
# No need to free the brush | |
DeleteDC(hdcBitmap) | |
DestroyIcon(hicon) | |
return hbm | |
def _command(self, hwnd, msg, wparam, lparam): | |
id = LOWORD(wparam) | |
self._execute_menu_option(id) | |
def _execute_menu_option(self, id): | |
menu_action = self._menu_actions_by_id[id] | |
if menu_action == SysTrayIcon.QUIT: | |
DestroyWindow(self._hwnd) | |
else: | |
menu_action(self) | |
def non_string_iterable(obj): | |
try: | |
iter(obj) | |
except TypeError: | |
return False | |
else: | |
return not isinstance(obj, str) | |
from ctypes import windll, POINTER, byref, pointer | |
from ctypes import c_ushort, c_wchar_p, c_char_p, c_void_p, create_string_buffer, c_longlong, c_long, c_int, c_ulong | |
from ctypes.wintypes import HANDLE, DWORD, LPCWSTR, ULONG, LONG, LPCSTR | |
HKLM = 0x80000002 | |
RegOpenKeyExA = windll.advapi32.RegOpenKeyExA | |
RegOpenKeyExA.argtypes = ULONG, LPCSTR, DWORD, ULONG, POINTER(HANDLE) | |
RegOpenKeyExA.restype = LONG | |
RegCloseKey = windll.advapi32.RegCloseKey | |
RegCloseKey.argtypes = HANDLE, | |
RegCloseKey.restype = LONG | |
RegOpenKeyA = windll.advapi32.RegOpenKeyExA | |
RegOpenKeyA.argtypes = ULONG, LPCSTR, DWORD, ULONG, POINTER(HANDLE) | |
RegOpenKeyA.restype = LONG | |
RegQueryValueExA = windll.advapi32.RegQueryValueExA | |
RegQueryValueExA.argtypes = \ | |
HANDLE, LPCSTR, POINTER(DWORD), POINTER(DWORD), c_void_p, POINTER(DWORD) | |
RegQueryValueExA.restype = LONG | |
RegSetValueExA = windll.advapi32.RegSetValueExA | |
RegSetValueExA.argtypes = HANDLE, LPCSTR, DWORD, DWORD, c_void_p, DWORD | |
RegSetValueExA.restype = LONG | |
REG_DWORD = 4 | |
KEY_QUERY_VALUE = 0x0001 | |
KEY_SET_VALUE = 0x0002 | |
S_OK = 0 | |
def get_HKLM_DWORD_A(key, value): | |
hKey = HANDLE() | |
status = RegOpenKeyExA(HKLM, key, 0, KEY_QUERY_VALUE, pointer(hKey)) | |
assert status == S_OK, "Error(%d) in RegOpenKeyExA" % status | |
try: | |
_type = DWORD() | |
_data = DWORD() | |
_data_len = c_ulong(4) | |
status = RegQueryValueExA(hKey, value, None, pointer(_type), pointer(_data), pointer(_data_len)) | |
assert status == S_OK, "Error(%d) in RegQueryValueExA" % status | |
assert _type.value == REG_DWORD | |
return _data.value | |
finally: | |
RegCloseKey(hKey) | |
def set_HKLM_DWORD_A(key, value, data): | |
hKey = HANDLE() | |
status = RegOpenKeyExA(HKLM, key, 0, KEY_SET_VALUE, pointer(hKey)) | |
assert status == S_OK, "Error(%d) in RegOpenKeyExA" % status | |
try: | |
_type = DWORD(REG_DWORD) | |
_data = DWORD(data) | |
_data_len = c_ulong(4) | |
status = RegSetValueExA(hKey, value, 0, _type, pointer(_data), _data_len) | |
assert status == S_OK, "Error(%d) in RegSetValueExA" % status | |
return True | |
finally: | |
RegCloseKey(hKey) | |
def getCurrentHour(): | |
import datetime | |
return datetime.datetime.now().hour | |
WIN_UPDATE_SETTINGS = b"SOFTWARE\\Microsoft\\WindowsUpdate\\UX\\Settings" | |
ACTIVE_HOUR_START = b"ActiveHoursStart" | |
ACTIVE_HOUR_END = b"ActiveHoursEnd" | |
def getNewRange(hour): | |
return (hour + 23) % 24, (hour + 9) % 24 | |
def say_hello(systray): | |
print("Hello, World!") | |
""" | |
print("ActiveHoursStart: ", get_HKLM_DWORD_A(WIN_UPDATE_SETTINGS, ACTIVE_HOUR_START)) | |
print("ActiveHoursEnd: ", get_HKLM_DWORD_A(WIN_UPDATE_SETTINGS, ACTIVE_HOUR_END)) | |
print("SET ActiveHoursStart", set_HKLM_DWORD_A(WIN_UPDATE_SETTINGS, ACTIVE_HOUR_START, 8)) | |
print("ActiveHoursStart: ", get_HKLM_DWORD_A(WIN_UPDATE_SETTINGS, ACTIVE_HOUR_START)) | |
print("current", getCurrentHour()) | |
""" | |
for i in range(24): | |
print(getNewRange(i)) | |
def worker(systray): | |
import time | |
_start = -1 | |
#time.sleep(5) | |
while 1: | |
st, ed = getNewRange(getCurrentHour()) | |
if st != _start: | |
systray.update(hover_text = "current active hours: %02d:00 - %02d:00" % (st, ed)) | |
set_HKLM_DWORD_A(WIN_UPDATE_SETTINGS, ACTIVE_HOUR_START, st) | |
set_HKLM_DWORD_A(WIN_UPDATE_SETTINGS, ACTIVE_HOUR_END, ed) | |
_start = st | |
time.sleep(60) | |
import threading | |
#menu_options = (("Say Hello", None, say_hello),) | |
menu_options = () | |
with SysTrayIcon("icon.ico", "...", menu_options) as systray: | |
t = threading.Thread(target=worker, args=(systray,)) | |
t.setDaemon(True) | |
t.start() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment