pyappify 1.0.2__tar.gz → 1.0.4__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyappify
3
- Version: 1.0.2
3
+ Version: 1.0.4
4
4
  Summary: My awesome Python application
5
5
  Classifier: Programming Language :: Python :: 3
6
6
  Classifier: License :: OSI Approved :: MIT License
@@ -0,0 +1,279 @@
1
+ # pyappify/__init__.py
2
+ import os
3
+ import signal
4
+ import hashlib
5
+ import json
6
+ import logging
7
+ import shutil
8
+ import urllib.request
9
+ import zipfile
10
+ import threading
11
+ import time
12
+
13
+ app_version = os.environ.get("PYAPPIFY_APP_VERSION")
14
+ app_starting_version = os.environ.get("PYAPPIFY_APP_STARTING_VERSION") or app_version
15
+ update_note = os.environ.get("PYAPPIFY_UPDATE_NOTE")
16
+ app_profile = os.environ.get("PYAPPIFY_APP_PROFILE")
17
+ pyappify_version = os.environ.get("PYAPPIFY_VERSION")
18
+ pyappify_executable = os.environ.get("PYAPPIFY_EXECUTABLE")
19
+
20
+ pyappify_upgradeable = os.environ.get("PYAPPIFY_UPGRADEABLE") == '1'
21
+ logger = None
22
+ _console_logger = None
23
+
24
+ try:
25
+ pid = int(os.environ.get("PYAPPIFY_PID"))
26
+ except (ValueError, TypeError):
27
+ pid = None
28
+
29
+ import sys
30
+
31
+ try:
32
+ import ctypes
33
+ except ImportError:
34
+ ctypes = None
35
+
36
+
37
+ def _get_logger():
38
+ global _console_logger
39
+
40
+ if logger is not None:
41
+ return logger
42
+ if _console_logger is None:
43
+ _console_logger = logging.getLogger("pyappify")
44
+ if not _console_logger.handlers:
45
+ handler = logging.StreamHandler()
46
+ handler.setFormatter(logging.Formatter("%(levelname)s: %(message)s"))
47
+ _console_logger.addHandler(handler)
48
+ _console_logger.setLevel(logging.INFO)
49
+ _console_logger.propagate = False
50
+ return _console_logger
51
+
52
+
53
+ def minimize_window_by_pid(pid):
54
+ if not ctypes or sys.platform != "win32":
55
+ return False
56
+
57
+ found_hwnd = []
58
+ EnumWindowsProc = ctypes.WINFUNCTYPE(ctypes.c_bool, ctypes.POINTER(ctypes.c_int), ctypes.POINTER(ctypes.c_int))
59
+
60
+ def enum_windows_callback(hwnd, lParam):
61
+ owner_pid = ctypes.c_ulong()
62
+ ctypes.windll.user32.GetWindowThreadProcessId(hwnd, ctypes.byref(owner_pid))
63
+ if owner_pid.value == pid and ctypes.windll.user32.IsWindowVisible(hwnd):
64
+ found_hwnd.append(hwnd)
65
+ return False
66
+ return True
67
+
68
+ ctypes.windll.user32.EnumWindows(EnumWindowsProc(enum_windows_callback), 0)
69
+
70
+ if found_hwnd:
71
+ ctypes.windll.user32.ShowWindow(found_hwnd[0], 6)
72
+ return True
73
+
74
+ return False
75
+
76
+ def kill_pyappify():
77
+ if pid:
78
+ log = _get_logger()
79
+ log.info(f"Attempting to terminate process with PID: {pid}")
80
+ try:
81
+ os.kill(pid, signal.SIGTERM)
82
+ if not _wait_for_process_exit(pid):
83
+ log.warning(f"Timed out waiting for process with PID {pid} to exit.")
84
+ except Exception as e:
85
+ log.error(f"Failed to terminate process with PID {pid}: {e}")
86
+ pass
87
+
88
+
89
+ def _wait_for_process_exit(process_pid, timeout=30):
90
+ if sys.platform == "win32" and ctypes:
91
+ synchronize = 0x00100000
92
+ wait_timeout = 0x00000102
93
+ wait_failed = 0xFFFFFFFF
94
+ ctypes.windll.kernel32.OpenProcess.argtypes = (
95
+ ctypes.c_uint,
96
+ ctypes.c_bool,
97
+ ctypes.c_ulong,
98
+ )
99
+ ctypes.windll.kernel32.OpenProcess.restype = ctypes.c_void_p
100
+ ctypes.windll.kernel32.WaitForSingleObject.argtypes = (
101
+ ctypes.c_void_p,
102
+ ctypes.c_uint,
103
+ )
104
+ ctypes.windll.kernel32.WaitForSingleObject.restype = ctypes.c_uint
105
+ ctypes.windll.kernel32.CloseHandle.argtypes = (ctypes.c_void_p,)
106
+ ctypes.windll.kernel32.CloseHandle.restype = ctypes.c_bool
107
+ handle = ctypes.windll.kernel32.OpenProcess(synchronize, False, process_pid)
108
+ if handle:
109
+ try:
110
+ result = ctypes.windll.kernel32.WaitForSingleObject(
111
+ handle, int(timeout * 1000)
112
+ )
113
+ if result == wait_failed:
114
+ return False
115
+ return result != wait_timeout
116
+ finally:
117
+ ctypes.windll.kernel32.CloseHandle(handle)
118
+
119
+ deadline = time.monotonic() + timeout
120
+ while time.monotonic() < deadline:
121
+ try:
122
+ os.kill(process_pid, 0)
123
+ except OSError:
124
+ return True
125
+ time.sleep(0.1)
126
+ return False
127
+
128
+
129
+ def _replace_executable(source_path, target_path, timeout=30):
130
+ deadline = time.monotonic() + timeout
131
+ last_error = None
132
+ while True:
133
+ try:
134
+ shutil.move(source_path, target_path)
135
+ return
136
+ except PermissionError as e:
137
+ last_error = e
138
+ if time.monotonic() >= deadline:
139
+ raise last_error
140
+ time.sleep(0.25)
141
+
142
+
143
+ def hide_pyappify():
144
+ if pid:
145
+ log = _get_logger()
146
+ log.info(f"Attempting to minimize window for process with PID: {pid}")
147
+ try:
148
+ minimize_window_by_pid(pid)
149
+ except Exception as e:
150
+ log.error(f"Failed to minimize window for process with PID {pid}: {e}")
151
+ pass
152
+
153
+ def upgrade(to_version, executable_sha256, executable_zip_urls, stop_event=None):
154
+ log = _get_logger()
155
+ if not pyappify_upgradeable or not is_greater_version(to_version, pyappify_version):
156
+ log.info(f"pyappify no need to upgrade {pyappify_upgradeable} {to_version} {executable_sha256} {executable_zip_urls}")
157
+ return
158
+ log.info(
159
+ f"pyappify start to upgrade {pyappify_upgradeable} {to_version} {executable_sha256} {executable_zip_urls}")
160
+ def _do_upgrade():
161
+ tmp_dir = os.path.join(os.getcwd(), "pyappify_tmp")
162
+ try:
163
+ os.makedirs(tmp_dir, exist_ok=True)
164
+ downloaded_zip_path = None
165
+ for url in executable_zip_urls:
166
+ try:
167
+ log.info(
168
+ f"pyappify start to download {url}")
169
+ local_zip_path = os.path.join(tmp_dir, os.path.basename(url))
170
+ with urllib.request.urlopen(url) as response, open(local_zip_path, 'wb') as out_file:
171
+ while True:
172
+ if stop_event and stop_event.is_set():
173
+ log.info("pyappify Upgrade download cancelled by stop event.")
174
+ return
175
+ chunk = response.read(8192)
176
+ if not chunk:
177
+ break
178
+ out_file.write(chunk)
179
+ downloaded_zip_path = local_zip_path
180
+ log.info(
181
+ f"pyappify download success {url}")
182
+ break
183
+ except Exception as e:
184
+ log.warning(f"pyappify Failed to download from {url}: {e}")
185
+ continue
186
+
187
+ if not downloaded_zip_path:
188
+ log.error("pyappify Failed to download upgrade.")
189
+ return
190
+
191
+ with zipfile.ZipFile(downloaded_zip_path, 'r') as zip_ref:
192
+ zip_ref.extractall(tmp_dir)
193
+
194
+ new_executable_name = os.path.basename(pyappify_executable)
195
+ found_executable_path = None
196
+ for root, _, files in os.walk(tmp_dir):
197
+ if new_executable_name in files:
198
+ found_executable_path = os.path.join(root, new_executable_name)
199
+ break
200
+
201
+ if not found_executable_path:
202
+ log.error("pyappify Executable not found in zip.")
203
+ return
204
+
205
+ sha256_hash = hashlib.sha256()
206
+ with open(found_executable_path, "rb") as f:
207
+ for byte_block in iter(lambda: f.read(4096), b""):
208
+ sha256_hash.update(byte_block)
209
+
210
+ if executable_sha256 and sha256_hash.hexdigest() != executable_sha256:
211
+ log.error("pyappify SHA256 checksum mismatch.")
212
+ return
213
+
214
+ kill_pyappify()
215
+ _replace_executable(found_executable_path, pyappify_executable)
216
+ log.info(f"pyappify Upgrade success")
217
+ except Exception as e:
218
+ log.error(f"pyappify Upgrade failed: {e}")
219
+ finally:
220
+ if os.path.exists(tmp_dir):
221
+ shutil.rmtree(tmp_dir)
222
+
223
+ thread = threading.Thread(target=_do_upgrade)
224
+ thread.daemon = True
225
+ thread.start()
226
+
227
+
228
+ def is_app_updated():
229
+ return is_greater_version(app_version, app_starting_version)
230
+
231
+
232
+ def is_app_downgraded():
233
+ return is_greater_version(app_starting_version, app_version)
234
+
235
+
236
+ def is_updated():
237
+ return is_app_updated()
238
+
239
+
240
+ def is_downgrade():
241
+ return is_app_downgraded()
242
+
243
+
244
+ def get_update_notes():
245
+ if not update_note:
246
+ return []
247
+ try:
248
+ notes = json.loads(update_note)
249
+ except (TypeError, ValueError):
250
+ return []
251
+ if isinstance(notes, list):
252
+ return [str(note) for note in notes]
253
+ return []
254
+
255
+
256
+ def get_update_note():
257
+ return get_update_notes()
258
+
259
+
260
+ def is_greater_version(version1, version2):
261
+ """
262
+ Compares two semantic version strings.
263
+
264
+ Args:
265
+ version1 (str): The first version string.
266
+ version2 (str): The second version string.
267
+
268
+ Returns:
269
+ bool: True if version1 is strictly greater than version2,
270
+ False otherwise or if parsing fails.
271
+ """
272
+ try:
273
+ version1 = version1.lstrip('v')
274
+ version2 = version2.lstrip('v')
275
+ v1_parts = [int(p) for p in version1.split('.')]
276
+ v2_parts = [int(p) for p in version2.split('.')]
277
+ return v1_parts > v2_parts
278
+ except (ValueError, AttributeError):
279
+ return False
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyappify
3
- Version: 1.0.2
3
+ Version: 1.0.4
4
4
  Summary: My awesome Python application
5
5
  Classifier: Programming Language :: Python :: 3
6
6
  Classifier: License :: OSI Approved :: MIT License
@@ -7,4 +7,7 @@ pyappify.egg-info/dependency_links.txt
7
7
  pyappify.egg-info/entry_points.txt
8
8
  pyappify.egg-info/requires.txt
9
9
  pyappify.egg-info/top_level.txt
10
+ tests/TestUpdateEnv.py
11
+ tests/TestUpgrade.py
12
+ tests/TestUpgradeOnline.py
10
13
  tests/TestVersion.py
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "pyappify"
7
- version = "1.0.2"
7
+ version = "1.0.4"
8
8
  description = "My awesome Python application"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.7"
@@ -22,4 +22,4 @@ dev = ["twine", "build"]
22
22
 
23
23
  [tool.setuptools.packages.find]
24
24
  where = ["."]
25
- include = ["pyappify"]
25
+ include = ["pyappify"]
@@ -0,0 +1,94 @@
1
+ import importlib
2
+ import os
3
+ import unittest
4
+
5
+ import pyappify
6
+
7
+
8
+ ENV_KEYS = (
9
+ "PYAPPIFY_APP_VERSION",
10
+ "PYAPPIFY_APP_STARTING_VERSION",
11
+ "PYAPPIFY_UPDATE_NOTE",
12
+ )
13
+
14
+
15
+ class TestUpdateEnv(unittest.TestCase):
16
+ def setUp(self):
17
+ self._saved_env = {key: os.environ.get(key) for key in ENV_KEYS}
18
+
19
+ def tearDown(self):
20
+ for key, value in self._saved_env.items():
21
+ if value is None:
22
+ os.environ.pop(key, None)
23
+ else:
24
+ os.environ[key] = value
25
+ importlib.reload(pyappify)
26
+
27
+ def _reload_with_env(self, **env):
28
+ for key in ENV_KEYS:
29
+ os.environ.pop(key, None)
30
+ for key, value in env.items():
31
+ os.environ[key] = value
32
+ return importlib.reload(pyappify)
33
+
34
+ def test_starting_version_falls_back_to_app_version(self):
35
+ module = self._reload_with_env(PYAPPIFY_APP_VERSION="1.2.3")
36
+
37
+ self.assertEqual("1.2.3", module.app_version)
38
+ self.assertEqual("1.2.3", module.app_starting_version)
39
+ self.assertFalse(module.is_app_updated())
40
+ self.assertFalse(module.is_updated())
41
+ self.assertFalse(module.is_app_downgraded())
42
+ self.assertFalse(module.is_downgrade())
43
+
44
+ def test_missing_versions_are_not_update_or_downgrade(self):
45
+ module = self._reload_with_env()
46
+
47
+ self.assertIsNone(module.app_version)
48
+ self.assertIsNone(module.app_starting_version)
49
+ self.assertFalse(module.is_app_updated())
50
+ self.assertFalse(module.is_app_downgraded())
51
+
52
+ def test_detects_update_and_downgrade(self):
53
+ module = self._reload_with_env(
54
+ PYAPPIFY_APP_VERSION="2.0.0",
55
+ PYAPPIFY_APP_STARTING_VERSION="1.0.0",
56
+ )
57
+
58
+ self.assertTrue(module.is_app_updated())
59
+ self.assertTrue(module.is_updated())
60
+ self.assertFalse(module.is_app_downgraded())
61
+ self.assertFalse(module.is_downgrade())
62
+
63
+ module = self._reload_with_env(
64
+ PYAPPIFY_APP_VERSION="1.0.0",
65
+ PYAPPIFY_APP_STARTING_VERSION="2.0.0",
66
+ )
67
+
68
+ self.assertFalse(module.is_app_updated())
69
+ self.assertFalse(module.is_updated())
70
+ self.assertTrue(module.is_app_downgraded())
71
+ self.assertTrue(module.is_downgrade())
72
+
73
+ def test_returns_update_notes_from_json_array(self):
74
+ module = self._reload_with_env(
75
+ PYAPPIFY_UPDATE_NOTE='["first change", "second change"]'
76
+ )
77
+
78
+ self.assertEqual(["first change", "second change"], module.get_update_notes())
79
+ self.assertEqual(["first change", "second change"], module.get_update_note())
80
+
81
+ def test_invalid_or_missing_update_notes_return_empty_list(self):
82
+ self.assertEqual([], self._reload_with_env().get_update_notes())
83
+ self.assertEqual(
84
+ [],
85
+ self._reload_with_env(PYAPPIFY_UPDATE_NOTE="not json").get_update_notes(),
86
+ )
87
+ self.assertEqual(
88
+ [],
89
+ self._reload_with_env(PYAPPIFY_UPDATE_NOTE='"single note"').get_update_notes(),
90
+ )
91
+
92
+
93
+ if __name__ == "__main__":
94
+ unittest.main()
@@ -0,0 +1,110 @@
1
+ import hashlib
2
+ import logging
3
+ import os
4
+ import shutil
5
+ import subprocess
6
+ import tempfile
7
+ import time
8
+ import unittest
9
+ from pathlib import Path
10
+
11
+ import pyappify
12
+
13
+
14
+ EXECUTABLE_SHA256 = "50ef2557c193ca1f97c1e699bab3dad35b3966c9e8e8b860fb609bd5fb3861d1"
15
+
16
+
17
+ def _sha256(path):
18
+ digest = hashlib.sha256()
19
+ with open(path, "rb") as file:
20
+ for block in iter(lambda: file.read(1024 * 1024), b""):
21
+ digest.update(block)
22
+ return digest.hexdigest()
23
+
24
+
25
+ def assert_upgrade_kills_running_executable_and_replaces_it(
26
+ test_case, zip_url, timeout_seconds
27
+ ):
28
+ source_executable = Path(os.environ["WINDIR"]) / "System32" / "PING.EXE"
29
+ test_case.assertTrue(source_executable.exists(), "Windows PING.EXE is required")
30
+
31
+ old_cwd = os.getcwd()
32
+ old_upgradeable = pyappify.pyappify_upgradeable
33
+ old_version = pyappify.pyappify_version
34
+ old_executable = pyappify.pyappify_executable
35
+ old_pid = pyappify.pid
36
+ old_logger = pyappify.logger
37
+
38
+ with tempfile.TemporaryDirectory() as temporary_directory:
39
+ executable = Path(temporary_directory) / "ok-ww.exe"
40
+ upgrade_temporary_directory = Path(temporary_directory) / "pyappify_tmp"
41
+ shutil.copy2(source_executable, executable)
42
+ test_case.assertNotEqual(_sha256(executable), EXECUTABLE_SHA256)
43
+
44
+ process = subprocess.Popen(
45
+ [str(executable), "-t", "127.0.0.1"],
46
+ cwd=temporary_directory,
47
+ stdout=subprocess.DEVNULL,
48
+ stderr=subprocess.DEVNULL,
49
+ )
50
+ try:
51
+ time.sleep(1)
52
+ test_case.assertIsNone(
53
+ process.poll(), "the current executable exited before upgrade began"
54
+ )
55
+
56
+ pyappify.pyappify_upgradeable = True
57
+ pyappify.pyappify_version = "v3.3.45"
58
+ pyappify.pyappify_executable = str(executable)
59
+ pyappify.pid = process.pid
60
+ pyappify.logger = logging.getLogger(__name__)
61
+ os.chdir(temporary_directory)
62
+
63
+ pyappify.upgrade("v3.3.46", EXECUTABLE_SHA256, [zip_url])
64
+
65
+ deadline = time.monotonic() + timeout_seconds
66
+ while time.monotonic() < deadline:
67
+ process_stopped = process.poll() is not None
68
+ upgraded = (
69
+ executable.exists()
70
+ and _sha256(executable) == EXECUTABLE_SHA256
71
+ )
72
+ update_finished = not upgrade_temporary_directory.exists()
73
+ if process_stopped and upgraded and update_finished:
74
+ break
75
+ time.sleep(0.1)
76
+
77
+ test_case.assertIsNotNone(
78
+ process.poll(), "upgrade did not stop the old process"
79
+ )
80
+ test_case.assertEqual(EXECUTABLE_SHA256, _sha256(executable))
81
+ finally:
82
+ os.chdir(old_cwd)
83
+ pyappify.pyappify_upgradeable = old_upgradeable
84
+ pyappify.pyappify_version = old_version
85
+ pyappify.pyappify_executable = old_executable
86
+ pyappify.pid = old_pid
87
+ pyappify.logger = old_logger
88
+ if process.poll() is None:
89
+ process.terminate()
90
+ try:
91
+ process.wait(timeout=5)
92
+ except subprocess.TimeoutExpired:
93
+ process.kill()
94
+ process.wait(timeout=5)
95
+
96
+
97
+ class TestUpgrade(unittest.TestCase):
98
+ LOCAL_ZIP = Path(__file__).with_name("ok-ww-win32.zip")
99
+
100
+ @unittest.skipUnless(os.name == "nt", "executable replacement test is Windows-only")
101
+ def test_upgrade_from_local_zip_kills_running_executable_and_replaces_it(self):
102
+ self.assertTrue(self.LOCAL_ZIP.exists(), "tests/ok-ww-win32.zip is required")
103
+ assert_upgrade_kills_running_executable_and_replaces_it(
104
+ self,
105
+ self.LOCAL_ZIP.as_uri(), timeout_seconds=30
106
+ )
107
+
108
+
109
+ if __name__ == "__main__":
110
+ unittest.main()
@@ -0,0 +1,21 @@
1
+ import os
2
+ import unittest
3
+
4
+ from tests.TestUpgrade import assert_upgrade_kills_running_executable_and_replaces_it
5
+
6
+
7
+ class TestUpgradeOnline(unittest.TestCase):
8
+ ZIP_URL = (
9
+ "https://github.com/ok-oldking/ok-wuthering-waves/"
10
+ "releases/download/v3.3.46/ok-ww-win32.zip"
11
+ )
12
+
13
+ @unittest.skipUnless(os.name == "nt", "executable replacement test is Windows-only")
14
+ def test_upgrade_from_online_zip_kills_running_executable_and_replaces_it(self):
15
+ assert_upgrade_kills_running_executable_and_replaces_it(
16
+ self, self.ZIP_URL, timeout_seconds=600
17
+ )
18
+
19
+
20
+ if __name__ == "__main__":
21
+ unittest.main()
@@ -1,179 +0,0 @@
1
- # pyappify/__init__.py
2
- import os
3
- import signal
4
- import hashlib
5
- import shutil
6
- import urllib.request
7
- import zipfile
8
- import threading
9
-
10
- app_version = os.environ.get("PYAPPIFY_APP_VERSION")
11
- app_profile = os.environ.get("PYAPPIFY_APP_PROFILE")
12
- pyappify_version = os.environ.get("PYAPPIFY_VERSION")
13
- pyappify_executable = os.environ.get("PYAPPIFY_EXECUTABLE")
14
-
15
- pyappify_upgradeable = os.environ.get("PYAPPIFY_UPGRADEABLE") == '1'
16
- logger = None
17
-
18
- try:
19
- pid = int(os.environ.get("PYAPPIFY_PID"))
20
- except (ValueError, TypeError):
21
- pid = None
22
-
23
- import sys
24
-
25
- try:
26
- import ctypes
27
- except ImportError:
28
- ctypes = None
29
-
30
-
31
- def minimize_window_by_pid(pid):
32
- if not ctypes or sys.platform != "win32":
33
- return False
34
-
35
- found_hwnd = []
36
- EnumWindowsProc = ctypes.WINFUNCTYPE(ctypes.c_bool, ctypes.POINTER(ctypes.c_int), ctypes.POINTER(ctypes.c_int))
37
-
38
- def enum_windows_callback(hwnd, lParam):
39
- owner_pid = ctypes.c_ulong()
40
- ctypes.windll.user32.GetWindowThreadProcessId(hwnd, ctypes.byref(owner_pid))
41
- if owner_pid.value == pid and ctypes.windll.user32.IsWindowVisible(hwnd):
42
- found_hwnd.append(hwnd)
43
- return False
44
- return True
45
-
46
- ctypes.windll.user32.EnumWindows(EnumWindowsProc(enum_windows_callback), 0)
47
-
48
- if found_hwnd:
49
- ctypes.windll.user32.ShowWindow(found_hwnd[0], 6)
50
- return True
51
-
52
- return False
53
-
54
- def kill_pyappify():
55
- if pid:
56
- if logger:
57
- logger.info(f"Attempting to terminate process with PID: {pid}")
58
- try:
59
- os.kill(pid, signal.SIGTERM)
60
- except Exception as e:
61
- if logger:
62
- logger.error(f"Failed to terminate process with PID {pid}: {e}")
63
- pass
64
-
65
- def hide_pyappify():
66
- if pid:
67
- if logger:
68
- logger.info(f"Attempting to minimize window for process with PID: {pid}")
69
- try:
70
- minimize_window_by_pid(pid)
71
- except Exception as e:
72
- if logger:
73
- logger.error(f"Failed to minimize window for process with PID {pid}: {e}")
74
- pass
75
-
76
- def upgrade(to_version, executable_sha256, executable_zip_urls, stop_event=None):
77
- if not pyappify_upgradeable or not is_greater_version(to_version, pyappify_version):
78
- if logger:
79
- logger.info(f"pyappify no need to upgrade {pyappify_upgradeable} {to_version} {executable_sha256} {executable_zip_urls}")
80
- return
81
- if logger:
82
- logger.info(
83
- f"pyappify start to upgrade {pyappify_upgradeable} {to_version} {executable_sha256} {executable_zip_urls}")
84
- def _do_upgrade():
85
- tmp_dir = os.path.join(os.getcwd(), "pyappify_tmp")
86
- try:
87
- os.makedirs(tmp_dir, exist_ok=True)
88
- downloaded_zip_path = None
89
- for url in executable_zip_urls:
90
- try:
91
- if logger:
92
- logger.info(
93
- f"pyappify start to download {url}")
94
- local_zip_path = os.path.join(tmp_dir, os.path.basename(url))
95
- with urllib.request.urlopen(url) as response, open(local_zip_path, 'wb') as out_file:
96
- while True:
97
- if stop_event and stop_event.is_set():
98
- if logger:
99
- logger.info("pyappify Upgrade download cancelled by stop event.")
100
- return
101
- chunk = response.read(8192)
102
- if not chunk:
103
- break
104
- out_file.write(chunk)
105
- downloaded_zip_path = local_zip_path
106
- if logger:
107
- logger.info(
108
- f"pyappify download success {url}")
109
- break
110
- except Exception as e:
111
- if logger:
112
- logger.warning(f"pyappify Failed to download from {url}: {e}")
113
- continue
114
-
115
- if not downloaded_zip_path:
116
- if logger:
117
- logger.error("pyappify Failed to download upgrade.")
118
- return
119
-
120
- with zipfile.ZipFile(downloaded_zip_path, 'r') as zip_ref:
121
- zip_ref.extractall(tmp_dir)
122
-
123
- new_executable_name = os.path.basename(pyappify_executable)
124
- found_executable_path = None
125
- for root, _, files in os.walk(tmp_dir):
126
- if new_executable_name in files:
127
- found_executable_path = os.path.join(root, new_executable_name)
128
- break
129
-
130
- if not found_executable_path:
131
- if logger:
132
- logger.error("pyappify Executable not found in zip.")
133
- return
134
-
135
- sha256_hash = hashlib.sha256()
136
- with open(found_executable_path, "rb") as f:
137
- for byte_block in iter(lambda: f.read(4096), b""):
138
- sha256_hash.update(byte_block)
139
-
140
- if executable_sha256 and sha256_hash.hexdigest() != executable_sha256:
141
- if logger:
142
- logger.error("pyappify SHA256 checksum mismatch.")
143
- return
144
-
145
- kill_pyappify()
146
- shutil.move(found_executable_path, pyappify_executable)
147
- if logger:
148
- logger.info(f"pyappify Upgrade success")
149
- except Exception as e:
150
- if logger:
151
- logger.error(f"pyappify Upgrade failed: {e}")
152
- finally:
153
- if os.path.exists(tmp_dir):
154
- shutil.rmtree(tmp_dir)
155
-
156
- thread = threading.Thread(target=_do_upgrade)
157
- thread.daemon = True
158
- thread.start()
159
-
160
- def is_greater_version(version1, version2):
161
- """
162
- Compares two semantic version strings.
163
-
164
- Args:
165
- version1 (str): The first version string.
166
- version2 (str): The second version string.
167
-
168
- Returns:
169
- bool: True if version1 is strictly greater than version2,
170
- False otherwise or if parsing fails.
171
- """
172
- try:
173
- version1 = version1.lstrip('v')
174
- version2 = version2.lstrip('v')
175
- v1_parts = [int(p) for p in version1.split('.')]
176
- v2_parts = [int(p) for p in version2.split('.')]
177
- return v1_parts > v2_parts
178
- except (ValueError, AttributeError):
179
- return False
File without changes
File without changes
File without changes