fix errors kill s.
This commit is contained in:
parent
445bd65b4a
commit
b843160304
4 changed files with 244 additions and 20 deletions
|
|
@ -1,4 +1,4 @@
|
||||||
from core.Errors import MissingSubscriptionError, InvalidSubscriptionError, ConnectionUnprotectedError, EndpointVerificationError, ProfileStateConflictError
|
from core.Errors import MissingSubscriptionError, InvalidSubscriptionError, ConnectionUnprotectedError, EndpointVerificationError, ProfileStateConflictError, PaymentRequiredError
|
||||||
from core.controllers.ApplicationVersionController import ApplicationVersionController
|
from core.controllers.ApplicationVersionController import ApplicationVersionController
|
||||||
from core.controllers.InvoiceController import InvoiceController
|
from core.controllers.InvoiceController import InvoiceController
|
||||||
from core.controllers.LocationController import LocationController
|
from core.controllers.LocationController import LocationController
|
||||||
|
|
@ -12,6 +12,7 @@ from core.models.system.SystemProfile import SystemProfile
|
||||||
from cli.helpers import sanitize_profile, parse_application_argument, parse_location_argument
|
from cli.helpers import sanitize_profile, parse_application_argument, parse_location_argument
|
||||||
from cli.observers import profile_observer, application_version_observer, connection_observer, invoice_observer
|
from cli.observers import profile_observer, application_version_observer, connection_observer, invoice_observer
|
||||||
from core.Errors import SingboxNotInstalledException
|
from core.Errors import SingboxNotInstalledException
|
||||||
|
from cli.killswitch_monitor import KillswitchMonitor
|
||||||
import pprint
|
import pprint
|
||||||
|
|
||||||
NAME = 'profile'
|
NAME = 'profile'
|
||||||
|
|
@ -112,11 +113,82 @@ def handle(arguments, main_parser):
|
||||||
if profile is None:
|
if profile is None:
|
||||||
main_parser.error('the following argument should be a valid reference: --id/-i')
|
main_parser.error('the following argument should be a valid reference: --id/-i')
|
||||||
try:
|
try:
|
||||||
ProfileController.enable(profile, ignore=ignore, pristine=arguments.pristine, asynchronous=True, profile_observer=profile_observer, application_version_observer=application_version_observer, connection_observer=connection_observer)
|
from cli import observers as obs
|
||||||
except SingboxNotInstalledException as exception:
|
|
||||||
|
monitor = KillswitchMonitor()
|
||||||
|
timer_state = {'stop': None, 'reconnecting': None, '_retrying': False}
|
||||||
|
obs._timer_state = timer_state
|
||||||
|
|
||||||
|
def _stop_timer():
|
||||||
|
if timer_state['stop'] is not None:
|
||||||
|
timer_state['stop'].set()
|
||||||
|
timer_state['stop'] = None
|
||||||
|
|
||||||
|
def _do_disable():
|
||||||
|
_stop_timer()
|
||||||
|
ProfileController.disable(
|
||||||
|
profile,
|
||||||
|
ignore=ignore,
|
||||||
|
profile_observer=profile_observer,
|
||||||
|
connection_observer=connection_observer
|
||||||
|
)
|
||||||
|
|
||||||
|
def _on_leak():
|
||||||
|
_stop_timer()
|
||||||
|
print('\n')
|
||||||
|
print(' ⚠ Leak detected. Network blocked.')
|
||||||
|
print()
|
||||||
|
choice = None
|
||||||
|
while choice not in ('1', '2'):
|
||||||
|
print(' [1] Disable profile and restore internet')
|
||||||
|
print(' [2] Retry connection')
|
||||||
|
try:
|
||||||
|
choice = input('\n Choose an option: ').strip()
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
print()
|
||||||
|
_do_disable()
|
||||||
|
return
|
||||||
|
|
||||||
|
if choice == '1':
|
||||||
|
_do_disable()
|
||||||
|
elif choice == '2':
|
||||||
|
try:
|
||||||
|
obs._timer_state['_retrying'] = True
|
||||||
|
ProfileController.enable(
|
||||||
|
profile, ignore=ignore, pristine=False,
|
||||||
|
asynchronous=True, profile_observer=profile_observer,
|
||||||
|
application_version_observer=application_version_observer,
|
||||||
|
connection_observer=connection_observer
|
||||||
|
)
|
||||||
|
monitor.run_blocking()
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
print()
|
||||||
|
_do_disable()
|
||||||
|
|
||||||
|
monitor.set_on_disconnect(_do_disable)
|
||||||
|
monitor.set_on_leak(_on_leak)
|
||||||
|
obs._monitor = monitor
|
||||||
|
|
||||||
|
try:
|
||||||
|
ProfileController.enable(
|
||||||
|
profile, ignore=ignore, pristine=arguments.pristine,
|
||||||
|
asynchronous=True, profile_observer=profile_observer,
|
||||||
|
application_version_observer=application_version_observer,
|
||||||
|
connection_observer=connection_observer
|
||||||
|
)
|
||||||
|
monitor.run_blocking()
|
||||||
|
_stop_timer()
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
print()
|
||||||
|
_do_disable()
|
||||||
|
|
||||||
|
except SingboxNotInstalledException:
|
||||||
main_parser.error('sing-box is not installed. Please run the installation script first.')
|
main_parser.error('sing-box is not installed. Please run the installation script first.')
|
||||||
|
except PaymentRequiredError:
|
||||||
|
print('Payment processing is currently unavailable. The subscription could not be activated.\nPlease try again later or contact support.')
|
||||||
except (InvalidSubscriptionError, MissingSubscriptionError) as exception:
|
except (InvalidSubscriptionError, MissingSubscriptionError) as exception:
|
||||||
_handle_subscription_error(exception, profile, ignore, arguments, main_parser)
|
_handle_subscription_error(exception, profile, ignore, arguments, main_parser)
|
||||||
|
|
||||||
elif arguments.subcommand == 'disable':
|
elif arguments.subcommand == 'disable':
|
||||||
profile = ProfileController.get(arguments.id)
|
profile = ProfileController.get(arguments.id)
|
||||||
if profile is not None:
|
if profile is not None:
|
||||||
|
|
@ -124,6 +196,7 @@ def handle(arguments, main_parser):
|
||||||
else:
|
else:
|
||||||
main_parser.error('the following argument should be a valid reference: --id/-i')
|
main_parser.error('the following argument should be a valid reference: --id/-i')
|
||||||
|
|
||||||
|
|
||||||
def _handle_subscription_error(exception, profile, ignore, arguments, main_parser):
|
def _handle_subscription_error(exception, profile, ignore, arguments, main_parser):
|
||||||
if type(exception).__name__ == 'InvalidSubscriptionError':
|
if type(exception).__name__ == 'InvalidSubscriptionError':
|
||||||
print('The profile\'s subscription appears to be invalid.\n')
|
print('The profile\'s subscription appears to be invalid.\n')
|
||||||
|
|
@ -153,14 +226,18 @@ def _handle_subscription_error(exception, profile, ignore, arguments, main_parse
|
||||||
|
|
||||||
ProfileController.attach_subscription(profile, potential_subscription)
|
ProfileController.attach_subscription(profile, potential_subscription)
|
||||||
|
|
||||||
# Operator no necesita invoice
|
|
||||||
if potential_subscription.operator_id is None:
|
if potential_subscription.operator_id is None:
|
||||||
subscription = InvoiceController.handle_payment(potential_subscription.billing_code, invoice_observer=invoice_observer, connection_observer=connection_observer)
|
subscription = InvoiceController.handle_payment(potential_subscription.billing_code, invoice_observer=invoice_observer, connection_observer=connection_observer)
|
||||||
if subscription is None:
|
if subscription is None:
|
||||||
raise RuntimeError('The subscription could not be activated. Please try again later.')
|
print('\nPayment processing is currently unavailable. Please try again later or contact support.')
|
||||||
|
return
|
||||||
ProfileController.attach_subscription(profile, subscription)
|
ProfileController.attach_subscription(profile, subscription)
|
||||||
|
|
||||||
ProfileController.enable(profile, ignore=ignore, pristine=arguments.pristine, asynchronous=True, profile_observer=profile_observer, application_version_observer=application_version_observer, connection_observer=connection_observer)
|
try:
|
||||||
|
ProfileController.enable(profile, ignore=ignore, pristine=arguments.pristine, asynchronous=True, profile_observer=profile_observer, application_version_observer=application_version_observer, connection_observer=connection_observer)
|
||||||
|
except PaymentRequiredError:
|
||||||
|
print('\nPayment processing is currently unavailable. The subscription could not be activated.\nPlease try again later or contact support.')
|
||||||
|
return
|
||||||
|
|
||||||
elif manage_subscription_input == '2':
|
elif manage_subscription_input == '2':
|
||||||
billing_code = input('\nEnter your billing code: ')
|
billing_code = input('\nEnter your billing code: ')
|
||||||
|
|
@ -168,7 +245,11 @@ def _handle_subscription_error(exception, profile, ignore, arguments, main_parse
|
||||||
subscription = SubscriptionController.get(billing_code, connection_observer=connection_observer)
|
subscription = SubscriptionController.get(billing_code, connection_observer=connection_observer)
|
||||||
if subscription is not None:
|
if subscription is not None:
|
||||||
ProfileController.attach_subscription(profile, subscription)
|
ProfileController.attach_subscription(profile, subscription)
|
||||||
ProfileController.enable(profile, ignore=ignore, pristine=arguments.pristine, asynchronous=True, profile_observer=profile_observer, application_version_observer=application_version_observer, connection_observer=connection_observer)
|
try:
|
||||||
|
ProfileController.enable(profile, ignore=ignore, pristine=arguments.pristine, asynchronous=True, profile_observer=profile_observer, application_version_observer=application_version_observer, connection_observer=connection_observer)
|
||||||
|
except PaymentRequiredError:
|
||||||
|
print('\nPayment processing is currently unavailable. The subscription could not be activated.\nPlease try again later or contact support.')
|
||||||
|
return
|
||||||
else:
|
else:
|
||||||
print('\nThe billing code appears to be invalid.\n')
|
print('\nThe billing code appears to be invalid.\n')
|
||||||
manage_subscription_input = None
|
manage_subscription_input = None
|
||||||
|
|
@ -178,6 +259,7 @@ def _handle_subscription_error(exception, profile, ignore, arguments, main_parse
|
||||||
else:
|
else:
|
||||||
print('\nInput appears to be invalid. Please try again.\n')
|
print('\nInput appears to be invalid. Please try again.\n')
|
||||||
|
|
||||||
|
|
||||||
def _build_ignore(arguments):
|
def _build_ignore(arguments):
|
||||||
ignore = []
|
ignore = []
|
||||||
if getattr(arguments, 'without_connection_protection', False):
|
if getattr(arguments, 'without_connection_protection', False):
|
||||||
|
|
|
||||||
54
cli/killswitch_monitor.py
Normal file
54
cli/killswitch_monitor.py
Normal file
|
|
@ -0,0 +1,54 @@
|
||||||
|
import time
|
||||||
|
import threading
|
||||||
|
from core.utils.encrypted_proxy import killswitch
|
||||||
|
from core.utils.encrypted_proxy.net import get_proxied_ip
|
||||||
|
|
||||||
|
|
||||||
|
class KillswitchMonitor:
|
||||||
|
|
||||||
|
POLL_INTERVAL = 10
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self._on_disconnect = None
|
||||||
|
self._on_leak = None
|
||||||
|
self._socks5_port = None
|
||||||
|
self._real_ip = None
|
||||||
|
self._running = False
|
||||||
|
self._stop_event = threading.Event()
|
||||||
|
|
||||||
|
def set_on_disconnect(self, fn):
|
||||||
|
self._on_disconnect = fn
|
||||||
|
|
||||||
|
def set_on_leak(self, fn):
|
||||||
|
self._on_leak = fn
|
||||||
|
|
||||||
|
def set_connection_data(self, socks5_port, real_ip):
|
||||||
|
self._socks5_port = socks5_port
|
||||||
|
self._real_ip = real_ip
|
||||||
|
|
||||||
|
def stop(self):
|
||||||
|
self._running = False
|
||||||
|
self._stop_event.set()
|
||||||
|
|
||||||
|
def run_blocking(self):
|
||||||
|
self._running = True
|
||||||
|
self._stop_event.clear()
|
||||||
|
|
||||||
|
while self._running:
|
||||||
|
interrupted = self._stop_event.wait(timeout=self.POLL_INTERVAL)
|
||||||
|
if interrupted or not self._running:
|
||||||
|
break
|
||||||
|
|
||||||
|
if not killswitch.status():
|
||||||
|
if self._on_disconnect:
|
||||||
|
self._on_disconnect()
|
||||||
|
self._running = False
|
||||||
|
return
|
||||||
|
|
||||||
|
if self._socks5_port and self._real_ip:
|
||||||
|
proxied = get_proxied_ip(self._socks5_port, timeout=10)
|
||||||
|
if proxied == "unknown" or proxied == self._real_ip:
|
||||||
|
if self._on_leak:
|
||||||
|
self._on_leak()
|
||||||
|
self._running = False
|
||||||
|
return
|
||||||
|
|
@ -1,10 +1,12 @@
|
||||||
|
from core.utils.encrypted_proxy.net import get_proxied_ip
|
||||||
|
from core.utils.encrypted_proxy import killswitch
|
||||||
from core.controllers.ApplicationController import ApplicationController
|
from core.controllers.ApplicationController import ApplicationController
|
||||||
from core.observers.ApplicationVersionObserver import ApplicationVersionObserver
|
from core.observers.ApplicationVersionObserver import ApplicationVersionObserver
|
||||||
from core.observers.ClientObserver import ClientObserver
|
from core.observers.ClientObserver import ClientObserver
|
||||||
from core.observers.ConnectionObserver import ConnectionObserver
|
from core.observers.ConnectionObserver import ConnectionObserver
|
||||||
from core.observers.InvoiceObserver import InvoiceObserver
|
from core.observers.InvoiceObserver import InvoiceObserver
|
||||||
from core.observers.ProfileObserver import ProfileObserver
|
from core.observers.ProfileObserver import ProfileObserver
|
||||||
from cli.ui import Spinner, label_ok, label_error, label_warn, label_connected, label_disconnect, progress_bar
|
from cli.ui import Spinner, label_ok, label_error, label_warn, label_connected, label_disconnect, progress_bar, label_killswitch
|
||||||
from cli.helpers import sanitize_profile
|
from cli.helpers import sanitize_profile
|
||||||
import pprint
|
import pprint
|
||||||
|
|
||||||
|
|
@ -14,6 +16,10 @@ connection_observer = ConnectionObserver()
|
||||||
invoice_observer = InvoiceObserver()
|
invoice_observer = InvoiceObserver()
|
||||||
profile_observer = ProfileObserver()
|
profile_observer = ProfileObserver()
|
||||||
|
|
||||||
|
_spinner = None
|
||||||
|
_monitor = None
|
||||||
|
_timer_state = None
|
||||||
|
|
||||||
application_version_observer.subscribe('downloading', lambda event: print(f'Downloading {ApplicationController.get(event.subject.application_code).name}, version {event.subject.version_number}...'))
|
application_version_observer.subscribe('downloading', lambda event: print(f'Downloading {ApplicationController.get(event.subject.application_code).name}, version {event.subject.version_number}...'))
|
||||||
application_version_observer.subscribe('download_progressing', lambda event: print(f'Current progress: {event.meta.get("progress"):.2f}%', flush=True, end='\r'))
|
application_version_observer.subscribe('download_progressing', lambda event: print(f'Current progress: {event.meta.get("progress"):.2f}%', flush=True, end='\r'))
|
||||||
application_version_observer.subscribe('downloaded', lambda event: print('\n'))
|
application_version_observer.subscribe('downloaded', lambda event: print('\n'))
|
||||||
|
|
@ -23,48 +29,84 @@ client_observer.subscribe('updating', lambda event: print('Updating client...'))
|
||||||
client_observer.subscribe('update_progressing', lambda event: print(f'Current progress: {event.meta.get("progress"):.2f}%', flush=True, end='\r'))
|
client_observer.subscribe('update_progressing', lambda event: print(f'Current progress: {event.meta.get("progress"):.2f}%', flush=True, end='\r'))
|
||||||
client_observer.subscribe('updated', lambda event: print('\n'))
|
client_observer.subscribe('updated', lambda event: print('\n'))
|
||||||
|
|
||||||
_spinner = None
|
|
||||||
|
|
||||||
def _on_connecting(event):
|
def _on_connecting(event):
|
||||||
global _spinner
|
global _spinner, _timer_state
|
||||||
|
if _timer_state and _timer_state['stop']:
|
||||||
|
_timer_state['stop'].set()
|
||||||
|
_timer_state['stop'] = None
|
||||||
|
if _timer_state and _timer_state['reconnecting'] is None and _timer_state.get('_retrying'):
|
||||||
|
from cli.ui import connecting_spinner
|
||||||
|
_timer_state['reconnecting'] = connecting_spinner()
|
||||||
attempt = event.subject.get("attempt_count", "?")
|
attempt = event.subject.get("attempt_count", "?")
|
||||||
total = event.subject.get("maximum_number_of_attempts", "?")
|
total = event.subject.get("maximum_number_of_attempts", "?")
|
||||||
_spinner = Spinner(f"Connecting... [{attempt}/{total}]")
|
_spinner = Spinner(f"[net] verify_ip attempt {attempt}/{total}...")
|
||||||
_spinner.start()
|
_spinner.start()
|
||||||
|
|
||||||
|
|
||||||
def _on_connected(event):
|
def _on_connected(event):
|
||||||
global _spinner
|
global _spinner, _monitor, _timer_state
|
||||||
if _spinner:
|
if _spinner:
|
||||||
_spinner.stop()
|
_spinner.stop()
|
||||||
_spinner = None
|
_spinner = None
|
||||||
|
if _timer_state and _timer_state.get('reconnecting'):
|
||||||
|
_timer_state['reconnecting'].set()
|
||||||
|
_timer_state['reconnecting'] = None
|
||||||
|
if _timer_state:
|
||||||
|
_timer_state['_retrying'] = False
|
||||||
d = event.subject or {}
|
d = event.subject or {}
|
||||||
label_connected(f"proxy={d.get('proxy_ip','?')} real={d.get('real_ip','?')} port={d.get('socks5_port','?')}")
|
label_connected(f"proxy={d.get('proxy_ip','?')} real={d.get('real_ip','?')} port={d.get('socks5_port','?')}")
|
||||||
|
label_killswitch(killswitch.status())
|
||||||
|
if _monitor is not None:
|
||||||
|
_monitor.set_connection_data(
|
||||||
|
socks5_port=d.get('socks5_port'),
|
||||||
|
real_ip=d.get('real_ip')
|
||||||
|
)
|
||||||
|
if _timer_state is not None:
|
||||||
|
from cli.ui import session_timer
|
||||||
|
_timer_state['stop'] = session_timer()
|
||||||
|
|
||||||
|
|
||||||
def _on_disconnected(event):
|
def _on_disconnected(event):
|
||||||
global _spinner
|
global _spinner, _monitor, _timer_state
|
||||||
if _spinner:
|
if _spinner:
|
||||||
_spinner.stop()
|
_spinner.stop()
|
||||||
_spinner = None
|
_spinner = None
|
||||||
|
if _timer_state and _timer_state['stop']:
|
||||||
|
_timer_state['stop'].set()
|
||||||
|
_timer_state['stop'] = None
|
||||||
|
if _monitor is not None:
|
||||||
|
_monitor.stop()
|
||||||
|
_monitor = None
|
||||||
d = event.subject or {}
|
d = event.subject or {}
|
||||||
ip = d.get("ip", "?")
|
label_disconnect(f"real={d.get('ip','?')}")
|
||||||
label_disconnect(f"real={ip}")
|
|
||||||
|
|
||||||
def _on_error(event):
|
def _on_error(event):
|
||||||
global _spinner
|
global _spinner, _monitor, _timer_state
|
||||||
if _spinner:
|
if _spinner:
|
||||||
_spinner.stop()
|
_spinner.stop()
|
||||||
_spinner = None
|
_spinner = None
|
||||||
|
if _timer_state and _timer_state['stop']:
|
||||||
|
_timer_state['stop'].set()
|
||||||
|
_timer_state['stop'] = None
|
||||||
|
if _monitor is not None:
|
||||||
|
_monitor.stop()
|
||||||
|
_monitor = None
|
||||||
label_error(str(event.subject or "Unknown error"))
|
label_error(str(event.subject or "Unknown error"))
|
||||||
|
|
||||||
|
|
||||||
def _on_tor_bootstrapping(event):
|
def _on_tor_bootstrapping(event):
|
||||||
global _spinner
|
global _spinner
|
||||||
_spinner = Spinner("Starting Tor...")
|
_spinner = Spinner("Starting Tor...")
|
||||||
_spinner.start()
|
_spinner.start()
|
||||||
|
|
||||||
|
|
||||||
def _on_tor_bootstrap_progressing(event):
|
def _on_tor_bootstrap_progressing(event):
|
||||||
pct = int(event.meta.get("progress", 0))
|
pct = int(event.meta.get("progress", 0))
|
||||||
progress_bar(pct, 100, "Tor bootstrap")
|
progress_bar(pct, 100, "Tor bootstrap")
|
||||||
|
|
||||||
|
|
||||||
def _on_tor_bootstrapped(event):
|
def _on_tor_bootstrapped(event):
|
||||||
global _spinner
|
global _spinner
|
||||||
if _spinner:
|
if _spinner:
|
||||||
|
|
@ -73,6 +115,7 @@ def _on_tor_bootstrapped(event):
|
||||||
print()
|
print()
|
||||||
label_ok("Tor ready")
|
label_ok("Tor ready")
|
||||||
|
|
||||||
|
|
||||||
connection_observer.subscribe('connecting', _on_connecting)
|
connection_observer.subscribe('connecting', _on_connecting)
|
||||||
connection_observer.subscribe('connected', _on_connected)
|
connection_observer.subscribe('connected', _on_connected)
|
||||||
connection_observer.subscribe('disconnected', _on_disconnected)
|
connection_observer.subscribe('disconnected', _on_disconnected)
|
||||||
|
|
|
||||||
45
cli/ui.py
45
cli/ui.py
|
|
@ -142,3 +142,48 @@ def wait_with_spinner(msg: str, fn, *args, result_msg: str = "", **kwargs):
|
||||||
if result_box[1] is not None:
|
if result_box[1] is not None:
|
||||||
raise result_box[1]
|
raise result_box[1]
|
||||||
return result_box[0]
|
return result_box[0]
|
||||||
|
def label_killswitch(armed: bool):
|
||||||
|
if armed:
|
||||||
|
print(f" {BGREEN}[ KS ]{RESET} Kill switch active")
|
||||||
|
else:
|
||||||
|
print(f" {BRED}[ KS ]{RESET} Kill switch {BRED}INACTIVE — connection unprotected{RESET}")
|
||||||
|
def session_timer() -> threading.Event:
|
||||||
|
stop_event = threading.Event()
|
||||||
|
start = time.time()
|
||||||
|
|
||||||
|
def _run():
|
||||||
|
while not stop_event.is_set():
|
||||||
|
elapsed = int(time.time() - start)
|
||||||
|
h = elapsed // 3600
|
||||||
|
m = (elapsed % 3600) // 60
|
||||||
|
s = elapsed % 60
|
||||||
|
sys.stdout.write(
|
||||||
|
f"\r {BGREEN}●{RESET} {BWHITE}Protected{RESET} "
|
||||||
|
f"{DIM}[{h:02d}:{m:02d}:{s:02d}]{RESET} "
|
||||||
|
)
|
||||||
|
sys.stdout.flush()
|
||||||
|
time.sleep(1)
|
||||||
|
sys.stdout.write("\r" + " " * 50 + "\r")
|
||||||
|
sys.stdout.flush()
|
||||||
|
|
||||||
|
threading.Thread(target=_run, daemon=True).start()
|
||||||
|
return stop_event
|
||||||
|
def connecting_spinner() -> threading.Event:
|
||||||
|
stop_event = threading.Event()
|
||||||
|
frames = ("─", "\\", "│", "/")
|
||||||
|
|
||||||
|
def _run():
|
||||||
|
i = 0
|
||||||
|
while not stop_event.is_set():
|
||||||
|
frame = frames[i % len(frames)]
|
||||||
|
sys.stdout.write(
|
||||||
|
f"\r {BYELLOW}{frame}{RESET} {DIM}Reconnecting...{RESET} "
|
||||||
|
)
|
||||||
|
sys.stdout.flush()
|
||||||
|
i += 1
|
||||||
|
time.sleep(0.12)
|
||||||
|
sys.stdout.write("\r" + " " * 60 + "\r")
|
||||||
|
sys.stdout.flush()
|
||||||
|
|
||||||
|
threading.Thread(target=_run, daemon=True).start()
|
||||||
|
return stop_event
|
||||||
Loading…
Reference in a new issue