Skip to content

Commit e0866ed

Browse files
authored
Update proton_manager.py
1 parent 7f1bc8d commit e0866ed

File tree

1 file changed

+151
-31
lines changed

1 file changed

+151
-31
lines changed

proton_manager.py

Lines changed: 151 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,25 @@
77
import time
88
from datetime import datetime
99
from config_manager import ConfigManager
10+
import logging
11+
1012
class ProtonManager:
1113
def __init__(self):
1214
self.protons_dir = ConfigManager.protons_dir
1315
os.makedirs(self.protons_dir, exist_ok=True)
16+
self.available_ge_cache = None
17+
self.available_official_stable_cache = None
18+
self.available_official_exp_cache = None
19+
self.cache_time = 0
20+
self.cache_duration = 3600 # 1 hour
21+
22+
def refresh_cache_if_needed(self):
23+
if time.time() - self.cache_time > self.cache_duration:
24+
self.available_ge_cache = self.get_available_ge()
25+
self.available_official_stable_cache = self.get_available_official(stable=True)
26+
self.available_official_exp_cache = self.get_available_official(stable=False)
27+
self.cache_time = time.time()
28+
1429
def get_installed_protons(self):
1530
protons = []
1631
for d in os.listdir(self.protons_dir):
@@ -23,47 +38,113 @@ def get_installed_protons(self):
2338
status = 'Update Available' if update_info else 'Installed'
2439
protons.append({'version': version, 'type': proton_type, 'date': install_date, 'status': status})
2540
return sorted(protons, key=lambda x: x['version'])
41+
2642
def get_proton_path(self, version):
27-
return os.path.join(self.protons_dir, version, 'proton')
43+
base = os.path.join(self.protons_dir, version)
44+
possible_paths = [
45+
os.path.join(base, 'proton'),
46+
os.path.join(base, 'dist', 'bin', 'proton'),
47+
os.path.join(base, 'bin', 'proton'),
48+
os.path.join(base, 'proton-run')
49+
]
50+
for path in possible_paths:
51+
if os.path.exists(path):
52+
return path
53+
raise Exception(f"Proton binary not found in {version}")
54+
55+
def _version_key(self, version):
56+
# Improved version sorting: handle numbers and non-numbers, including decimals
57+
version = version.replace('GE-Proton', '').replace('Proton-', '')
58+
parts = []
59+
current = ''
60+
for char in version:
61+
if char.isdigit() or char == '.':
62+
if current and not (current[-1].isdigit() or current[-1] == '.'):
63+
parts.append(current)
64+
current = ''
65+
current += char
66+
else:
67+
if current and (current[-1].isdigit() or current[-1] == '.'):
68+
parts.append(current)
69+
current = ''
70+
current += char
71+
if current:
72+
parts.append(current)
73+
# Convert parts: numbers to float (for decimals like 10.0), others as strings
74+
def convert_part(part):
75+
try:
76+
return float(part) if '.' in part else int(part)
77+
except ValueError:
78+
return part
79+
return [convert_part(part) for part in parts]
80+
2881
def get_available_ge(self):
82+
if self.available_ge_cache is not None:
83+
return self.available_ge_cache
2984
for attempt in range(3):
3085
try:
3186
url = 'https://api.github.com/repos/GloriousEggroll/proton-ge-custom/releases'
32-
response = requests.get(url, timeout=10)
87+
response = requests.get(url, timeout=15) # Increased timeout
3388
response.raise_for_status()
3489
releases = json.loads(response.text)
35-
return [r['tag_name'] for r in releases if 'tag_name' in r and r['tag_name'].startswith('GE-Proton')]
90+
tags = [r['tag_name'] for r in releases if 'tag_name' in r and r['tag_name'].startswith('GE-Proton')]
91+
tags.sort(key=self._version_key, reverse=True)
92+
self.available_ge_cache = tags
93+
return tags
3694
except Exception as e:
37-
print(f"Error fetching GE protons (attempt {attempt+1}/3): {e}")
38-
time.sleep(2)
95+
logging.error(f"Error fetching GE protons (attempt {attempt+1}/3): {e}")
96+
time.sleep(3) # Slightly longer delay
97+
logging.warning("Failed to fetch GE protons after retries, returning empty list")
98+
self.available_ge_cache = []
3999
return []
40-
def get_available_official(self):
100+
101+
def get_available_official(self, stable=True):
102+
if stable and self.available_official_stable_cache is not None:
103+
return self.available_official_stable_cache
104+
if not stable and self.available_official_exp_cache is not None:
105+
return self.available_official_exp_cache
41106
for attempt in range(3):
42107
try:
43108
url = 'https://api.github.com/repos/ValveSoftware/Proton/releases'
44-
response = requests.get(url, timeout=10)
109+
response = requests.get(url, timeout=15) # Increased timeout
45110
response.raise_for_status()
46111
releases = json.loads(response.text)
47-
return [r['tag_name'] for r in releases if 'tag_name' in r and ('experimental' in r['tag_name'].lower() or 'hotfix' in r['tag_name'].lower() or r['tag_name'].startswith('Proton-'))]
112+
filtered = [r['tag_name'] for r in releases if 'tag_name' in r]
113+
if stable:
114+
filtered = [t for t in filtered if 'experimental' not in t.lower() and 'hotfix' not in t.lower()]
115+
else:
116+
filtered = [t for t in filtered if 'experimental' in t.lower() or 'hotfix' in t.lower()]
117+
filtered.sort(key=self._version_key, reverse=True)
118+
if stable:
119+
self.available_official_stable_cache = filtered
120+
else:
121+
self.available_official_exp_cache = filtered
122+
return filtered
48123
except Exception as e:
49-
print(f"Error fetching official protons (attempt {attempt+1}/3): {e}")
50-
time.sleep(2)
124+
logging.error(f"Error fetching official protons (attempt {attempt+1}/3): {e}")
125+
time.sleep(3)
126+
logging.warning(f"Failed to fetch {'stable' if stable else 'experimental'} official protons after retries, returning empty list")
127+
if stable:
128+
self.available_official_stable_cache = []
129+
else:
130+
self.available_official_exp_cache = []
51131
return []
132+
52133
def install_proton(self, version, proton_type, progress_callback=None):
53134
try:
54135
repo = 'GloriousEggroll/proton-ge-custom' if proton_type == 'GE' else 'ValveSoftware/Proton'
55136
url = f'https://api.github.com/repos/{repo}/releases'
56-
response = requests.get(url, timeout=10)
137+
response = requests.get(url, timeout=15)
57138
response.raise_for_status()
58139
releases = json.loads(response.text)
59140
selected_release = next((r for r in releases if r['tag_name'] == version), None)
60141
if not selected_release:
61-
print(f"No release found for {version}")
142+
logging.error(f"No release found for {version}")
62143
return False, f"No release found for {version}"
63144
assets = selected_release['assets']
64145
tar_asset = next((a for a in assets if a['name'].endswith('.tar.gz')), None)
65146
if not tar_asset:
66-
print(f"No tar.gz asset found for {version}")
147+
logging.error(f"No tar.gz asset found for {version}")
67148
return False, f"No tar.gz asset found for {version}"
68149
dl_url = tar_asset['browser_download_url']
69150
progress_callback("Downloading", 0, 100)
@@ -82,67 +163,106 @@ def install_proton(self, version, proton_type, progress_callback=None):
82163
progress_callback("Downloading", downloaded, total_size)
83164
temp_tar_path = temp_tar.name
84165
progress_callback("Extracting", 0, 100)
166+
extract_dir = os.path.join(self.protons_dir, version)
167+
os.makedirs(extract_dir, exist_ok=True)
85168
with tarfile.open(temp_tar_path) as tar:
169+
for member in tar.getmembers():
170+
member_path = os.path.join(extract_dir, member.name)
171+
abs_path = os.path.abspath(member_path)
172+
if not abs_path.startswith(os.path.abspath(extract_dir)):
173+
raise Exception("Path traversal detected in tar file")
86174
total_size = sum(m.size for m in tar.getmembers())
87175
extracted = 0
88176
for member in tar.getmembers():
89-
tar.extract(member, self.protons_dir)
177+
tar.extract(member, extract_dir)
90178
extracted += member.size
91179
if progress_callback and total_size:
92180
progress_callback("Extracting", extracted, total_size)
181+
subdirs = [d for d in os.listdir(extract_dir) if os.path.isdir(os.path.join(extract_dir, d))]
182+
if len(subdirs) == 1:
183+
subdir = os.path.join(extract_dir, subdirs[0])
184+
for item in os.listdir(subdir):
185+
shutil.move(os.path.join(subdir, item), extract_dir)
186+
os.rmdir(subdir)
93187
os.remove(temp_tar_path)
94-
# Verify proton binary exists
95-
proton_path = os.path.join(self.protons_dir, version, 'proton')
96-
if not os.path.exists(proton_path):
188+
if not os.path.exists(self.get_proton_path(version)):
189+
shutil.rmtree(extract_dir)
97190
return False, f"Proton binary not found after extraction for {version}"
191+
logging.info(f"Installed Proton {version} ({proton_type})")
98192
return True, "Success"
99193
except Exception as e:
100-
print(f"Error installing {proton_type} proton {version}: {e}")
194+
logging.error(f"Error installing {proton_type} proton {version}: {e}")
101195
return False, str(e)
196+
102197
def install_custom_tar(self, tar_path, version, progress_callback=None):
103198
try:
199+
extract_dir = os.path.join(self.protons_dir, version)
200+
os.makedirs(extract_dir, exist_ok=True)
104201
progress_callback("Extracting", 0, 100)
105202
with tarfile.open(tar_path) as tar:
203+
for member in tar.getmembers():
204+
member_path = os.path.join(extract_dir, member.name)
205+
abs_path = os.path.abspath(member_path)
206+
if not abs_path.startswith(os.path.abspath(extract_dir)):
207+
raise Exception("Path traversal detected in tar file")
106208
total_size = sum(m.size for m in tar.getmembers())
107209
extracted = 0
108210
for member in tar.getmembers():
109-
tar.extract(member, self.protons_dir)
211+
tar.extract(member, extract_dir)
110212
extracted += member.size
111213
if progress_callback and total_size:
112214
progress_callback("Extracting", extracted, total_size)
113-
proton_path = os.path.join(self.protons_dir, version, 'proton')
114-
if not os.path.exists(proton_path):
215+
subdirs = [d for d in os.listdir(extract_dir) if os.path.isdir(os.path.join(extract_dir, d))]
216+
if len(subdirs) == 1:
217+
subdir = os.path.join(extract_dir, subdirs[0])
218+
for item in os.listdir(subdir):
219+
shutil.move(os.path.join(subdir, item), extract_dir)
220+
os.rmdir(subdir)
221+
if not os.path.exists(self.get_proton_path(version)):
222+
shutil.rmtree(extract_dir)
115223
return False, f"Proton binary not found after extraction for {version}"
224+
logging.info(f"Installed custom Proton {version} from tar")
116225
return True, "Success"
117226
except Exception as e:
118-
print(f"Error installing custom tar: {e}")
227+
logging.error(f"Error installing custom tar: {e}")
119228
return False, str(e)
229+
120230
def install_custom_folder(self, src_folder, version):
121231
try:
122232
dest = os.path.join(self.protons_dir, version)
123-
shutil.copytree(src_folder, dest)
124-
proton_path = os.path.join(self.protons_dir, version, 'proton')
125-
if not os.path.exists(proton_path):
233+
shutil.copytree(src_folder, dest, dirs_exist_ok=True)
234+
if not os.path.exists(self.get_proton_path(version)):
126235
shutil.rmtree(dest)
127236
return False, f"Proton binary not found in folder for {version}"
237+
logging.info(f"Installed custom Proton {version} from folder")
128238
return True, "Success"
129239
except Exception as e:
130-
print(f"Error installing custom folder: {e}")
240+
logging.error(f"Error installing custom folder: {e}")
131241
return False, str(e)
242+
132243
def remove_proton(self, version):
244+
path = os.path.join(self.protons_dir, version)
245+
if not os.path.exists(path):
246+
return False
133247
try:
134-
shutil.rmtree(os.path.join(self.protons_dir, version))
248+
shutil.rmtree(path)
249+
logging.info(f"Removed Proton {version}")
135250
return True
136251
except Exception as e:
137-
print(f"Error removing proton: {e}")
252+
logging.error(f"Error removing proton: {e}")
138253
return False
254+
139255
def check_update(self, version, proton_type):
256+
self.refresh_cache_if_needed()
140257
if proton_type == 'GE':
141-
available = self.get_available_ge()
258+
available = self.available_ge_cache
142259
if available and available[0] != version:
143260
return ('GE', available[0])
144261
elif proton_type == 'Official':
145-
available = self.get_available_official()
262+
available_stable = self.available_official_stable_cache
263+
available_exp = self.available_official_exp_cache
264+
available = available_stable + available_exp
146265
if available and available[0] != version:
147-
return ('Official', available[0])
266+
new_type = 'Official' if available[0] in available_stable else 'Experimental'
267+
return (new_type, available[0])
148268
return None

0 commit comments

Comments
 (0)