Skip to content

Commit 7fc713a

Browse files
author
Earth1283
committed
patched thread leaks
1 parent a0275ee commit 7fc713a

5 files changed

Lines changed: 36 additions & 18 deletions

File tree

pymcl/config_manager.py

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import json
22
import os
3+
import threading
34
from pathlib import Path
45

56
# Determine default data directory (mirrors constants.py logic to avoid circular import)
@@ -19,10 +20,12 @@
1920

2021
class ConfigManager:
2122
_instance = None
23+
_lock = threading.Lock()
2224

2325
def __new__(cls):
2426
if cls._instance is None:
2527
cls._instance = super(ConfigManager, cls).__new__(cls)
28+
cls._instance._rw_lock = threading.Lock()
2629
cls._instance._load_config()
2730
return cls._instance
2831

@@ -36,18 +39,22 @@ def _load_config(self):
3639
print(f"Error loading settings: {e}")
3740

3841
def get(self, key, default=None):
39-
return self._settings.get(key, default)
42+
with self._rw_lock:
43+
return self._settings.get(key, default)
4044

4145
def set(self, key, value):
42-
self._settings[key] = value
46+
with self._rw_lock:
47+
self._settings[key] = value
4348

4449
def save(self):
45-
CONFIG_DIR.mkdir(parents=True, exist_ok=True)
46-
try:
47-
with open(SETTINGS_FILE, "w") as f:
48-
json.dump(self._settings, f, indent=4)
49-
except OSError as e:
50-
print(f"Error saving settings: {e}")
50+
with self._rw_lock:
51+
CONFIG_DIR.mkdir(parents=True, exist_ok=True)
52+
try:
53+
with open(SETTINGS_FILE, "w") as f:
54+
json.dump(self._settings, f, indent=4)
55+
except OSError as e:
56+
print(f"Error saving settings: {e}")
5157

5258
def get_all(self):
53-
return self._settings.copy()
59+
with self._rw_lock:
60+
return self._settings.copy()

pymcl/constants.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import os
2+
from pathlib import Path
23
from typing import TypedDict
34
from .config_manager import ConfigManager
45

@@ -23,11 +24,13 @@
2324

2425
def get_game_dir(version_id: str) -> str:
2526
"""Returns the game directory for a specific version instance."""
26-
# sanitize version_id to avoid path traversal or invalid characters if necessary
27-
# For now, we assume version_id is safe as it comes from the launcher lib
2827
if not version_id or version_id == "Loading versions...":
29-
return MINECRAFT_DIR # Fallback to default
30-
return os.path.join(MINECRAFT_DIR, "instances", version_id)
28+
return MINECRAFT_DIR
29+
base = Path(MINECRAFT_DIR) / "instances"
30+
result = (base / version_id).resolve()
31+
if not str(result).startswith(str(base.resolve())):
32+
raise ValueError(f"Invalid version_id: {version_id!r}")
33+
return str(result)
3134

3235

3336
def get_mods_dir(version_id: str) -> str:

pymcl/image_cache.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,6 @@ class ImageCache(QObject):
3232

3333
def __init__(self, parent=None):
3434
super().__init__(parent)
35-
self.downloader_threads = []
3635

3736
def get_image(self, url):
3837
filename = url.split("/")[-1]
@@ -45,7 +44,7 @@ def get_image(self, url):
4544
return None
4645

4746
def download_image(self, url, cache_path):
48-
thread = QThread()
47+
thread = QThread(self) # parent=self keeps it alive until finished
4948
downloader = ImageDownloader(url, cache_path)
5049
downloader.moveToThread(thread)
5150

@@ -57,7 +56,6 @@ def download_image(self, url, cache_path):
5756
thread.finished.connect(thread.deleteLater)
5857

5958
thread.start()
60-
self.downloader_threads.append(thread)
6159

6260
@pyqtSlot(str, str)
6361
def on_image_downloaded(self, url, path):

pymcl/microsoft_auth.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,17 @@ def start_login(self):
4444

4545
self.server_thread = QThread()
4646
self.http_server = HTTPServer(("localhost", 8000), lambda *args, **kwargs: AuthHandler(self.finish_login, *args, **kwargs))
47-
self.server_thread.run = self.http_server.handle_request
47+
self.http_server.socket.settimeout(120) # unblock after 2 minutes if auth never completes
48+
49+
def run_server():
50+
try:
51+
self.http_server.handle_request()
52+
except OSError:
53+
self.login_failed.emit("Login timed out or was cancelled.")
54+
if self.http_server:
55+
self.http_server.server_close()
56+
57+
self.server_thread.run = run_server
4858
self.server_thread.start()
4959

5060
def finish_login(self, auth_code):

pymcl/skin_manager.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ def run(self):
8080
else:
8181
try:
8282
err = resp.json().get("errorMessage", resp.text)
83-
except:
83+
except (ValueError, KeyError):
8484
err = resp.text
8585
self.finished.emit(False, f"Upload failed: {err}")
8686

0 commit comments

Comments
 (0)