pyappify 1.0.3__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.
- {pyappify-1.0.3 → pyappify-1.0.5}/PKG-INFO +1 -1
- {pyappify-1.0.3 → pyappify-1.0.5}/pyappify/__init__.py +229 -79
- {pyappify-1.0.3 → pyappify-1.0.5}/pyappify.egg-info/PKG-INFO +1 -1
- {pyappify-1.0.3 → pyappify-1.0.5}/pyappify.egg-info/SOURCES.txt +1 -0
- {pyappify-1.0.3 → pyappify-1.0.5}/pyproject.toml +1 -1
- pyappify-1.0.5/tests/TestProcessControls.py +77 -0
- {pyappify-1.0.3 → pyappify-1.0.5}/pyappify/main.py +0 -0
- {pyappify-1.0.3 → pyappify-1.0.5}/pyappify.egg-info/dependency_links.txt +0 -0
- {pyappify-1.0.3 → pyappify-1.0.5}/pyappify.egg-info/entry_points.txt +0 -0
- {pyappify-1.0.3 → pyappify-1.0.5}/pyappify.egg-info/requires.txt +0 -0
- {pyappify-1.0.3 → pyappify-1.0.5}/pyappify.egg-info/top_level.txt +0 -0
- {pyappify-1.0.3 → pyappify-1.0.5}/setup.cfg +0 -0
- {pyappify-1.0.3 → pyappify-1.0.5}/tests/TestUpdateEnv.py +0 -0
- {pyappify-1.0.3 → pyappify-1.0.5}/tests/TestUpgrade.py +0 -0
- {pyappify-1.0.3 → pyappify-1.0.5}/tests/TestUpgradeOnline.py +0 -0
- {pyappify-1.0.3 → pyappify-1.0.5}/tests/TestVersion.py +0 -0
|
@@ -1,13 +1,15 @@
|
|
|
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
|
|
12
|
+
import time
|
|
11
13
|
|
|
12
14
|
app_version = os.environ.get("PYAPPIFY_APP_VERSION")
|
|
13
15
|
app_starting_version = os.environ.get("PYAPPIFY_APP_STARTING_VERSION") or app_version
|
|
@@ -15,20 +17,20 @@ update_note = os.environ.get("PYAPPIFY_UPDATE_NOTE")
|
|
|
15
17
|
app_profile = os.environ.get("PYAPPIFY_APP_PROFILE")
|
|
16
18
|
pyappify_version = os.environ.get("PYAPPIFY_VERSION")
|
|
17
19
|
pyappify_executable = os.environ.get("PYAPPIFY_EXECUTABLE")
|
|
18
|
-
|
|
20
|
+
|
|
19
21
|
pyappify_upgradeable = os.environ.get("PYAPPIFY_UPGRADEABLE") == '1'
|
|
20
22
|
logger = None
|
|
21
23
|
_console_logger = None
|
|
22
|
-
|
|
23
|
-
try:
|
|
24
|
-
pid = int(os.environ.get("PYAPPIFY_PID"))
|
|
25
|
-
except (ValueError, TypeError):
|
|
26
|
-
pid = None
|
|
27
|
-
|
|
28
|
-
import sys
|
|
29
|
-
|
|
30
|
-
try:
|
|
31
|
-
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
|
|
32
34
|
except ImportError:
|
|
33
35
|
ctypes = None
|
|
34
36
|
|
|
@@ -49,38 +51,186 @@ def _get_logger():
|
|
|
49
51
|
return _console_logger
|
|
50
52
|
|
|
51
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
|
+
|
|
52
74
|
def minimize_window_by_pid(pid):
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
return
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
return True
|
|
72
|
-
|
|
73
|
-
return False
|
|
74
|
-
|
|
75
|
-
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):
|
|
76
93
|
if pid:
|
|
77
94
|
log = _get_logger()
|
|
78
95
|
log.info(f"Attempting to terminate process with PID: {pid}")
|
|
79
96
|
try:
|
|
80
97
|
os.kill(pid, signal.SIGTERM)
|
|
98
|
+
if not _wait_for_process_exit(pid, timeout):
|
|
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
|
|
81
103
|
except Exception as e:
|
|
82
104
|
log.error(f"Failed to terminate process with PID {pid}: {e}")
|
|
83
|
-
|
|
105
|
+
return False
|
|
106
|
+
return False
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def kill_pyappify_exe(timeout=30):
|
|
110
|
+
return kill_pyappify(timeout)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _wait_for_process_exit(process_pid, timeout=30):
|
|
114
|
+
if sys.platform == "win32" and ctypes:
|
|
115
|
+
synchronize = 0x00100000
|
|
116
|
+
wait_timeout = 0x00000102
|
|
117
|
+
wait_failed = 0xFFFFFFFF
|
|
118
|
+
ctypes.windll.kernel32.OpenProcess.argtypes = (
|
|
119
|
+
ctypes.c_uint,
|
|
120
|
+
ctypes.c_bool,
|
|
121
|
+
ctypes.c_ulong,
|
|
122
|
+
)
|
|
123
|
+
ctypes.windll.kernel32.OpenProcess.restype = ctypes.c_void_p
|
|
124
|
+
ctypes.windll.kernel32.WaitForSingleObject.argtypes = (
|
|
125
|
+
ctypes.c_void_p,
|
|
126
|
+
ctypes.c_uint,
|
|
127
|
+
)
|
|
128
|
+
ctypes.windll.kernel32.WaitForSingleObject.restype = ctypes.c_uint
|
|
129
|
+
ctypes.windll.kernel32.CloseHandle.argtypes = (ctypes.c_void_p,)
|
|
130
|
+
ctypes.windll.kernel32.CloseHandle.restype = ctypes.c_bool
|
|
131
|
+
handle = ctypes.windll.kernel32.OpenProcess(synchronize, False, process_pid)
|
|
132
|
+
if handle:
|
|
133
|
+
try:
|
|
134
|
+
result = ctypes.windll.kernel32.WaitForSingleObject(
|
|
135
|
+
handle, int(timeout * 1000)
|
|
136
|
+
)
|
|
137
|
+
if result == wait_failed:
|
|
138
|
+
return False
|
|
139
|
+
return result != wait_timeout
|
|
140
|
+
finally:
|
|
141
|
+
ctypes.windll.kernel32.CloseHandle(handle)
|
|
142
|
+
|
|
143
|
+
deadline = time.monotonic() + timeout
|
|
144
|
+
while time.monotonic() < deadline:
|
|
145
|
+
try:
|
|
146
|
+
os.kill(process_pid, 0)
|
|
147
|
+
except OSError:
|
|
148
|
+
return True
|
|
149
|
+
time.sleep(0.1)
|
|
150
|
+
return False
|
|
151
|
+
|
|
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
|
+
|
|
221
|
+
def _replace_executable(source_path, target_path, timeout=30):
|
|
222
|
+
deadline = time.monotonic() + timeout
|
|
223
|
+
last_error = None
|
|
224
|
+
while True:
|
|
225
|
+
try:
|
|
226
|
+
shutil.move(source_path, target_path)
|
|
227
|
+
return
|
|
228
|
+
except PermissionError as e:
|
|
229
|
+
last_error = e
|
|
230
|
+
if time.monotonic() >= deadline:
|
|
231
|
+
raise last_error
|
|
232
|
+
time.sleep(0.25)
|
|
233
|
+
|
|
84
234
|
|
|
85
235
|
def hide_pyappify():
|
|
86
236
|
if pid:
|
|
@@ -102,7 +252,7 @@ def upgrade(to_version, executable_sha256, executable_zip_urls, stop_event=None)
|
|
|
102
252
|
def _do_upgrade():
|
|
103
253
|
tmp_dir = os.path.join(os.getcwd(), "pyappify_tmp")
|
|
104
254
|
try:
|
|
105
|
-
os.makedirs(tmp_dir, exist_ok=True)
|
|
255
|
+
os.makedirs(tmp_dir, exist_ok=True)
|
|
106
256
|
downloaded_zip_path = None
|
|
107
257
|
for url in executable_zip_urls:
|
|
108
258
|
try:
|
|
@@ -114,10 +264,10 @@ def upgrade(to_version, executable_sha256, executable_zip_urls, stop_event=None)
|
|
|
114
264
|
if stop_event and stop_event.is_set():
|
|
115
265
|
log.info("pyappify Upgrade download cancelled by stop event.")
|
|
116
266
|
return
|
|
117
|
-
chunk = response.read(8192)
|
|
118
|
-
if not chunk:
|
|
119
|
-
break
|
|
120
|
-
out_file.write(chunk)
|
|
267
|
+
chunk = response.read(8192)
|
|
268
|
+
if not chunk:
|
|
269
|
+
break
|
|
270
|
+
out_file.write(chunk)
|
|
121
271
|
downloaded_zip_path = local_zip_path
|
|
122
272
|
log.info(
|
|
123
273
|
f"pyappify download success {url}")
|
|
@@ -129,39 +279,39 @@ def upgrade(to_version, executable_sha256, executable_zip_urls, stop_event=None)
|
|
|
129
279
|
if not downloaded_zip_path:
|
|
130
280
|
log.error("pyappify Failed to download upgrade.")
|
|
131
281
|
return
|
|
132
|
-
|
|
133
|
-
with zipfile.ZipFile(downloaded_zip_path, 'r') as zip_ref:
|
|
134
|
-
zip_ref.extractall(tmp_dir)
|
|
135
|
-
|
|
136
|
-
new_executable_name = os.path.basename(pyappify_executable)
|
|
137
|
-
found_executable_path = None
|
|
138
|
-
for root, _, files in os.walk(tmp_dir):
|
|
139
|
-
if new_executable_name in files:
|
|
140
|
-
found_executable_path = os.path.join(root, new_executable_name)
|
|
141
|
-
break
|
|
142
|
-
|
|
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
|
+
|
|
143
293
|
if not found_executable_path:
|
|
144
294
|
log.error("pyappify Executable not found in zip.")
|
|
145
295
|
return
|
|
146
|
-
|
|
147
|
-
sha256_hash = hashlib.sha256()
|
|
148
|
-
with open(found_executable_path, "rb") as f:
|
|
149
|
-
for byte_block in iter(lambda: f.read(4096), b""):
|
|
150
|
-
sha256_hash.update(byte_block)
|
|
151
|
-
|
|
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
|
+
|
|
152
302
|
if executable_sha256 and sha256_hash.hexdigest() != executable_sha256:
|
|
153
303
|
log.error("pyappify SHA256 checksum mismatch.")
|
|
154
304
|
return
|
|
155
305
|
|
|
156
306
|
kill_pyappify()
|
|
157
|
-
|
|
307
|
+
_replace_executable(found_executable_path, pyappify_executable)
|
|
158
308
|
log.info(f"pyappify Upgrade success")
|
|
159
309
|
except Exception as e:
|
|
160
310
|
log.error(f"pyappify Upgrade failed: {e}")
|
|
161
|
-
finally:
|
|
162
|
-
if os.path.exists(tmp_dir):
|
|
163
|
-
shutil.rmtree(tmp_dir)
|
|
164
|
-
|
|
311
|
+
finally:
|
|
312
|
+
if os.path.exists(tmp_dir):
|
|
313
|
+
shutil.rmtree(tmp_dir)
|
|
314
|
+
|
|
165
315
|
thread = threading.Thread(target=_do_upgrade)
|
|
166
316
|
thread.daemon = True
|
|
167
317
|
thread.start()
|
|
@@ -202,20 +352,20 @@ def get_update_note():
|
|
|
202
352
|
def is_greater_version(version1, version2):
|
|
203
353
|
"""
|
|
204
354
|
Compares two semantic version strings.
|
|
205
|
-
|
|
206
|
-
Args:
|
|
207
|
-
version1 (str): The first version string.
|
|
208
|
-
version2 (str): The second version string.
|
|
209
|
-
|
|
210
|
-
Returns:
|
|
211
|
-
bool: True if version1 is strictly greater than version2,
|
|
212
|
-
False otherwise or if parsing fails.
|
|
213
|
-
"""
|
|
214
|
-
try:
|
|
215
|
-
version1 = version1.lstrip('v')
|
|
216
|
-
version2 = version2.lstrip('v')
|
|
217
|
-
v1_parts = [int(p) for p in version1.split('.')]
|
|
218
|
-
v2_parts = [int(p) for p in version2.split('.')]
|
|
219
|
-
return v1_parts > v2_parts
|
|
220
|
-
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):
|
|
221
371
|
return False
|
|
@@ -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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|