pyappify 1.0.4__tar.gz → 1.0.5__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.4
3
+ Version: 1.0.5
4
4
  Summary: My awesome Python application
5
5
  Classifier: Programming Language :: Python :: 3
6
6
  Classifier: License :: OSI Approved :: MIT License
@@ -1,10 +1,11 @@
1
- # pyappify/__init__.py
1
+ # pyappify/__init__.py
2
2
  import os
3
3
  import signal
4
4
  import hashlib
5
5
  import json
6
6
  import logging
7
7
  import shutil
8
+ import subprocess
8
9
  import urllib.request
9
10
  import zipfile
10
11
  import threading
@@ -16,20 +17,20 @@ update_note = os.environ.get("PYAPPIFY_UPDATE_NOTE")
16
17
  app_profile = os.environ.get("PYAPPIFY_APP_PROFILE")
17
18
  pyappify_version = os.environ.get("PYAPPIFY_VERSION")
18
19
  pyappify_executable = os.environ.get("PYAPPIFY_EXECUTABLE")
19
-
20
+
20
21
  pyappify_upgradeable = os.environ.get("PYAPPIFY_UPGRADEABLE") == '1'
21
22
  logger = None
22
23
  _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
24
+
25
+ try:
26
+ pid = int(os.environ.get("PYAPPIFY_PID"))
27
+ except (ValueError, TypeError):
28
+ pid = None
29
+
30
+ import sys
31
+
32
+ try:
33
+ import ctypes
33
34
  except ImportError:
34
35
  ctypes = None
35
36
 
@@ -50,40 +51,63 @@ def _get_logger():
50
51
  return _console_logger
51
52
 
52
53
 
54
+ def _find_visible_window_by_pid(process_pid):
55
+ if not ctypes or sys.platform != "win32":
56
+ return None
57
+
58
+ found_hwnd = []
59
+ EnumWindowsProc = ctypes.WINFUNCTYPE(ctypes.c_bool, ctypes.c_void_p, ctypes.c_void_p)
60
+
61
+ def enum_windows_callback(hwnd, lParam):
62
+ owner_pid = ctypes.c_ulong()
63
+ ctypes.windll.user32.GetWindowThreadProcessId(hwnd, ctypes.byref(owner_pid))
64
+ if owner_pid.value == process_pid and ctypes.windll.user32.IsWindowVisible(hwnd):
65
+ found_hwnd.append(hwnd)
66
+ return False
67
+ return True
68
+
69
+ ctypes.windll.user32.EnumWindows(EnumWindowsProc(enum_windows_callback), 0)
70
+
71
+ return found_hwnd[0] if found_hwnd else None
72
+
73
+
53
74
  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():
75
+ hwnd = _find_visible_window_by_pid(pid)
76
+ if hwnd:
77
+ ctypes.windll.user32.ShowWindow(hwnd, 6)
78
+ return True
79
+
80
+ return False
81
+
82
+
83
+ def bring_window_to_front_by_pid(pid):
84
+ hwnd = _find_visible_window_by_pid(pid)
85
+ if hwnd:
86
+ ctypes.windll.user32.ShowWindow(hwnd, 9)
87
+ return bool(ctypes.windll.user32.SetForegroundWindow(hwnd))
88
+
89
+ return False
90
+
91
+
92
+ def kill_pyappify(timeout=30):
77
93
  if pid:
78
94
  log = _get_logger()
79
95
  log.info(f"Attempting to terminate process with PID: {pid}")
80
96
  try:
81
97
  os.kill(pid, signal.SIGTERM)
82
- if not _wait_for_process_exit(pid):
98
+ if not _wait_for_process_exit(pid, timeout):
83
99
  log.warning(f"Timed out waiting for process with PID {pid} to exit.")
100
+ return False
101
+ log.info(f'_wait_for_process_exit success {pid}')
102
+ return True
84
103
  except Exception as e:
85
104
  log.error(f"Failed to terminate process with PID {pid}: {e}")
86
- pass
105
+ return False
106
+ return False
107
+
108
+
109
+ def kill_pyappify_exe(timeout=30):
110
+ return kill_pyappify(timeout)
87
111
 
88
112
 
89
113
  def _wait_for_process_exit(process_pid, timeout=30):
@@ -126,6 +150,74 @@ def _wait_for_process_exit(process_pid, timeout=30):
126
150
  return False
127
151
 
128
152
 
153
+ def _is_process_running(process_pid):
154
+ if not process_pid:
155
+ return False
156
+
157
+ if sys.platform == "win32" and ctypes:
158
+ synchronize = 0x00100000
159
+ wait_timeout = 0x00000102
160
+ ctypes.windll.kernel32.OpenProcess.argtypes = (
161
+ ctypes.c_uint,
162
+ ctypes.c_bool,
163
+ ctypes.c_ulong,
164
+ )
165
+ ctypes.windll.kernel32.OpenProcess.restype = ctypes.c_void_p
166
+ ctypes.windll.kernel32.WaitForSingleObject.argtypes = (
167
+ ctypes.c_void_p,
168
+ ctypes.c_uint,
169
+ )
170
+ ctypes.windll.kernel32.WaitForSingleObject.restype = ctypes.c_uint
171
+ ctypes.windll.kernel32.CloseHandle.argtypes = (ctypes.c_void_p,)
172
+ ctypes.windll.kernel32.CloseHandle.restype = ctypes.c_bool
173
+ handle = ctypes.windll.kernel32.OpenProcess(synchronize, False, process_pid)
174
+ if not handle:
175
+ return False
176
+ try:
177
+ return ctypes.windll.kernel32.WaitForSingleObject(handle, 0) == wait_timeout
178
+ finally:
179
+ ctypes.windll.kernel32.CloseHandle(handle)
180
+
181
+ try:
182
+ os.kill(process_pid, 0)
183
+ except OSError:
184
+ return False
185
+ return True
186
+
187
+
188
+ def show_pyappify(args=None, cwd=None, env=None):
189
+ global pid
190
+
191
+ log = _get_logger()
192
+ if _is_process_running(pid):
193
+ log.info(f"PyAppify is already running with PID: {pid}")
194
+ bring_window_to_front_by_pid(pid)
195
+ return pid
196
+
197
+ if not pyappify_executable:
198
+ log.error("PYAPPIFY_EXECUTABLE is not configured.")
199
+ return None
200
+
201
+ command = [pyappify_executable]
202
+ if args:
203
+ if isinstance(args, str):
204
+ command.append(args)
205
+ else:
206
+ command.extend(args)
207
+
208
+ try:
209
+ process = subprocess.Popen(
210
+ command,
211
+ cwd=cwd or os.path.dirname(pyappify_executable) or None,
212
+ env=env,
213
+ )
214
+ pid = process.pid
215
+ return pid
216
+ except Exception as e:
217
+ log.error(f"Failed to start PyAppify executable {pyappify_executable}: {e}")
218
+ return None
219
+
220
+
129
221
  def _replace_executable(source_path, target_path, timeout=30):
130
222
  deadline = time.monotonic() + timeout
131
223
  last_error = None
@@ -160,7 +252,7 @@ def upgrade(to_version, executable_sha256, executable_zip_urls, stop_event=None)
160
252
  def _do_upgrade():
161
253
  tmp_dir = os.path.join(os.getcwd(), "pyappify_tmp")
162
254
  try:
163
- os.makedirs(tmp_dir, exist_ok=True)
255
+ os.makedirs(tmp_dir, exist_ok=True)
164
256
  downloaded_zip_path = None
165
257
  for url in executable_zip_urls:
166
258
  try:
@@ -172,10 +264,10 @@ def upgrade(to_version, executable_sha256, executable_zip_urls, stop_event=None)
172
264
  if stop_event and stop_event.is_set():
173
265
  log.info("pyappify Upgrade download cancelled by stop event.")
174
266
  return
175
- chunk = response.read(8192)
176
- if not chunk:
177
- break
178
- out_file.write(chunk)
267
+ chunk = response.read(8192)
268
+ if not chunk:
269
+ break
270
+ out_file.write(chunk)
179
271
  downloaded_zip_path = local_zip_path
180
272
  log.info(
181
273
  f"pyappify download success {url}")
@@ -187,26 +279,26 @@ def upgrade(to_version, executable_sha256, executable_zip_urls, stop_event=None)
187
279
  if not downloaded_zip_path:
188
280
  log.error("pyappify Failed to download upgrade.")
189
281
  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
-
282
+
283
+ with zipfile.ZipFile(downloaded_zip_path, 'r') as zip_ref:
284
+ zip_ref.extractall(tmp_dir)
285
+
286
+ new_executable_name = os.path.basename(pyappify_executable)
287
+ found_executable_path = None
288
+ for root, _, files in os.walk(tmp_dir):
289
+ if new_executable_name in files:
290
+ found_executable_path = os.path.join(root, new_executable_name)
291
+ break
292
+
201
293
  if not found_executable_path:
202
294
  log.error("pyappify Executable not found in zip.")
203
295
  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
-
296
+
297
+ sha256_hash = hashlib.sha256()
298
+ with open(found_executable_path, "rb") as f:
299
+ for byte_block in iter(lambda: f.read(4096), b""):
300
+ sha256_hash.update(byte_block)
301
+
210
302
  if executable_sha256 and sha256_hash.hexdigest() != executable_sha256:
211
303
  log.error("pyappify SHA256 checksum mismatch.")
212
304
  return
@@ -216,10 +308,10 @@ def upgrade(to_version, executable_sha256, executable_zip_urls, stop_event=None)
216
308
  log.info(f"pyappify Upgrade success")
217
309
  except Exception as e:
218
310
  log.error(f"pyappify Upgrade failed: {e}")
219
- finally:
220
- if os.path.exists(tmp_dir):
221
- shutil.rmtree(tmp_dir)
222
-
311
+ finally:
312
+ if os.path.exists(tmp_dir):
313
+ shutil.rmtree(tmp_dir)
314
+
223
315
  thread = threading.Thread(target=_do_upgrade)
224
316
  thread.daemon = True
225
317
  thread.start()
@@ -260,20 +352,20 @@ def get_update_note():
260
352
  def is_greater_version(version1, version2):
261
353
  """
262
354
  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):
355
+
356
+ Args:
357
+ version1 (str): The first version string.
358
+ version2 (str): The second version string.
359
+
360
+ Returns:
361
+ bool: True if version1 is strictly greater than version2,
362
+ False otherwise or if parsing fails.
363
+ """
364
+ try:
365
+ version1 = version1.lstrip('v')
366
+ version2 = version2.lstrip('v')
367
+ v1_parts = [int(p) for p in version1.split('.')]
368
+ v2_parts = [int(p) for p in version2.split('.')]
369
+ return v1_parts > v2_parts
370
+ except (ValueError, AttributeError):
279
371
  return False
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyappify
3
- Version: 1.0.4
3
+ Version: 1.0.5
4
4
  Summary: My awesome Python application
5
5
  Classifier: Programming Language :: Python :: 3
6
6
  Classifier: License :: OSI Approved :: MIT License
@@ -7,6 +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/TestProcessControls.py
10
11
  tests/TestUpdateEnv.py
11
12
  tests/TestUpgrade.py
12
13
  tests/TestUpgradeOnline.py
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "pyappify"
7
- version = "1.0.4"
7
+ version = "1.0.5"
8
8
  description = "My awesome Python application"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.7"
@@ -0,0 +1,77 @@
1
+ import os
2
+ import signal
3
+ import unittest
4
+ from unittest import mock
5
+
6
+ import pyappify
7
+
8
+
9
+ class TestProcessControls(unittest.TestCase):
10
+ def setUp(self):
11
+ self.old_pid = pyappify.pid
12
+ self.old_executable = pyappify.pyappify_executable
13
+ self.old_logger = pyappify.logger
14
+ pyappify.logger = mock.Mock()
15
+
16
+ def tearDown(self):
17
+ pyappify.pid = self.old_pid
18
+ pyappify.pyappify_executable = self.old_executable
19
+ pyappify.logger = self.old_logger
20
+
21
+ def test_kill_pyappify_exe_terminates_configured_pid(self):
22
+ pyappify.pid = 1234
23
+
24
+ with mock.patch.object(pyappify.os, "kill") as kill, mock.patch.object(
25
+ pyappify, "_wait_for_process_exit", return_value=True
26
+ ) as wait:
27
+ self.assertTrue(pyappify.kill_pyappify_exe(timeout=5))
28
+
29
+ kill.assert_called_once_with(1234, signal.SIGTERM)
30
+ wait.assert_called_once_with(1234, 5)
31
+
32
+ def test_show_pyappify_brings_existing_window_to_front(self):
33
+ pyappify.pid = 1234
34
+
35
+ with mock.patch.object(
36
+ pyappify, "_is_process_running", return_value=True
37
+ ) as is_running, mock.patch.object(
38
+ pyappify, "bring_window_to_front_by_pid", return_value=True
39
+ ) as bring_to_front, mock.patch.object(pyappify.subprocess, "Popen") as popen:
40
+ self.assertEqual(1234, pyappify.show_pyappify())
41
+
42
+ is_running.assert_called_once_with(1234)
43
+ bring_to_front.assert_called_once_with(1234)
44
+ popen.assert_not_called()
45
+
46
+ def test_show_pyappify_starts_executable_when_not_running(self):
47
+ executable = os.path.abspath("pyappify.exe")
48
+ pyappify.pid = 1234
49
+ pyappify.pyappify_executable = executable
50
+ process = mock.Mock()
51
+ process.pid = 5678
52
+
53
+ with mock.patch.object(
54
+ pyappify, "_is_process_running", return_value=False
55
+ ), mock.patch.object(pyappify.subprocess, "Popen", return_value=process) as popen:
56
+ self.assertEqual(
57
+ 5678,
58
+ pyappify.show_pyappify(args=["--profile", "dev"]),
59
+ )
60
+
61
+ popen.assert_called_once_with(
62
+ [executable, "--profile", "dev"],
63
+ cwd=os.path.dirname(executable),
64
+ env=None,
65
+ )
66
+ self.assertEqual(5678, pyappify.pid)
67
+
68
+ def test_show_pyappify_returns_none_without_executable(self):
69
+ pyappify.pid = None
70
+ pyappify.pyappify_executable = None
71
+
72
+ with mock.patch.object(pyappify, "_is_process_running", return_value=False):
73
+ self.assertIsNone(pyappify.show_pyappify())
74
+
75
+
76
+ if __name__ == "__main__":
77
+ unittest.main()
File without changes
File without changes
File without changes
File without changes