pyappify 1.0.8__tar.gz → 1.0.9__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.8 → pyappify-1.0.9}/PKG-INFO +1 -1
- {pyappify-1.0.8 → pyappify-1.0.9}/pyappify/__init__.py +115 -32
- {pyappify-1.0.8 → pyappify-1.0.9}/pyappify/app_config.py +28 -10
- {pyappify-1.0.8 → pyappify-1.0.9}/pyappify.egg-info/PKG-INFO +1 -1
- {pyappify-1.0.8 → pyappify-1.0.9}/pyproject.toml +1 -1
- {pyappify-1.0.8 → pyappify-1.0.9}/pyappify/main.py +0 -0
- {pyappify-1.0.8 → pyappify-1.0.9}/pyappify.egg-info/SOURCES.txt +0 -0
- {pyappify-1.0.8 → pyappify-1.0.9}/pyappify.egg-info/dependency_links.txt +0 -0
- {pyappify-1.0.8 → pyappify-1.0.9}/pyappify.egg-info/entry_points.txt +0 -0
- {pyappify-1.0.8 → pyappify-1.0.9}/pyappify.egg-info/requires.txt +0 -0
- {pyappify-1.0.8 → pyappify-1.0.9}/pyappify.egg-info/top_level.txt +0 -0
- {pyappify-1.0.8 → pyappify-1.0.9}/setup.cfg +0 -0
|
@@ -44,6 +44,7 @@ app_json_path = configure_app_json(os.environ.get("PYAPPIFY_APP_JSON_PATH"))
|
|
|
44
44
|
pyappify_upgradeable = os.environ.get("PYAPPIFY_UPGRADEABLE") == '1'
|
|
45
45
|
logger = None
|
|
46
46
|
_console_logger = None
|
|
47
|
+
_DOWNLOAD_IO_TIMEOUT_SECONDS = 2
|
|
47
48
|
|
|
48
49
|
try:
|
|
49
50
|
pid = int(os.environ.get("PYAPPIFY_PID"))
|
|
@@ -139,13 +140,13 @@ def bring_window_to_front_by_pid(pid):
|
|
|
139
140
|
return False
|
|
140
141
|
|
|
141
142
|
|
|
142
|
-
def kill_pyappify(timeout=30):
|
|
143
|
+
def kill_pyappify(timeout=30, exit_event=None):
|
|
143
144
|
if pid:
|
|
144
145
|
log = _get_logger()
|
|
145
146
|
log.info(f"Attempting to terminate process with PID: {pid}")
|
|
146
147
|
try:
|
|
147
148
|
os.kill(pid, signal.SIGTERM)
|
|
148
|
-
if not _wait_for_process_exit(pid, timeout):
|
|
149
|
+
if not _wait_for_process_exit(pid, timeout, exit_event=exit_event):
|
|
149
150
|
log.warning(f"Timed out waiting for process with PID {pid} to exit.")
|
|
150
151
|
return False
|
|
151
152
|
log.info(f'_wait_for_process_exit success {pid}')
|
|
@@ -156,11 +157,11 @@ def kill_pyappify(timeout=30):
|
|
|
156
157
|
return False
|
|
157
158
|
|
|
158
159
|
|
|
159
|
-
def kill_pyappify_exe(timeout=30):
|
|
160
|
-
return kill_pyappify(timeout)
|
|
160
|
+
def kill_pyappify_exe(timeout=30, exit_event=None):
|
|
161
|
+
return kill_pyappify(timeout, exit_event=exit_event)
|
|
161
162
|
|
|
162
163
|
|
|
163
|
-
def _wait_for_process_exit(process_pid, timeout=30):
|
|
164
|
+
def _wait_for_process_exit(process_pid, timeout=30, exit_event=None):
|
|
164
165
|
if sys.platform == "win32" and ctypes:
|
|
165
166
|
synchronize = 0x00100000
|
|
166
167
|
wait_timeout = 0x00000102
|
|
@@ -181,22 +182,32 @@ def _wait_for_process_exit(process_pid, timeout=30):
|
|
|
181
182
|
handle = ctypes.windll.kernel32.OpenProcess(synchronize, False, process_pid)
|
|
182
183
|
if handle:
|
|
183
184
|
try:
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
185
|
+
deadline = time.monotonic() + timeout
|
|
186
|
+
while time.monotonic() < deadline:
|
|
187
|
+
if _exit_requested(exit_event):
|
|
188
|
+
return False
|
|
189
|
+
remaining_ms = max(0, int((deadline - time.monotonic()) * 1000))
|
|
190
|
+
result = ctypes.windll.kernel32.WaitForSingleObject(
|
|
191
|
+
handle, min(100, remaining_ms)
|
|
192
|
+
)
|
|
193
|
+
if result == wait_failed:
|
|
194
|
+
return False
|
|
195
|
+
if result != wait_timeout:
|
|
196
|
+
return True
|
|
197
|
+
return False
|
|
190
198
|
finally:
|
|
191
199
|
ctypes.windll.kernel32.CloseHandle(handle)
|
|
192
200
|
|
|
193
201
|
deadline = time.monotonic() + timeout
|
|
194
202
|
while time.monotonic() < deadline:
|
|
203
|
+
if _exit_requested(exit_event):
|
|
204
|
+
return False
|
|
195
205
|
try:
|
|
196
206
|
os.kill(process_pid, 0)
|
|
197
207
|
except OSError:
|
|
198
208
|
return True
|
|
199
|
-
|
|
209
|
+
if _wait_for_exit(exit_event, 0.1):
|
|
210
|
+
return False
|
|
200
211
|
return False
|
|
201
212
|
|
|
202
213
|
|
|
@@ -285,7 +296,32 @@ def _require_pyappify_executable():
|
|
|
285
296
|
return pyappify_executable
|
|
286
297
|
|
|
287
298
|
|
|
288
|
-
def
|
|
299
|
+
def _exit_requested(*events):
|
|
300
|
+
return any(event is not None and event.is_set() for event in events)
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
def _wait_for_exit(exit_event, timeout):
|
|
304
|
+
if exit_event is not None:
|
|
305
|
+
return exit_event.wait(timeout)
|
|
306
|
+
time.sleep(timeout)
|
|
307
|
+
return False
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
def _raise_if_exit_requested(exit_event):
|
|
311
|
+
if _exit_requested(exit_event):
|
|
312
|
+
raise InterruptedError("PyAppify operation cancelled because exit_event was set")
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
def _terminate_process(process):
|
|
316
|
+
try:
|
|
317
|
+
if process.poll() is None:
|
|
318
|
+
process.terminate()
|
|
319
|
+
except (AttributeError, OSError):
|
|
320
|
+
pass
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
def _run_launcher_api(arguments, timeout=300, exit_event=None):
|
|
324
|
+
_raise_if_exit_requested(exit_event)
|
|
289
325
|
executable = _require_pyappify_executable()
|
|
290
326
|
response_path = os.path.join(
|
|
291
327
|
tempfile.gettempdir(),
|
|
@@ -293,7 +329,7 @@ def _run_launcher_api(arguments, timeout=300):
|
|
|
293
329
|
)
|
|
294
330
|
command = list(arguments) + ["--response-file", response_path]
|
|
295
331
|
try:
|
|
296
|
-
subprocess.Popen(
|
|
332
|
+
process = subprocess.Popen(
|
|
297
333
|
[executable] + command,
|
|
298
334
|
cwd=os.path.dirname(executable) or None,
|
|
299
335
|
stdout=subprocess.DEVNULL,
|
|
@@ -305,8 +341,13 @@ def _run_launcher_api(arguments, timeout=300):
|
|
|
305
341
|
)
|
|
306
342
|
|
|
307
343
|
deadline = time.monotonic() + timeout
|
|
344
|
+
cancelled = False
|
|
308
345
|
try:
|
|
309
346
|
while time.monotonic() < deadline:
|
|
347
|
+
if _exit_requested(exit_event):
|
|
348
|
+
cancelled = True
|
|
349
|
+
_terminate_process(process)
|
|
350
|
+
break
|
|
310
351
|
try:
|
|
311
352
|
with open(response_path, "r", encoding="utf-8") as response_file:
|
|
312
353
|
response = json.load(response_file)
|
|
@@ -318,18 +359,25 @@ def _run_launcher_api(arguments, timeout=300):
|
|
|
318
359
|
except json.JSONDecodeError:
|
|
319
360
|
# The running launcher may still be completing the response write.
|
|
320
361
|
pass
|
|
321
|
-
|
|
362
|
+
if _wait_for_exit(exit_event, 0.05):
|
|
363
|
+
cancelled = True
|
|
364
|
+
_terminate_process(process)
|
|
365
|
+
break
|
|
322
366
|
finally:
|
|
323
367
|
try:
|
|
324
368
|
os.remove(response_path)
|
|
325
369
|
except FileNotFoundError:
|
|
326
370
|
pass
|
|
327
371
|
|
|
372
|
+
if cancelled:
|
|
373
|
+
raise InterruptedError("PyAppify operation cancelled because exit_event was set")
|
|
328
374
|
raise TimeoutError("Timed out waiting for a response from PyAppify")
|
|
329
375
|
|
|
330
376
|
|
|
331
|
-
def get_version_list(
|
|
332
|
-
|
|
377
|
+
def get_version_list(
|
|
378
|
+
number_versions=10, release_only=True, timeout=120, exit_event=None
|
|
379
|
+
):
|
|
380
|
+
"""Return version details, cancelling with InterruptedError on exit_event."""
|
|
333
381
|
if isinstance(number_versions, bool) or not isinstance(number_versions, int):
|
|
334
382
|
raise TypeError("number_versions must be an integer")
|
|
335
383
|
if number_versions <= 0:
|
|
@@ -338,7 +386,8 @@ def get_version_list(number_versions=10, release_only=True, timeout=120):
|
|
|
338
386
|
raise TypeError("release_only must be a boolean")
|
|
339
387
|
|
|
340
388
|
if "PYAPPIFY_PYTHON_TEST" in os.environ:
|
|
341
|
-
|
|
389
|
+
if _wait_for_exit(exit_event, 5):
|
|
390
|
+
_raise_if_exit_requested(exit_event)
|
|
342
391
|
return _get_mock_version_list(number_versions)
|
|
343
392
|
|
|
344
393
|
response = _run_launcher_api(
|
|
@@ -350,15 +399,16 @@ def get_version_list(number_versions=10, release_only=True, timeout=120):
|
|
|
350
399
|
str(release_only).lower(),
|
|
351
400
|
],
|
|
352
401
|
timeout=timeout,
|
|
402
|
+
exit_event=exit_event,
|
|
353
403
|
)
|
|
354
404
|
if not isinstance(response, list):
|
|
355
405
|
raise RuntimeError("PyAppify returned an invalid version-list response")
|
|
356
406
|
return response
|
|
357
407
|
|
|
358
408
|
|
|
359
|
-
def get_versions(number_versions=10, release_only=True, timeout=120):
|
|
409
|
+
def get_versions(number_versions=10, release_only=True, timeout=120, exit_event=None):
|
|
360
410
|
"""Alias for get_version_list."""
|
|
361
|
-
return get_version_list(number_versions, release_only, timeout)
|
|
411
|
+
return get_version_list(number_versions, release_only, timeout, exit_event)
|
|
362
412
|
|
|
363
413
|
|
|
364
414
|
def calculate_update_notes(update_notes, current_version, target_version):
|
|
@@ -401,15 +451,17 @@ def calculate_update_notes(update_notes, current_version, target_version):
|
|
|
401
451
|
return [str(note) for note in notes]
|
|
402
452
|
|
|
403
453
|
|
|
404
|
-
def update_to_version(version, timeout=300):
|
|
405
|
-
"""
|
|
454
|
+
def update_to_version(version, timeout=300, exit_event=None):
|
|
455
|
+
"""Update the app, cancelling with InterruptedError on exit_event."""
|
|
406
456
|
if not isinstance(version, str) or not version.strip():
|
|
407
457
|
raise ValueError("version must be a non-empty string")
|
|
408
458
|
if "PYAPPIFY_PYTHON_TEST" in os.environ:
|
|
459
|
+
_raise_if_exit_requested(exit_event)
|
|
409
460
|
return {"updated": True, "version": version, "mocked": True}
|
|
410
461
|
response = _run_launcher_api(
|
|
411
462
|
["--update-to-version", version],
|
|
412
463
|
timeout=timeout,
|
|
464
|
+
exit_event=exit_event,
|
|
413
465
|
)
|
|
414
466
|
if not isinstance(response, dict) or not response.get("updated"):
|
|
415
467
|
raise RuntimeError("PyAppify returned an invalid update response")
|
|
@@ -463,10 +515,11 @@ def _get_mock_version_list(number_versions):
|
|
|
463
515
|
return versions[:number_versions]
|
|
464
516
|
|
|
465
517
|
|
|
466
|
-
def _replace_executable(source_path, target_path, timeout=30):
|
|
518
|
+
def _replace_executable(source_path, target_path, timeout=30, exit_event=None):
|
|
467
519
|
deadline = time.monotonic() + timeout
|
|
468
520
|
last_error = None
|
|
469
521
|
while True:
|
|
522
|
+
_raise_if_exit_requested(exit_event)
|
|
470
523
|
try:
|
|
471
524
|
shutil.move(source_path, target_path)
|
|
472
525
|
return
|
|
@@ -474,7 +527,8 @@ def _replace_executable(source_path, target_path, timeout=30):
|
|
|
474
527
|
last_error = e
|
|
475
528
|
if time.monotonic() >= deadline:
|
|
476
529
|
raise last_error
|
|
477
|
-
|
|
530
|
+
if _wait_for_exit(exit_event, 0.25):
|
|
531
|
+
_raise_if_exit_requested(exit_event)
|
|
478
532
|
|
|
479
533
|
|
|
480
534
|
def hide_pyappify():
|
|
@@ -487,7 +541,13 @@ def hide_pyappify():
|
|
|
487
541
|
log.error(f"Failed to minimize window for process with PID {pid}: {e}")
|
|
488
542
|
pass
|
|
489
543
|
|
|
490
|
-
def upgrade(
|
|
544
|
+
def upgrade(
|
|
545
|
+
to_version,
|
|
546
|
+
executable_sha256,
|
|
547
|
+
executable_zip_urls,
|
|
548
|
+
stop_event=None,
|
|
549
|
+
exit_event=None,
|
|
550
|
+
):
|
|
491
551
|
log = _get_logger()
|
|
492
552
|
if not pyappify_upgradeable or not is_greater_version(to_version, pyappify_version):
|
|
493
553
|
log.info(f"pyappify no need to upgrade {pyappify_upgradeable} {to_version} {executable_sha256} {executable_zip_urls}")
|
|
@@ -497,17 +557,23 @@ def upgrade(to_version, executable_sha256, executable_zip_urls, stop_event=None)
|
|
|
497
557
|
def _do_upgrade():
|
|
498
558
|
tmp_dir = os.path.join(os.getcwd(), "pyappify_tmp")
|
|
499
559
|
try:
|
|
560
|
+
if _exit_requested(stop_event, exit_event):
|
|
561
|
+
return
|
|
500
562
|
os.makedirs(tmp_dir, exist_ok=True)
|
|
501
563
|
downloaded_zip_path = None
|
|
502
564
|
for url in executable_zip_urls:
|
|
565
|
+
if _exit_requested(stop_event, exit_event):
|
|
566
|
+
return
|
|
503
567
|
try:
|
|
504
568
|
log.info(
|
|
505
569
|
f"pyappify start to download {url}")
|
|
506
570
|
local_zip_path = os.path.join(tmp_dir, os.path.basename(url))
|
|
507
|
-
with urllib.request.urlopen(
|
|
571
|
+
with urllib.request.urlopen(
|
|
572
|
+
url, timeout=_DOWNLOAD_IO_TIMEOUT_SECONDS
|
|
573
|
+
) as response, open(local_zip_path, 'wb') as out_file:
|
|
508
574
|
while True:
|
|
509
|
-
if stop_event
|
|
510
|
-
log.info("pyappify Upgrade download cancelled
|
|
575
|
+
if _exit_requested(stop_event, exit_event):
|
|
576
|
+
log.info("pyappify Upgrade download cancelled.")
|
|
511
577
|
return
|
|
512
578
|
chunk = response.read(8192)
|
|
513
579
|
if not chunk:
|
|
@@ -526,11 +592,16 @@ def upgrade(to_version, executable_sha256, executable_zip_urls, stop_event=None)
|
|
|
526
592
|
return
|
|
527
593
|
|
|
528
594
|
with zipfile.ZipFile(downloaded_zip_path, 'r') as zip_ref:
|
|
529
|
-
zip_ref.
|
|
595
|
+
for member in zip_ref.infolist():
|
|
596
|
+
if _exit_requested(stop_event, exit_event):
|
|
597
|
+
return
|
|
598
|
+
zip_ref.extract(member, tmp_dir)
|
|
530
599
|
|
|
531
600
|
new_executable_name = os.path.basename(pyappify_executable)
|
|
532
601
|
found_executable_path = None
|
|
533
602
|
for root, _, files in os.walk(tmp_dir):
|
|
603
|
+
if _exit_requested(stop_event, exit_event):
|
|
604
|
+
return
|
|
534
605
|
if new_executable_name in files:
|
|
535
606
|
found_executable_path = os.path.join(root, new_executable_name)
|
|
536
607
|
break
|
|
@@ -541,15 +612,26 @@ def upgrade(to_version, executable_sha256, executable_zip_urls, stop_event=None)
|
|
|
541
612
|
|
|
542
613
|
sha256_hash = hashlib.sha256()
|
|
543
614
|
with open(found_executable_path, "rb") as f:
|
|
544
|
-
|
|
615
|
+
while True:
|
|
616
|
+
if _exit_requested(stop_event, exit_event):
|
|
617
|
+
return
|
|
618
|
+
byte_block = f.read(4096)
|
|
619
|
+
if not byte_block:
|
|
620
|
+
break
|
|
545
621
|
sha256_hash.update(byte_block)
|
|
546
622
|
|
|
547
623
|
if executable_sha256 and sha256_hash.hexdigest() != executable_sha256:
|
|
548
624
|
log.error("pyappify SHA256 checksum mismatch.")
|
|
549
625
|
return
|
|
550
626
|
|
|
551
|
-
|
|
552
|
-
|
|
627
|
+
if _exit_requested(stop_event, exit_event):
|
|
628
|
+
return
|
|
629
|
+
kill_pyappify(exit_event=exit_event or stop_event)
|
|
630
|
+
_replace_executable(
|
|
631
|
+
found_executable_path,
|
|
632
|
+
pyappify_executable,
|
|
633
|
+
exit_event=exit_event or stop_event,
|
|
634
|
+
)
|
|
553
635
|
log.info(f"pyappify Upgrade success")
|
|
554
636
|
except Exception as e:
|
|
555
637
|
log.error(f"pyappify Upgrade failed: {e}")
|
|
@@ -560,6 +642,7 @@ def upgrade(to_version, executable_sha256, executable_zip_urls, stop_event=None)
|
|
|
560
642
|
thread = threading.Thread(target=_do_upgrade)
|
|
561
643
|
thread.daemon = True
|
|
562
644
|
thread.start()
|
|
645
|
+
return thread
|
|
563
646
|
|
|
564
647
|
|
|
565
648
|
def is_app_updated():
|
|
@@ -22,7 +22,7 @@ _LOG = logging.getLogger("pyappify.app_config")
|
|
|
22
22
|
class AppConfigAPI:
|
|
23
23
|
"""Thread-safe access to the app preferences stored by PyAppify."""
|
|
24
24
|
|
|
25
|
-
def __init__(self, path=None, watch=False, watch_interval=0.5):
|
|
25
|
+
def __init__(self, path=None, watch=False, watch_interval=0.5, exit_event=None):
|
|
26
26
|
self._lock = threading.RLock()
|
|
27
27
|
self._path = None
|
|
28
28
|
self._preferences = {
|
|
@@ -31,22 +31,29 @@ class AppConfigAPI:
|
|
|
31
31
|
}
|
|
32
32
|
self._listeners = set()
|
|
33
33
|
self._watch_interval = watch_interval
|
|
34
|
+
self._exit_event = exit_event
|
|
34
35
|
self._watch_stop = None
|
|
35
36
|
self._watch_thread = None
|
|
36
37
|
if path:
|
|
37
|
-
self.configure(
|
|
38
|
+
self.configure(
|
|
39
|
+
path,
|
|
40
|
+
watch=watch,
|
|
41
|
+
watch_interval=watch_interval,
|
|
42
|
+
exit_event=exit_event,
|
|
43
|
+
)
|
|
38
44
|
|
|
39
45
|
@property
|
|
40
46
|
def path(self):
|
|
41
47
|
with self._lock:
|
|
42
48
|
return self._path
|
|
43
49
|
|
|
44
|
-
def configure(self, path, watch=True, watch_interval=0.5):
|
|
50
|
+
def configure(self, path, watch=True, watch_interval=0.5, exit_event=None):
|
|
45
51
|
self.stop_watcher()
|
|
46
52
|
normalized_path = os.path.abspath(os.path.expanduser(path)) if path else None
|
|
47
53
|
with self._lock:
|
|
48
54
|
self._path = normalized_path
|
|
49
55
|
self._watch_interval = watch_interval
|
|
56
|
+
self._exit_event = exit_event
|
|
50
57
|
self._preferences = {
|
|
51
58
|
"auto_start": False,
|
|
52
59
|
"update_method": UPDATE_METHOD_AUTO,
|
|
@@ -153,17 +160,19 @@ class AppConfigAPI:
|
|
|
153
160
|
with self._lock:
|
|
154
161
|
self._listeners.discard(listener)
|
|
155
162
|
|
|
156
|
-
def start_watcher(self):
|
|
163
|
+
def start_watcher(self, exit_event=None):
|
|
157
164
|
with self._lock:
|
|
158
165
|
if not self._path:
|
|
159
166
|
raise RuntimeError("PYAPPIFY_APP_JSON_PATH is not configured")
|
|
160
167
|
if self._watch_thread and self._watch_thread.is_alive():
|
|
161
168
|
return self._watch_thread
|
|
169
|
+
if exit_event is not None:
|
|
170
|
+
self._exit_event = exit_event
|
|
162
171
|
stop_event = threading.Event()
|
|
163
172
|
self._watch_stop = stop_event
|
|
164
173
|
thread = threading.Thread(
|
|
165
174
|
target=self._watch_loop,
|
|
166
|
-
args=(stop_event,),
|
|
175
|
+
args=(stop_event, self._exit_event),
|
|
167
176
|
name="pyappify-app-config-watch",
|
|
168
177
|
daemon=True,
|
|
169
178
|
)
|
|
@@ -182,8 +191,12 @@ class AppConfigAPI:
|
|
|
182
191
|
if thread and thread is not threading.current_thread():
|
|
183
192
|
thread.join(timeout=max(1.0, self._watch_interval * 2))
|
|
184
193
|
|
|
185
|
-
def _watch_loop(self, stop_event):
|
|
194
|
+
def _watch_loop(self, stop_event, exit_event=None):
|
|
195
|
+
if exit_event is not None and exit_event.is_set():
|
|
196
|
+
return
|
|
186
197
|
while not stop_event.wait(self._watch_interval):
|
|
198
|
+
if exit_event is not None and exit_event.is_set():
|
|
199
|
+
return
|
|
187
200
|
try:
|
|
188
201
|
self.refresh()
|
|
189
202
|
except (OSError, ValueError, json.JSONDecodeError):
|
|
@@ -193,8 +206,13 @@ class AppConfigAPI:
|
|
|
193
206
|
_api = AppConfigAPI()
|
|
194
207
|
|
|
195
208
|
|
|
196
|
-
def configure_app_json(path=None, watch=True, watch_interval=0.5):
|
|
197
|
-
return _api.configure(
|
|
209
|
+
def configure_app_json(path=None, watch=True, watch_interval=0.5, exit_event=None):
|
|
210
|
+
return _api.configure(
|
|
211
|
+
path,
|
|
212
|
+
watch=watch,
|
|
213
|
+
watch_interval=watch_interval,
|
|
214
|
+
exit_event=exit_event,
|
|
215
|
+
)
|
|
198
216
|
|
|
199
217
|
|
|
200
218
|
def get_app_json_path():
|
|
@@ -233,8 +251,8 @@ def remove_app_config_listener(listener):
|
|
|
233
251
|
_api.remove_listener(listener)
|
|
234
252
|
|
|
235
253
|
|
|
236
|
-
def start_app_config_watcher():
|
|
237
|
-
return _api.start_watcher()
|
|
254
|
+
def start_app_config_watcher(exit_event=None):
|
|
255
|
+
return _api.start_watcher(exit_event=exit_event)
|
|
238
256
|
|
|
239
257
|
|
|
240
258
|
def stop_app_config_watcher():
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|