|
11 | 11 | import requests |
12 | 12 | import platform |
13 | 13 | import itertools |
14 | | -import threading |
15 | 14 | import base64 |
16 | 15 | import subprocess |
17 | 16 | import uuid |
18 | 17 | import getpass |
19 | 18 | import hashlib |
20 | 19 | import argparse |
21 | 20 | import logging |
| 21 | +from gevent import Timeout |
22 | 22 | from pathlib import Path |
23 | 23 | from cryptography.fernet import Fernet |
24 | 24 | from cryptography.hazmat.primitives import hashes |
@@ -201,8 +201,17 @@ def handle_exception(exc_type, exc_value, exc_traceback): |
201 | 201 |
|
202 | 202 | def prompt(self, msg: str) -> str: |
203 | 203 | """Log a prompt with [→] but keep input on same line.""" |
204 | | - # Get the formatted prefix from the logger (e.g. "[→] ") |
205 | | - prefix = ConsoleFormatter.SYMBOLS.get("INFO", "[→] ") |
| 204 | + prefix = ConsoleFormatter.SYMBOLS.get("INFO", "[->] ") |
| 205 | + |
| 206 | + # Print prefix and message WITHOUT newline |
| 207 | + print(f"{prefix}{msg} ", end="", flush=True) |
| 208 | + |
| 209 | + # Now take input |
| 210 | + return input() |
| 211 | + |
| 212 | + def promptwarn(self, msg: str) -> str: |
| 213 | + """Log a prompt with [→] but keep input on same line.""" |
| 214 | + prefix = ConsoleFormatter.SYMBOLS.get("INFO", "[!!] ") |
206 | 215 |
|
207 | 216 | # Print prefix and message WITHOUT newline |
208 | 217 | print(f"{prefix}{msg} ", end="", flush=True) |
@@ -250,7 +259,7 @@ def login(self, login_input=None): |
250 | 259 |
|
251 | 260 | # Step 3: Handle login result |
252 | 261 | if result != EResult.OK: |
253 | | - self.logger.log_error(f"Steam login failed: {result.name}") |
| 262 | + self.logger.log_error(f"Steam login failed, exiting") |
254 | 263 | self.client.logout() |
255 | 264 | sys.exit(EXIT_LOGIN_FAILED) |
256 | 265 |
|
@@ -350,44 +359,98 @@ def get_available_accounts(self): |
350 | 359 | def attempt_login(self): |
351 | 360 | """Perform the main login""" |
352 | 361 | result = None |
353 | | - prompt_for_unavailable = True |
| 362 | + prompt_disabled = False |
| 363 | + login_timeout = 2 |
| 364 | + retry_count = 0 |
| 365 | + max_tries = 10 |
354 | 366 |
|
355 | | - while result in (EResult.TryAnotherCM, EResult.ServiceUnavailable, EResult.InvalidPassword, None): |
| 367 | + while True: |
| 368 | + try: |
| 369 | + if retry_count == 0: |
| 370 | + self.logger.log_info(f"Logging in...") |
| 371 | + else: |
| 372 | + self.logger.log_info(f"Login attempt {retry_count + 1}...") |
| 373 | + with Timeout(login_timeout): |
| 374 | + self.client.connect() |
| 375 | + result = self.client.login(self.username, self.env_password, self.refresh_token) |
| 376 | + except Timeout: |
| 377 | + self.logger.log_warning(f"Login timed out after {login_timeout} seconds") |
| 378 | + result = EResult.Timeout |
| 379 | + except Exception as e: |
| 380 | + self.logger.log_error(f"Login error: {e}") |
| 381 | + result = None |
356 | 382 |
|
357 | | - # Handle connection issues |
358 | | - if result in (EResult.TryAnotherCM, EResult.ServiceUnavailable): |
359 | | - if prompt_for_unavailable and result == EResult.ServiceUnavailable: |
360 | | - if not self.handle_service_unavailable(): |
361 | | - break |
362 | | - prompt_for_unavailable = False |
363 | | - self.client.reconnect(maxdelay=15) |
| 383 | + if result == EResult.OK: |
| 384 | + break |
364 | 385 |
|
365 | | - # Handle authentication failures |
| 386 | + # Handle authentication failures (non-retryable) |
366 | 387 | if result == EResult.InvalidPassword: |
367 | | - self.logger.log_error("Invalid password or refresh_token.") |
368 | | - self.logger.log_error(f"Correct the password or delete '{self.main.SAVED_LOGINS_FILE}' and try again.") |
369 | | - self.client.logout() |
| 388 | + if self.refresh_token or self.main.SILENT_MODE: |
| 389 | + self.logger.log_error("Looks like the token wasn't accepted or the password is wrong") |
| 390 | + self.logger.log_error(f"Try deleting '{self.main.SAVED_LOGINS_FILE}' and then try again.") |
| 391 | + sys.exit(EXIT_LOGIN_FAILED) |
| 392 | + else: |
| 393 | + self.logger.log_error("Invalid password. Please try again with the correct one") |
| 394 | + self.client.logout() |
| 395 | + self.client.disconnect() |
| 396 | + continue |
| 397 | + |
| 398 | + if not self.main.SILENT_MODE and not prompt_disabled: |
| 399 | + self.handle_service_unavailable() |
| 400 | + prompt_disabled = True |
| 401 | + |
| 402 | + # Ask if we should continue |
| 403 | + if not self.main.INFINITE_RETRY and not self.main.SILENT_MODE and retry_count >= max_tries: |
| 404 | + self.handle_service_unavailable() |
| 405 | + max_tries += 5 |
| 406 | + |
| 407 | + # Handle connection issues (retryable) |
| 408 | + retry_count += 1 |
| 409 | + |
| 410 | + msg = f"An unrecognized error occurred when trying to login: {result}" |
| 411 | + if result == EResult.TryAnotherCM: |
| 412 | + msg = "The Steam Servers aren't letting us login (TryAnotherCM) - Try waiting a while before contacting the servers again" |
| 413 | + elif result == EResult.Timeout: |
| 414 | + msg = "The login attempt timed out (Timeout)" |
| 415 | + elif result == EResult.ServiceUnavailable: |
| 416 | + msg = "The Steam Servers aren't available right now (ServiceUnavailable)" |
| 417 | + |
| 418 | + self.logger.log_error(msg) |
| 419 | + if self.main.SILENT_MODE and not self.main.INFINITE_RETRY and prompt_disabled: |
370 | 420 | sys.exit(EXIT_LOGIN_FAILED) |
371 | 421 |
|
372 | | - # Get credentials via web auth if needed |
373 | | - if not self.refresh_token: |
374 | | - if not self.perform_web_authentication(): |
375 | | - self.client.logout() |
376 | | - sys.exit(EXIT_LOGIN_FAILED) |
| 422 | + base_wait = min(5 * (2 ** (retry_count - 1)), 1) |
| 423 | + jitter = base_wait * 0.1 # Add 10% random jitter |
| 424 | + wait_time = base_wait + (time.time() % jitter) |
377 | 425 |
|
378 | | - result = self.client.login(self.username, self.env_password, self.refresh_token) |
| 426 | + self.logger.log_info(f"Waiting {wait_time:.1f} seconds before retry ({retry_count}/{'infinity' if self.main.INFINITE_RETRY else max_tries})") |
| 427 | + time.sleep(wait_time) |
| 428 | + self.client.disconnect() |
| 429 | + continue |
| 430 | + |
| 431 | + # Get credentials via web auth if needed (only on success) |
| 432 | + if not self.refresh_token and result not in (EResult.InvalidPassword, None): |
| 433 | + if not self.perform_web_authentication(): |
| 434 | + self.client.logout() |
| 435 | + sys.exit(EXIT_LOGIN_FAILED) |
379 | 436 |
|
380 | 437 | return result |
381 | 438 |
|
382 | 439 | def handle_service_unavailable(self): |
383 | | - """Handle Steam service unavailable scenario""" |
384 | | - if self.main.SILENT_MODE: |
385 | | - return False |
386 | | - |
| 440 | + """Handle Steam service unavailable scenario with y/n/i options""" |
387 | 441 | while True: |
388 | | - answer = input("[!] Steam is down. Keep retrying? [y/n]: ").lower() |
389 | | - if answer in 'yn': |
390 | | - return answer == 'y' |
| 442 | + answer = self.logger.promptwarn(f"Keep retrying? (y=yes, n=no, i=infinite): ").lower() |
| 443 | + if answer == 'i': |
| 444 | + self.main.INFINITE_RETRY = True |
| 445 | + self.logger.log_info("Retrying infinitely. Press CTRL+C to cancel") |
| 446 | + return |
| 447 | + elif answer == 'y': |
| 448 | + self.main.INFINITE_RETRY = False |
| 449 | + self.logger.log_info("Retrying.") |
| 450 | + return |
| 451 | + else: |
| 452 | + self.logger.log_info("No retry selected, exiting.") |
| 453 | + sys.exit(EXIT_LOGIN_FAILED) |
391 | 454 |
|
392 | 455 | def perform_web_authentication(self): |
393 | 456 | """Perform web-based authentication when no refresh token exists""" |
@@ -1193,11 +1256,13 @@ def run(self): |
1193 | 1256 | parser.add_argument('--appid', type=str, help='Comma-separated list of app IDs to generate schemas for') |
1194 | 1257 | parser.add_argument('--save-dir', type=str, help='Base directory to save data and outputs (overrides default script-based base dir)') |
1195 | 1258 | parser.add_argument('--max-tries', type=int, help='Maximum number of consecutive "no schema" responses before giving up') |
| 1259 | + parser.add_argument('--infinite-retry', action='store_true', help='Retry login attempts infinitely when encountering network errors') |
1196 | 1260 |
|
1197 | 1261 | args = parser.parse_args() |
1198 | 1262 |
|
1199 | 1263 | self.SILENT_MODE = args.silent |
1200 | 1264 | self.VERBOSE = args.verbose |
| 1265 | + self.INFINITE_RETRY = args.infinite_retry |
1201 | 1266 |
|
1202 | 1267 | # If user specified a save directory, update BASE_DIR and all dependent paths |
1203 | 1268 | if args.save_dir: |
|
0 commit comments