-
-
Notifications
You must be signed in to change notification settings - Fork 551
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Fix IDAKLU solver crash when running in multiple threads + when telemetry is prompted for #4583
base: develop
Are you sure you want to change the base?
Changes from all commits
8709969
6fccf68
9348522
c89ee9c
b675502
e8680df
af0de0f
925cbd6
d8d37ed
fbb623e
0826afe
1da3d91
4f8b48c
91379bd
de60647
083184b
6f077e3
7433555
57553df
e4859dc
5495814
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -45,6 +45,7 @@ dependencies = [ | |
"pandas>=1.5.0", | ||
"pooch>=1.8.1", | ||
"posthog", | ||
"platformdirs", | ||
] | ||
|
||
[project.urls] | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,11 +1,14 @@ | ||
import uuid | ||
import os | ||
import platformdirs | ||
from pathlib import Path | ||
import pybamm | ||
import sys | ||
import threading | ||
import time | ||
import uuid | ||
|
||
from pathlib import Path | ||
|
||
import pybamm | ||
import platformdirs | ||
|
||
from pybamm.util import is_notebook | ||
|
||
|
||
def is_running_tests(): # pragma: no cover | ||
|
@@ -45,20 +48,118 @@ def is_running_tests(): # pragma: no cover | |
return False | ||
|
||
|
||
def ask_user_opt_in(timeout=10): | ||
def get_input_or_timeout(timeout): # pragma: no cover | ||
""" | ||
Ask the user if they want to opt in to telemetry. | ||
Cross-platform input with timeout, using various methods depending on the | ||
environment. Works in Jupyter notebooks, Windows, and Unix-like systems. | ||
|
||
Args: | ||
timeout (float): Timeout in seconds | ||
|
||
Returns: | ||
str | None: The user input if received before the timeout, or None if: | ||
- The timeout was reached | ||
- Telemetry is disabled via environment variable | ||
- Running in a non-interactive environment | ||
- An error occurred | ||
|
||
The caller can distinguish between an empty response (user just pressed Enter) | ||
which returns '', and a timeout/error which returns None. | ||
""" | ||
# Check for telemetry disable flag | ||
if os.getenv("PYBAMM_DISABLE_TELEMETRY", "false").lower() != "false": | ||
return None | ||
|
||
if not (sys.stdin.isatty() or is_notebook()): | ||
return None | ||
|
||
# 1. Handling for Jupyter notebooks. This is a simplified | ||
# implementation in comparison to widgets because notebooks | ||
# are usually run interactively, and we don't need to worry | ||
# about the event loop. The input is disabled when running | ||
# tests due to the environment variable set. | ||
if is_notebook(): # pragma: no cover | ||
try: | ||
from IPython.display import clear_output | ||
|
||
user_input = input("Do you want to enable telemetry? (Y/n): ") | ||
clear_output() | ||
|
||
return user_input | ||
|
||
Parameters | ||
---------- | ||
timeout : float, optional | ||
The timeout for the user to respond to the prompt. Default is 10 seconds. | ||
except Exception: # pragma: no cover | ||
return None | ||
|
||
Returns | ||
------- | ||
bool | ||
True if the user opts in, False otherwise. | ||
# 2. Windows-specific handling | ||
if sys.platform == "win32": | ||
try: | ||
import msvcrt | ||
|
||
start_time = time.time() | ||
input_chars = [] | ||
sys.stdout.write("Do you want to enable telemetry? (Y/n): ") | ||
sys.stdout.flush() | ||
|
||
while time.time() - start_time < timeout: | ||
if msvcrt.kbhit(): | ||
char = msvcrt.getwche() | ||
if char in ("\r", "\n"): | ||
sys.stdout.write("\n") | ||
return "".join(input_chars) | ||
input_chars.append(char) | ||
time.sleep(0.1) | ||
return None | ||
except Exception: | ||
return None | ||
|
||
# 3. POSIX-like systems will need to use termios | ||
else: # pragma: no cover | ||
try: | ||
import termios | ||
import tty | ||
import select | ||
|
||
# Save terminal settings for later | ||
old_settings = termios.tcgetattr(sys.stdin) | ||
try: | ||
# Set terminal to raw mode | ||
tty.setraw(sys.stdin.fileno()) | ||
|
||
sys.stdout.write("Do you want to enable telemetry? (Y/n): ") | ||
sys.stdout.flush() | ||
|
||
input_chars = [] | ||
start_time = time.time() | ||
|
||
while time.time() - start_time < timeout: | ||
rlist, _, _ = select.select([sys.stdin], [], [], 0.1) | ||
Comment on lines
+134
to
+135
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If we are already using select, do we need the loop? The timeout isn't very long There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, I checked and this loop ended up being required – since while normal
which the loop apparently allows us to do. But if you have something else in mind, I'm happy to try that. |
||
if rlist: | ||
char = sys.stdin.read(1) | ||
if char in ("\r", "\n"): | ||
sys.stdout.write("\n") | ||
return "".join(input_chars) | ||
input_chars.append(char) | ||
sys.stdout.write(char) | ||
sys.stdout.flush() | ||
return None | ||
|
||
finally: | ||
# Restore saved terminal settings | ||
termios.tcsetattr(sys.stdin, termios.TCSAFLUSH, old_settings) | ||
sys.stdout.write("\n") | ||
sys.stdout.flush() | ||
|
||
except Exception: # pragma: no cover | ||
return None | ||
|
||
return None | ||
|
||
|
||
def ask_user_opt_in(timeout=10): # pragma: no cover | ||
""" | ||
Ask the user if they want to opt in to telemetry. | ||
""" | ||
|
||
print( | ||
"PyBaMM can collect usage data and send it to the PyBaMM team to " | ||
"help us improve the software.\n" | ||
|
@@ -69,44 +170,25 @@ def ask_user_opt_in(timeout=10): | |
"For more information, see https://docs.pybamm.org/en/latest/source/user_guide/index.html#telemetry" | ||
) | ||
|
||
def get_input(): # pragma: no cover | ||
try: | ||
user_input = ( | ||
input("Do you want to enable telemetry? (Y/n): ").strip().lower() | ||
) | ||
answer.append(user_input) | ||
except Exception: | ||
# Handle any input errors | ||
pass | ||
user_input = get_input_or_timeout(timeout) | ||
|
||
time_start = time.time() | ||
if user_input is None: | ||
print("\nTimeout reached. Defaulting to not enabling telemetry.") | ||
return False | ||
|
||
while True: | ||
if time.time() - time_start > timeout: | ||
print("\nTimeout reached. Defaulting to not enabling telemetry.") | ||
if user_input.lower() in ["y", "yes", ""]: | ||
print("Telemetry enabled.\n") | ||
return True | ||
elif user_input.lower() in ["n", "no"]: | ||
print("Telemetry disabled.") | ||
return False | ||
|
||
answer = [] | ||
# Create and start input thread | ||
input_thread = threading.Thread(target=get_input) | ||
input_thread.daemon = True | ||
input_thread.start() | ||
|
||
# Wait for either timeout or input | ||
input_thread.join(timeout) | ||
|
||
if answer: | ||
if answer[0] in ["yes", "y", ""]: | ||
print("\nTelemetry enabled.\n") | ||
return True | ||
elif answer[0] in ["no", "n"]: | ||
print("\nTelemetry disabled.\n") | ||
return False | ||
else: | ||
print("\nInvalid input. Please enter 'yes/y' for yes or 'no/n' for no.") | ||
else: | ||
print("\nTimeout reached. Defaulting to not enabling telemetry.") | ||
return False | ||
print("Invalid input. Please enter 'Y/y' for yes or 'n/N' for no:") | ||
user_input = get_input_or_timeout(timeout) | ||
if user_input is None: | ||
print("\nTimeout reached. Defaulting to not enabling telemetry.") | ||
return False | ||
|
||
|
||
def generate(): | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It seems excessive that we need to change the terminal settings for this
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What this allows you to do is that you don't get new indentation when you write to the terminal again. I wrapped this in a try-finally block based on the Python docs: https://docs.python.org/3/library/termios.html#example.
So, by saving the attributes, I'm trying to disable the fact that
sys.stdout.write("\n")
will not return the cursor to the leftmost position (because we are in raw mode and we need to do that manually instead – for which I want to avoid).We end up with this, otherwise:
and this would break one's terminal until one restarts it – a more robust solution could be to use
TCSAFLUSH
instead ofTCSADRAIN
, I'll switch to that.