cleepcli 1.46.0__tar.gz → 1.48.0__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.
Files changed (41) hide show
  1. {cleepcli-1.46.0 → cleepcli-1.48.0}/MANIFEST.in +1 -0
  2. {cleepcli-1.46.0/cleepcli.egg-info → cleepcli-1.48.0}/PKG-INFO +20 -4
  3. {cleepcli-1.46.0 → cleepcli-1.48.0}/README.md +19 -3
  4. {cleepcli-1.46.0 → cleepcli-1.48.0}/bin/ccli +59 -4
  5. {cleepcli-1.46.0 → cleepcli-1.48.0}/bin/cleep-cli +59 -4
  6. {cleepcli-1.46.0 → cleepcli-1.48.0}/cleepcli/check.py +44 -0
  7. {cleepcli-1.46.0 → cleepcli-1.48.0}/cleepcli/file.py +45 -0
  8. {cleepcli-1.46.0 → cleepcli-1.48.0}/cleepcli/package.py +10 -0
  9. cleepcli-1.48.0/cleepcli/systemd/cleep-watch.service +18 -0
  10. {cleepcli-1.46.0 → cleepcli-1.48.0}/cleepcli/version.py +1 -1
  11. {cleepcli-1.46.0 → cleepcli-1.48.0}/cleepcli/watch.py +24 -2
  12. cleepcli-1.48.0/cleepcli/watchsvc.py +214 -0
  13. {cleepcli-1.46.0 → cleepcli-1.48.0/cleepcli.egg-info}/PKG-INFO +20 -4
  14. {cleepcli-1.46.0 → cleepcli-1.48.0}/cleepcli.egg-info/SOURCES.txt +5 -1
  15. {cleepcli-1.46.0 → cleepcli-1.48.0}/setup.py +1 -0
  16. cleepcli-1.48.0/tests/test_file_sync_all.py +91 -0
  17. {cleepcli-1.46.0 → cleepcli-1.48.0}/tests/test_watch.py +7 -0
  18. cleepcli-1.48.0/tests/test_watchsvc.py +57 -0
  19. {cleepcli-1.46.0 → cleepcli-1.48.0}/LICENSE +0 -0
  20. {cleepcli-1.46.0 → cleepcli-1.48.0}/cleepcli/__init__.py +0 -0
  21. {cleepcli-1.46.0 → cleepcli-1.48.0}/cleepcli/ci.py +0 -0
  22. {cleepcli-1.46.0 → cleepcli-1.48.0}/cleepcli/cleepapi.py +0 -0
  23. {cleepcli-1.46.0 → cleepcli-1.48.0}/cleepcli/config.py +0 -0
  24. {cleepcli-1.46.0 → cleepcli-1.48.0}/cleepcli/console.py +0 -0
  25. {cleepcli-1.46.0 → cleepcli-1.48.0}/cleepcli/docs.py +0 -0
  26. {cleepcli-1.46.0 → cleepcli-1.48.0}/cleepcli/git.py +0 -0
  27. {cleepcli-1.46.0 → cleepcli-1.48.0}/cleepcli/module.py +0 -0
  28. {cleepcli-1.46.0 → cleepcli-1.48.0}/cleepcli/test.py +0 -0
  29. {cleepcli-1.46.0 → cleepcli-1.48.0}/cleepcli/tools.py +0 -0
  30. {cleepcli-1.46.0 → cleepcli-1.48.0}/cleepcli.egg-info/dependency_links.txt +0 -0
  31. {cleepcli-1.46.0 → cleepcli-1.48.0}/cleepcli.egg-info/requires.txt +0 -0
  32. {cleepcli-1.46.0 → cleepcli-1.48.0}/cleepcli.egg-info/top_level.txt +0 -0
  33. {cleepcli-1.46.0 → cleepcli-1.48.0}/pyproject.toml +0 -0
  34. {cleepcli-1.46.0 → cleepcli-1.48.0}/requirements.txt +0 -0
  35. {cleepcli-1.46.0 → cleepcli-1.48.0}/setup.cfg +0 -0
  36. {cleepcli-1.46.0 → cleepcli-1.48.0}/tests/test_check.py +0 -0
  37. {cleepcli-1.46.0 → cleepcli-1.48.0}/tests/test_config.py +0 -0
  38. {cleepcli-1.46.0 → cleepcli-1.48.0}/tests/test_console.py +0 -0
  39. {cleepcli-1.46.0 → cleepcli-1.48.0}/tests/test_coverage.py +0 -0
  40. {cleepcli-1.46.0 → cleepcli-1.48.0}/tests/test_docs_breaking.py +0 -0
  41. {cleepcli-1.46.0 → cleepcli-1.48.0}/tests/test_module_scaffold.py +0 -0
@@ -7,3 +7,4 @@ include setup.cfg
7
7
  include MANIFEST.in
8
8
  recursive-include bin *
9
9
  recursive-include cleepcli *.py
10
+ recursive-include cleepcli/systemd *
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cleepcli
3
- Version: 1.46.0
3
+ Version: 1.48.0
4
4
  Summary: Cleep-cli helps developers to build great Cleep applications from command line.
5
5
  Home-page: https://github.com/CleepDevice/cleep-cli
6
6
  Author: Tanguy Bonneau
@@ -61,7 +61,9 @@ This utility helps developers to build Cleep applications providing some useful
61
61
  - `modcheckscripts` to check app scripts files
62
62
  - `modcheckcode` to check backend code quality (run pylint)
63
63
  - Watch commands:
64
- - `watch` to watch for repository filesystem changes and automatically update files on execution folders. It also restart backend or/and frontend according to changes.
64
+ - `watch` to watch for repository filesystem changes and automatically update files on execution folders. It also restart backend or/and frontend according to changes (catch-up sync on start).
65
+ - `watchsvc` to install/enable/disable/restart the durable `cleep-watch` systemd unit (runs `watch` outside `cleep.service`).
66
+ - `syncall` to force a one-shot sync of core + all modules (same catch-up as watch start).
65
67
  - Misc commands:
66
68
  - `version` to get cleep-cli version
67
69
 
@@ -85,9 +87,23 @@ Execute this command to get help on this cli:
85
87
 
86
88
  ## Watch usage
87
89
 
88
- Launch `watch` cli command to monitor changes on "/root/cleep-dev" directory after getting official Cleep repository using `coreget` command.
90
+ Preferred: let the CLI own the durable systemd unit:
89
91
 
90
- Then open your favorite editor on your development computer, configure a remote sync plugin to push local changes on your raspberry pi then cleep-cli will synchronize sources on Cleep execution environment and restart application automatically.
92
+ ```
93
+ cleep-cli watchsvc install
94
+ cleep-cli watchsvc enable
95
+ ```
96
+
97
+ The unit runs `cleep-cli watch`, survives `systemctl restart cleep`, and on each start performs a catch-up `syncall` so files written while the watcher was down are applied.
98
+
99
+ Manual one-shot resync (when the UI looks stale):
100
+
101
+ ```
102
+ cleep-cli syncall
103
+ cleep-cli watchsvc restart # catch-up again + resume watch
104
+ ```
105
+
106
+ You can still run `cleep-cli watch` in a terminal for debugging. Push sources to `/root/cleep-dev` (CleepDesktop developer sync or SFTP); the watcher copies them into the execution folders and restarts backend/frontend as needed.
91
107
 
92
108
  ### Paths
93
109
 
@@ -25,7 +25,9 @@ This utility helps developers to build Cleep applications providing some useful
25
25
  - `modcheckscripts` to check app scripts files
26
26
  - `modcheckcode` to check backend code quality (run pylint)
27
27
  - Watch commands:
28
- - `watch` to watch for repository filesystem changes and automatically update files on execution folders. It also restart backend or/and frontend according to changes.
28
+ - `watch` to watch for repository filesystem changes and automatically update files on execution folders. It also restart backend or/and frontend according to changes (catch-up sync on start).
29
+ - `watchsvc` to install/enable/disable/restart the durable `cleep-watch` systemd unit (runs `watch` outside `cleep.service`).
30
+ - `syncall` to force a one-shot sync of core + all modules (same catch-up as watch start).
29
31
  - Misc commands:
30
32
  - `version` to get cleep-cli version
31
33
 
@@ -49,9 +51,23 @@ Execute this command to get help on this cli:
49
51
 
50
52
  ## Watch usage
51
53
 
52
- Launch `watch` cli command to monitor changes on "/root/cleep-dev" directory after getting official Cleep repository using `coreget` command.
54
+ Preferred: let the CLI own the durable systemd unit:
53
55
 
54
- Then open your favorite editor on your development computer, configure a remote sync plugin to push local changes on your raspberry pi then cleep-cli will synchronize sources on Cleep execution environment and restart application automatically.
56
+ ```
57
+ cleep-cli watchsvc install
58
+ cleep-cli watchsvc enable
59
+ ```
60
+
61
+ The unit runs `cleep-cli watch`, survives `systemctl restart cleep`, and on each start performs a catch-up `syncall` so files written while the watcher was down are applied.
62
+
63
+ Manual one-shot resync (when the UI looks stale):
64
+
65
+ ```
66
+ cleep-cli syncall
67
+ cleep-cli watchsvc restart # catch-up again + resume watch
68
+ ```
69
+
70
+ You can still run `cleep-cli watch` in a terminal for debugging. Push sources to `/root/cleep-dev` (CleepDesktop developer sync or SFTP); the watcher copies them into the execution folders and restarts backend/frontend as needed.
55
71
 
56
72
  ### Paths
57
73
 
@@ -17,6 +17,7 @@ from cleepcli.git import Git
17
17
  from cleepcli.module import Module
18
18
  from cleepcli.file import File
19
19
  from cleepcli.watch import CleepWatchdog
20
+ from cleepcli.watchsvc import WatchService
20
21
  from cleepcli.test import Test, is_file_coverage_valid
21
22
  from cleepcli.package import Package
22
23
  from cleepcli.docs import Docs
@@ -108,6 +109,18 @@ def coresync():
108
109
  sys.exit(1)
109
110
  logging.info('Done')
110
111
 
112
+ @core.command()
113
+ def syncall():
114
+ """
115
+ Synchronize core and all modules from /root/cleep-dev to execution folders.
116
+
117
+ Same catch-up used when cleep-watch.service starts. Safe one-shot resync when
118
+ the UI looks stale: run this, then reload the device page.
119
+ """
120
+ f = File()
121
+ if not f.sync_all():
122
+ sys.exit(1)
123
+
111
124
  @core.command()
112
125
  @click.option('--coverage', is_flag=True, help='Display coverage report.')
113
126
  @click.option('--output', is_flag=True, help='Display tests output.')
@@ -358,9 +371,13 @@ def watchdog():
358
371
  @click.option('--quiet', is_flag=True, help='Disable logging.')
359
372
  @click.option('--loglevel', default=logging.INFO, help='Logging level (10=DEBUG, 20=INFO, 30=WARN, 40=ERROR).')
360
373
  @click.option('--rpc-url', default='https://127.0.0.1:443', help='Cleep RPC url (ex https://ip|host:port')
361
- def watch(quiet, loglevel, rpc_url):
374
+ @click.option('--skip-catchup', is_flag=True, help='Do not syncall before watching (not recommended).')
375
+ def watch(quiet, loglevel, rpc_url, skip_catchup):
362
376
  """
363
- Start watchdog that monitors filesystem changes on Cleep sources
377
+ Start watchdog that monitors filesystem changes on Cleep sources.
378
+
379
+ Prefer managing the durable unit with watchsvc (survives Cleep restarts).
380
+ On start, syncs core + all modules once unless --skip-catchup is set.
364
381
  """
365
382
  if logging.getLogger().getEffectiveLevel() == logging.DEBUG:
366
383
  # do not overwrite root logger level if configured to DEBUG (dev mode)
@@ -369,13 +386,51 @@ def watch(quiet, loglevel, rpc_url):
369
386
  logging.disable(logging.CRITICAL)
370
387
  else:
371
388
  logging.getLogger().setLevel(loglevel)
372
-
373
- w = CleepWatchdog(rpc_url)
389
+
390
+ w = CleepWatchdog(rpc_url, catch_up=not skip_catchup)
374
391
  res = w.watch()
375
392
 
376
393
  if not res:
377
394
  sys.exit(1)
378
395
 
396
+ @watchdog.command('watchsvc')
397
+ @click.argument(
398
+ 'action',
399
+ type=click.Choice(
400
+ ['install', 'uninstall', 'enable', 'disable', 'start', 'stop', 'restart', 'status']
401
+ ),
402
+ )
403
+ def watchsvc_cmd(action):
404
+ """
405
+ Manage the durable cleep-watch systemd unit.
406
+
407
+ ACTION is one of: install, uninstall, enable, disable, start, stop, restart, status.
408
+
409
+ The unit runs ``cleep-cli watch`` outside of cleep.service. Typical setup::
410
+
411
+ cleep-cli watchsvc install
412
+ cleep-cli watchsvc enable
413
+ """
414
+ svc = WatchService()
415
+ handlers = {
416
+ 'install': svc.install,
417
+ 'uninstall': svc.uninstall,
418
+ 'enable': svc.enable,
419
+ 'disable': svc.disable,
420
+ 'start': svc.start,
421
+ 'stop': svc.stop,
422
+ 'restart': svc.restart,
423
+ }
424
+ if action == 'status':
425
+ status = svc.status()
426
+ logging.info(status)
427
+ if status != 'active':
428
+ sys.exit(1)
429
+ return
430
+
431
+ if not handlers[action]():
432
+ sys.exit(1)
433
+
379
434
  @click.group()
380
435
  def test():
381
436
  pass
@@ -17,6 +17,7 @@ from cleepcli.git import Git
17
17
  from cleepcli.module import Module
18
18
  from cleepcli.file import File
19
19
  from cleepcli.watch import CleepWatchdog
20
+ from cleepcli.watchsvc import WatchService
20
21
  from cleepcli.test import Test, is_file_coverage_valid
21
22
  from cleepcli.package import Package
22
23
  from cleepcli.docs import Docs
@@ -108,6 +109,18 @@ def coresync():
108
109
  sys.exit(1)
109
110
  logging.info('Done')
110
111
 
112
+ @core.command()
113
+ def syncall():
114
+ """
115
+ Synchronize core and all modules from /root/cleep-dev to execution folders.
116
+
117
+ Same catch-up used when cleep-watch.service starts. Safe one-shot resync when
118
+ the UI looks stale: run this, then reload the device page.
119
+ """
120
+ f = File()
121
+ if not f.sync_all():
122
+ sys.exit(1)
123
+
111
124
  @core.command()
112
125
  @click.option('--coverage', is_flag=True, help='Display coverage report.')
113
126
  @click.option('--output', is_flag=True, help='Display tests output.')
@@ -358,9 +371,13 @@ def watchdog():
358
371
  @click.option('--quiet', is_flag=True, help='Disable logging.')
359
372
  @click.option('--loglevel', default=logging.INFO, help='Logging level (10=DEBUG, 20=INFO, 30=WARN, 40=ERROR).')
360
373
  @click.option('--rpc-url', default='https://127.0.0.1:443', help='Cleep RPC url (ex https://ip|host:port')
361
- def watch(quiet, loglevel, rpc_url):
374
+ @click.option('--skip-catchup', is_flag=True, help='Do not syncall before watching (not recommended).')
375
+ def watch(quiet, loglevel, rpc_url, skip_catchup):
362
376
  """
363
- Start watchdog that monitors filesystem changes on Cleep sources
377
+ Start watchdog that monitors filesystem changes on Cleep sources.
378
+
379
+ Prefer managing the durable unit with watchsvc (survives Cleep restarts).
380
+ On start, syncs core + all modules once unless --skip-catchup is set.
364
381
  """
365
382
  if logging.getLogger().getEffectiveLevel() == logging.DEBUG:
366
383
  # do not overwrite root logger level if configured to DEBUG (dev mode)
@@ -369,13 +386,51 @@ def watch(quiet, loglevel, rpc_url):
369
386
  logging.disable(logging.CRITICAL)
370
387
  else:
371
388
  logging.getLogger().setLevel(loglevel)
372
-
373
- w = CleepWatchdog(rpc_url)
389
+
390
+ w = CleepWatchdog(rpc_url, catch_up=not skip_catchup)
374
391
  res = w.watch()
375
392
 
376
393
  if not res:
377
394
  sys.exit(1)
378
395
 
396
+ @watchdog.command('watchsvc')
397
+ @click.argument(
398
+ 'action',
399
+ type=click.Choice(
400
+ ['install', 'uninstall', 'enable', 'disable', 'start', 'stop', 'restart', 'status']
401
+ ),
402
+ )
403
+ def watchsvc_cmd(action):
404
+ """
405
+ Manage the durable cleep-watch systemd unit.
406
+
407
+ ACTION is one of: install, uninstall, enable, disable, start, stop, restart, status.
408
+
409
+ The unit runs ``cleep-cli watch`` outside of cleep.service. Typical setup::
410
+
411
+ cleep-cli watchsvc install
412
+ cleep-cli watchsvc enable
413
+ """
414
+ svc = WatchService()
415
+ handlers = {
416
+ 'install': svc.install,
417
+ 'uninstall': svc.uninstall,
418
+ 'enable': svc.enable,
419
+ 'disable': svc.disable,
420
+ 'start': svc.start,
421
+ 'stop': svc.stop,
422
+ 'restart': svc.restart,
423
+ }
424
+ if action == 'status':
425
+ status = svc.status()
426
+ logging.info(status)
427
+ if status != 'active':
428
+ sys.exit(1)
429
+ return
430
+
431
+ if not handlers[action]():
432
+ sys.exit(1)
433
+
379
434
  @click.group()
380
435
  def test():
381
436
  pass
@@ -1497,6 +1497,50 @@ overgeneral-exceptions=Exception
1497
1497
  """
1498
1498
  return self._check_module_tree(module_name, "asset")
1499
1499
 
1500
+ def check_firmwares(self, module_name):
1501
+ """
1502
+ Validate optional ``firmwares/`` zip+sidecar ``.manifest`` pairs and list
1503
+ files for packaging (see ``cleep.libs.internals.firmware_manifest``).
1504
+
1505
+ Returns:
1506
+ dict: errors / warnings / files (same shape as check_scripts)
1507
+ """
1508
+ if not os.path.exists(os.path.join(config.MODULES_DST, module_name)):
1509
+ raise Exception('Module "%s" does not exist' % module_name)
1510
+
1511
+ firmwares_path = os.path.join(config.MODULES_SRC, module_name, "firmwares")
1512
+ out = {
1513
+ "errors": [],
1514
+ "warnings": [],
1515
+ "files": [],
1516
+ }
1517
+ if not os.path.isdir(firmwares_path):
1518
+ return out
1519
+
1520
+ try:
1521
+ from cleep.libs.internals.firmware_manifest import (
1522
+ FirmwareManifestError,
1523
+ scan_firmwares_dir,
1524
+ )
1525
+ except ImportError:
1526
+ out["errors"].append(
1527
+ "Unable to import cleep.libs.internals.firmware_manifest "
1528
+ "(is Cleep core available on PYTHONPATH?)"
1529
+ )
1530
+ return out
1531
+
1532
+ try:
1533
+ scan_firmwares_dir(firmwares_path)
1534
+ except FirmwareManifestError as error:
1535
+ out["errors"].append(str(error))
1536
+ return out
1537
+
1538
+ # Include valid pairs in the package file list
1539
+ tree = self._check_module_tree(module_name, "firmwares")
1540
+ out["files"] = tree["files"]
1541
+ out["warnings"] = tree["warnings"]
1542
+ return out
1543
+
1500
1544
  def check_tests(self, module_name):
1501
1545
  """
1502
1546
  Check test files
@@ -60,6 +60,51 @@ rsync -av "%(REPO_DIR)s/medias/sounds" "%(MEDIA_DST)s/sounds/" --delete
60
60
 
61
61
  return True
62
62
 
63
+ def list_modules(self):
64
+ """
65
+ List module folder names under MODULES_SRC (skips hidden dirs).
66
+
67
+ Returns:
68
+ list: sorted module names
69
+ """
70
+ if not os.path.isdir(config.MODULES_SRC):
71
+ return []
72
+ names = []
73
+ try:
74
+ for name in os.listdir(config.MODULES_SRC):
75
+ if name.startswith("."):
76
+ continue
77
+ path = os.path.join(config.MODULES_SRC, name)
78
+ if os.path.isdir(path):
79
+ names.append(name)
80
+ except OSError as error:
81
+ self.logger.error("Unable to list modules: %s", error)
82
+ return []
83
+ return sorted(names)
84
+
85
+ def sync_all(self):
86
+ """
87
+ Synchronize core and every module from REPO_DIR to execution folders.
88
+
89
+ Used as watch catch-up on start and by the ``syncall`` CLI command so a
90
+ full resync is always one explicit action away.
91
+
92
+ Returns:
93
+ bool: True if every sync succeeded
94
+ """
95
+ self.logger.info("Synchronizing core sources...")
96
+ if not self.core_sync():
97
+ return False
98
+
99
+ modules = self.list_modules()
100
+ for module_name in modules:
101
+ self.logger.info('Synchronizing module "%s" sources...', module_name)
102
+ if not self.module_sync(module_name):
103
+ return False
104
+
105
+ self.logger.info("Sync all done (%d module(s))", len(modules))
106
+ return True
107
+
63
108
  def module_sync(self, module_name, backend=True, frontend=True, scripts=True):
64
109
  """
65
110
  Copy specified module content to valid execution location
@@ -516,6 +516,12 @@ sha256sum $DEB > $SHA256
516
516
  "Error in asset/. Fix it before packaging application: %s"
517
517
  % data_asset["errors"]
518
518
  )
519
+ data_firmwares = check.check_firmwares(module_name)
520
+ if len(data_firmwares["errors"]) > 0:
521
+ raise Exception(
522
+ "Error in firmwares/. Fix it before packaging application: %s"
523
+ % data_firmwares["errors"]
524
+ )
519
525
  data_tests = check.check_tests(module_name)
520
526
  if len(data_tests["errors"]) > 0:
521
527
  raise Exception(
@@ -629,6 +635,10 @@ sha256sum $DEB > $SHA256
629
635
  for a_file in data_asset["files"]:
630
636
  archive.write(a_file["fullpath"], a_file["path"])
631
637
 
638
+ # add firmwares/ (zip + sidecar .manifest; installer copies to APP_FIRMWARE)
639
+ for a_file in data_firmwares["files"]:
640
+ archive.write(a_file["fullpath"], a_file["path"])
641
+
632
642
  # add module.json
633
643
  archive.write(module_json, "module.json")
634
644
 
@@ -0,0 +1,18 @@
1
+ [Unit]
2
+ Description=Cleep development source watcher (cleep-cli watch)
3
+ Documentation=https://github.com/CleepDevice/cleep-cli
4
+ # Independent from cleep.service so backend restarts do not kill the watcher
5
+ # mid-sync (Desktop writes would otherwise be missed until the next edit).
6
+ After=network-online.target
7
+ Wants=network-online.target
8
+
9
+ [Service]
10
+ Type=simple
11
+ ExecStart=/usr/local/bin/cleep-cli watch --loglevel=20
12
+ Restart=always
13
+ RestartSec=3
14
+ # Allow a long catch-up sync on start before the main loop
15
+ TimeoutStartSec=180
16
+
17
+ [Install]
18
+ WantedBy=multi-user.target
@@ -1,4 +1,4 @@
1
1
  #!/usr/bin/env python
2
2
  # -*- coding: utf-8 -*-
3
3
 
4
- VERSION = "1.46.0"
4
+ VERSION = "1.48.0"
@@ -18,6 +18,7 @@ from watchdog.events import (
18
18
  import watchdog.events as events
19
19
  from .cleepapi import CleepApi
20
20
  from .file import File
21
+ from .watchsvc import WATCH_SERVICE
21
22
 
22
23
  _WATCHED_EVENT_TYPES = (
23
24
  EVENT_TYPE_CREATED,
@@ -206,6 +207,11 @@ class CleepHandler(FileSystemEventHandler):
206
207
  REJECTED_FILENAMES = [
207
208
  "4913", # vim furtive temp file to check user permissions
208
209
  ".gitignore",
210
+ # Module-root docs are not synced by module_sync; ignore to avoid noise.
211
+ "CHANGELOG.md",
212
+ "README.md",
213
+ "LICENSE",
214
+ "COPYING",
209
215
  ]
210
216
  REJECTED_EXTENSIONS = [
211
217
  ".swp", # vim
@@ -521,22 +527,24 @@ class CleepWatchdog:
521
527
  Cleep core and module watchdog
522
528
  """
523
529
 
524
- def __init__(self, rpc_url):
530
+ def __init__(self, rpc_url, catch_up=True):
525
531
  """
526
532
  Constructor
527
533
 
528
534
  Args:
529
535
  rpc_url (str): Cleep RPC url
536
+ catch_up (bool): when True, sync core + all modules before watching
537
+ so files written while the watcher was down are not missed
530
538
  """
531
539
  self.logger = logging.getLogger(self.__class__.__name__)
532
540
  self.actions_executor = ActionsExecutor(rpc_url)
541
+ self.catch_up = catch_up
533
542
 
534
543
  def watch(self):
535
544
  """
536
545
  Start watchdog that monitors filesystem changes on Cleep sources
537
546
  """
538
547
  path = "%s/" % config.REPO_DIR
539
- self.actions_executor.start()
540
548
 
541
549
  if not os.path.exists(path):
542
550
  self.logger.error(
@@ -544,6 +552,20 @@ class CleepWatchdog:
544
552
  )
545
553
  return False
546
554
 
555
+ # Catch-up before inotify: Desktop (or editors) may have written while
556
+ # this process was stopped (cleep restart, create-app pause, boot).
557
+ if self.catch_up:
558
+ self.logger.info("Catch-up sync before watching...")
559
+ if not File().sync_all():
560
+ self.logger.error(
561
+ "Catch-up sync failed; fix errors then retry "
562
+ "(cleep-cli syncall / systemctl restart %s)",
563
+ WATCH_SERVICE,
564
+ )
565
+ return False
566
+
567
+ self.actions_executor.start()
568
+
547
569
  event_handler = CleepHandler(self.actions_executor)
548
570
  observer = Observer()
549
571
  event_handler.observer = observer
@@ -0,0 +1,214 @@
1
+ #!/usr/bin/env python
2
+ # -*- coding: utf-8 -*-
3
+
4
+ """
5
+ Manage the durable cleep-watch systemd unit.
6
+
7
+ The unit runs ``cleep-cli watch`` outside of cleep.service so backend restarts
8
+ do not kill the watcher mid-sync.
9
+ """
10
+
11
+ import logging
12
+ import os
13
+ import shutil
14
+
15
+ from .console import Console
16
+
17
+ WATCH_SERVICE = "cleep-watch.service"
18
+ WATCH_UNIT_DST = "/lib/systemd/system/%s" % WATCH_SERVICE
19
+
20
+
21
+ def bundled_unit_path():
22
+ """
23
+ Path to the unit file shipped inside the cleepcli package.
24
+
25
+ Returns:
26
+ str: absolute path to cleep-watch.service
27
+ """
28
+ return os.path.join(os.path.dirname(__file__), "systemd", WATCH_SERVICE)
29
+
30
+
31
+ class WatchService:
32
+ """
33
+ Install / enable / control cleep-watch.service via systemctl.
34
+ """
35
+
36
+ def __init__(self):
37
+ self.logger = logging.getLogger(self.__class__.__name__)
38
+ self.console = Console()
39
+
40
+ def _systemctl(self, *args, timeout=60.0):
41
+ cmd = "/bin/systemctl " + " ".join(args)
42
+ self.logger.debug("systemctl cmd: %s", cmd)
43
+ return self.console.command(cmd, timeout)
44
+
45
+ def _ok(self, resp):
46
+ return not resp.get("killed") and resp.get("returncode", 1) == 0
47
+
48
+ def _daemon_reload(self):
49
+ return self._ok(self._systemctl("daemon-reload"))
50
+
51
+ def install(self):
52
+ """
53
+ Copy the bundled unit to systemd and reload the daemon.
54
+
55
+ Returns:
56
+ bool: True on success
57
+ """
58
+ src = bundled_unit_path()
59
+ if not os.path.isfile(src):
60
+ self.logger.error("Bundled unit not found: %s", src)
61
+ return False
62
+ try:
63
+ os.makedirs(os.path.dirname(WATCH_UNIT_DST), exist_ok=True)
64
+ shutil.copyfile(src, WATCH_UNIT_DST)
65
+ except OSError as error:
66
+ self.logger.error("Unable to install %s: %s", WATCH_UNIT_DST, error)
67
+ return False
68
+ if not self._daemon_reload():
69
+ self.logger.error("systemctl daemon-reload failed after install")
70
+ return False
71
+ self.logger.info("Installed %s", WATCH_UNIT_DST)
72
+ return True
73
+
74
+ def uninstall(self):
75
+ """
76
+ Disable the unit, remove the unit file, reload systemd.
77
+
78
+ Returns:
79
+ bool: True on success (missing unit is OK)
80
+ """
81
+ self.disable()
82
+ try:
83
+ if os.path.isfile(WATCH_UNIT_DST):
84
+ os.remove(WATCH_UNIT_DST)
85
+ self.logger.info("Removed %s", WATCH_UNIT_DST)
86
+ except OSError as error:
87
+ self.logger.error("Unable to remove %s: %s", WATCH_UNIT_DST, error)
88
+ return False
89
+ if not self._daemon_reload():
90
+ self.logger.error("systemctl daemon-reload failed after uninstall")
91
+ return False
92
+ return True
93
+
94
+ def enable(self):
95
+ """
96
+ Enable and start the unit (installs the unit file first if missing).
97
+
98
+ Returns:
99
+ bool: True on success
100
+ """
101
+ if not os.path.isfile(WATCH_UNIT_DST):
102
+ if not self.install():
103
+ return False
104
+ resp = self._systemctl("enable", "--now", WATCH_SERVICE)
105
+ if not self._ok(resp):
106
+ self.logger.error(
107
+ "Unable to enable %s: %s %s",
108
+ WATCH_SERVICE,
109
+ resp.get("stdout"),
110
+ resp.get("stderr"),
111
+ )
112
+ return False
113
+ return self.is_active()
114
+
115
+ def disable(self):
116
+ """
117
+ Disable and stop the unit.
118
+
119
+ Returns:
120
+ bool: True on success (or if unit was not installed)
121
+ """
122
+ if not os.path.isfile(WATCH_UNIT_DST):
123
+ return True
124
+ resp = self._systemctl("disable", "--now", WATCH_SERVICE)
125
+ if not self._ok(resp):
126
+ # Unit may already be inactive/disabled
127
+ self.logger.debug(
128
+ "disable --now %s: %s %s",
129
+ WATCH_SERVICE,
130
+ resp.get("stdout"),
131
+ resp.get("stderr"),
132
+ )
133
+ return True
134
+
135
+ def start(self):
136
+ """
137
+ Start the unit (catch-up sync runs on ExecStart).
138
+
139
+ Returns:
140
+ bool: True on success
141
+ """
142
+ if not os.path.isfile(WATCH_UNIT_DST):
143
+ if not self.install():
144
+ return False
145
+ resp = self._systemctl("start", WATCH_SERVICE)
146
+ if not self._ok(resp):
147
+ self.logger.error(
148
+ "Unable to start %s: %s %s",
149
+ WATCH_SERVICE,
150
+ resp.get("stdout"),
151
+ resp.get("stderr"),
152
+ )
153
+ return False
154
+ return True
155
+
156
+ def stop(self):
157
+ """
158
+ Stop the unit.
159
+
160
+ Returns:
161
+ bool: True on success
162
+ """
163
+ if not os.path.isfile(WATCH_UNIT_DST):
164
+ return True
165
+ resp = self._systemctl("stop", WATCH_SERVICE)
166
+ if not self._ok(resp):
167
+ self.logger.error(
168
+ "Unable to stop %s: %s %s",
169
+ WATCH_SERVICE,
170
+ resp.get("stdout"),
171
+ resp.get("stderr"),
172
+ )
173
+ return False
174
+ return True
175
+
176
+ def restart(self):
177
+ """
178
+ Restart the unit (re-runs catch-up sync on start).
179
+
180
+ Returns:
181
+ bool: True on success
182
+ """
183
+ if not os.path.isfile(WATCH_UNIT_DST):
184
+ return self.enable()
185
+ resp = self._systemctl("restart", WATCH_SERVICE, timeout=180.0)
186
+ if not self._ok(resp):
187
+ self.logger.error(
188
+ "Unable to restart %s: %s %s",
189
+ WATCH_SERVICE,
190
+ resp.get("stdout"),
191
+ resp.get("stderr"),
192
+ )
193
+ return False
194
+ return True
195
+
196
+ def is_active(self):
197
+ """
198
+ Return True if the unit is active.
199
+ """
200
+ resp = self._systemctl("is-active", WATCH_SERVICE, timeout=5.0)
201
+ active = "".join(resp.get("stdout") or []).strip()
202
+ return active == "active"
203
+
204
+ def status(self):
205
+ """
206
+ Print / return a short status string for the unit.
207
+
208
+ Returns:
209
+ str: systemctl is-active output (or "not-installed")
210
+ """
211
+ if not os.path.isfile(WATCH_UNIT_DST):
212
+ return "not-installed"
213
+ resp = self._systemctl("is-active", WATCH_SERVICE, timeout=5.0)
214
+ return "".join(resp.get("stdout") or []).strip() or "unknown"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cleepcli
3
- Version: 1.46.0
3
+ Version: 1.48.0
4
4
  Summary: Cleep-cli helps developers to build great Cleep applications from command line.
5
5
  Home-page: https://github.com/CleepDevice/cleep-cli
6
6
  Author: Tanguy Bonneau
@@ -61,7 +61,9 @@ This utility helps developers to build Cleep applications providing some useful
61
61
  - `modcheckscripts` to check app scripts files
62
62
  - `modcheckcode` to check backend code quality (run pylint)
63
63
  - Watch commands:
64
- - `watch` to watch for repository filesystem changes and automatically update files on execution folders. It also restart backend or/and frontend according to changes.
64
+ - `watch` to watch for repository filesystem changes and automatically update files on execution folders. It also restart backend or/and frontend according to changes (catch-up sync on start).
65
+ - `watchsvc` to install/enable/disable/restart the durable `cleep-watch` systemd unit (runs `watch` outside `cleep.service`).
66
+ - `syncall` to force a one-shot sync of core + all modules (same catch-up as watch start).
65
67
  - Misc commands:
66
68
  - `version` to get cleep-cli version
67
69
 
@@ -85,9 +87,23 @@ Execute this command to get help on this cli:
85
87
 
86
88
  ## Watch usage
87
89
 
88
- Launch `watch` cli command to monitor changes on "/root/cleep-dev" directory after getting official Cleep repository using `coreget` command.
90
+ Preferred: let the CLI own the durable systemd unit:
89
91
 
90
- Then open your favorite editor on your development computer, configure a remote sync plugin to push local changes on your raspberry pi then cleep-cli will synchronize sources on Cleep execution environment and restart application automatically.
92
+ ```
93
+ cleep-cli watchsvc install
94
+ cleep-cli watchsvc enable
95
+ ```
96
+
97
+ The unit runs `cleep-cli watch`, survives `systemctl restart cleep`, and on each start performs a catch-up `syncall` so files written while the watcher was down are applied.
98
+
99
+ Manual one-shot resync (when the UI looks stale):
100
+
101
+ ```
102
+ cleep-cli syncall
103
+ cleep-cli watchsvc restart # catch-up again + resume watch
104
+ ```
105
+
106
+ You can still run `cleep-cli watch` in a terminal for debugging. Push sources to `/root/cleep-dev` (CleepDesktop developer sync or SFTP); the watcher copies them into the execution folders and restarts backend/frontend as needed.
91
107
 
92
108
  ### Paths
93
109
 
@@ -22,15 +22,19 @@ cleepcli/test.py
22
22
  cleepcli/tools.py
23
23
  cleepcli/version.py
24
24
  cleepcli/watch.py
25
+ cleepcli/watchsvc.py
25
26
  cleepcli.egg-info/PKG-INFO
26
27
  cleepcli.egg-info/SOURCES.txt
27
28
  cleepcli.egg-info/dependency_links.txt
28
29
  cleepcli.egg-info/requires.txt
29
30
  cleepcli.egg-info/top_level.txt
31
+ cleepcli/systemd/cleep-watch.service
30
32
  tests/test_check.py
31
33
  tests/test_config.py
32
34
  tests/test_console.py
33
35
  tests/test_coverage.py
34
36
  tests/test_docs_breaking.py
37
+ tests/test_file_sync_all.py
35
38
  tests/test_module_scaffold.py
36
- tests/test_watch.py
39
+ tests/test_watch.py
40
+ tests/test_watchsvc.py
@@ -39,6 +39,7 @@ setup(
39
39
  maintainer_email="tanguy.bonneau@gmail.com",
40
40
  url="https://github.com/CleepDevice/cleep-cli",
41
41
  packages=["cleepcli"],
42
+ package_data={"cleepcli": ["systemd/cleep-watch.service"]},
42
43
  include_package_data=True,
43
44
  install_requires=get_requirements(),
44
45
  python_requires=">=3.7",
@@ -0,0 +1,91 @@
1
+ #!/usr/bin/env python
2
+ # -*- coding: utf-8 -*-
3
+
4
+ import os
5
+ import shutil
6
+ import tempfile
7
+
8
+ from unittest.mock import Mock, patch
9
+
10
+ from cleepcli import config
11
+ from cleepcli.file import File
12
+
13
+
14
+ def test_list_modules_sorted():
15
+ repo = tempfile.mkdtemp()
16
+ try:
17
+ modules = os.path.join(repo, "modules")
18
+ os.makedirs(os.path.join(modules, "zeta"))
19
+ os.makedirs(os.path.join(modules, "alpha"))
20
+ os.makedirs(os.path.join(modules, ".hidden"))
21
+ open(os.path.join(modules, "file.txt"), "w", encoding="utf-8").close()
22
+
23
+ with patch.object(config, "MODULES_SRC", modules):
24
+ assert File().list_modules() == ["alpha", "zeta"]
25
+ finally:
26
+ shutil.rmtree(repo)
27
+
28
+
29
+ def test_sync_all_runs_core_then_modules():
30
+ f = File()
31
+ f.core_sync = Mock(return_value=True)
32
+ f.module_sync = Mock(return_value=True)
33
+ f.list_modules = Mock(return_value=["audio", "developer"])
34
+
35
+ assert f.sync_all() is True
36
+ f.core_sync.assert_called_once_with()
37
+ assert f.module_sync.call_args_list[0].args[0] == "audio"
38
+ assert f.module_sync.call_args_list[1].args[0] == "developer"
39
+
40
+
41
+ def test_sync_all_stops_on_core_failure():
42
+ f = File()
43
+ f.core_sync = Mock(return_value=False)
44
+ f.module_sync = Mock(return_value=True)
45
+ f.list_modules = Mock(return_value=["audio"])
46
+
47
+ assert f.sync_all() is False
48
+ f.module_sync.assert_not_called()
49
+
50
+
51
+ def test_watch_catch_up_calls_sync_all():
52
+ from cleepcli.watch import CleepWatchdog
53
+
54
+ with patch("cleepcli.watch.File") as file_cls, patch(
55
+ "cleepcli.watch.os.path.exists", return_value=True
56
+ ), patch("cleepcli.watch.Observer"), patch(
57
+ "cleepcli.watch.CleepHandler"
58
+ ) as handler_cls, patch(
59
+ "cleepcli.watch.ActionsExecutor"
60
+ ), patch(
61
+ "cleepcli.watch.time.sleep", side_effect=KeyboardInterrupt
62
+ ):
63
+ file_cls.return_value.sync_all.return_value = True
64
+ handler = handler_cls.return_value
65
+ handler._watched = {"/tmp"}
66
+ handler.schedule_all = Mock()
67
+
68
+ watchdog = CleepWatchdog("https://127.0.0.1:443", catch_up=True)
69
+ assert watchdog.watch() is True
70
+ file_cls.return_value.sync_all.assert_called_once_with()
71
+
72
+
73
+ def test_watch_skip_catch_up():
74
+ from cleepcli.watch import CleepWatchdog
75
+
76
+ with patch("cleepcli.watch.File") as file_cls, patch(
77
+ "cleepcli.watch.os.path.exists", return_value=True
78
+ ), patch("cleepcli.watch.Observer"), patch(
79
+ "cleepcli.watch.CleepHandler"
80
+ ) as handler_cls, patch(
81
+ "cleepcli.watch.ActionsExecutor"
82
+ ), patch(
83
+ "cleepcli.watch.time.sleep", side_effect=KeyboardInterrupt
84
+ ):
85
+ handler = handler_cls.return_value
86
+ handler._watched = {"/tmp"}
87
+ handler.schedule_all = Mock()
88
+
89
+ watchdog = CleepWatchdog("https://127.0.0.1:443", catch_up=False)
90
+ assert watchdog.watch() is True
91
+ file_cls.return_value.sync_all.assert_not_called()
@@ -131,6 +131,13 @@ def test_module_frontend_is_queued(handler):
131
131
  )
132
132
 
133
133
 
134
+ def test_module_changelog_is_ignored(handler):
135
+ h, executor = handler
136
+ path = os.path.join(config.MODULES_SRC, "audio", "CHANGELOG.md")
137
+ h.on_any_event(FileModifiedEvent(path))
138
+ executor.note_change.assert_not_called()
139
+
140
+
134
141
  def test_created_git_directory_is_not_watched(handler):
135
142
  h, executor = handler
136
143
  h.observer = Mock()
@@ -0,0 +1,57 @@
1
+ #!/usr/bin/env python
2
+ # -*- coding: utf-8 -*-
3
+
4
+ import os
5
+ from unittest.mock import Mock, patch
6
+
7
+ from cleepcli.watchsvc import WatchService, WATCH_UNIT_DST, bundled_unit_path
8
+
9
+
10
+ def test_bundled_unit_path_exists():
11
+ assert os.path.isfile(bundled_unit_path())
12
+
13
+
14
+ def test_install_copies_unit_and_reloads():
15
+ svc = WatchService()
16
+ svc.console = Mock()
17
+ svc.console.command.return_value = {"returncode": 0, "stdout": "", "stderr": ""}
18
+
19
+ with patch("cleepcli.watchsvc.shutil.copyfile") as copyfile, patch(
20
+ "cleepcli.watchsvc.os.makedirs"
21
+ ), patch("cleepcli.watchsvc.os.path.isfile", return_value=True):
22
+ assert svc.install() is True
23
+ copyfile.assert_called_once()
24
+ assert copyfile.call_args.args[1] == WATCH_UNIT_DST
25
+ svc.console.command.assert_called_with(
26
+ "/bin/systemctl daemon-reload", 60.0
27
+ )
28
+
29
+
30
+ def test_enable_installs_when_missing():
31
+ svc = WatchService()
32
+ svc.install = Mock(return_value=True)
33
+ svc.is_active = Mock(return_value=True)
34
+ svc.console = Mock()
35
+ svc.console.command.return_value = {"returncode": 0, "stdout": "", "stderr": ""}
36
+
37
+ with patch("cleepcli.watchsvc.os.path.isfile", return_value=False):
38
+ assert svc.enable() is True
39
+ svc.install.assert_called_once_with()
40
+
41
+
42
+ def test_status_not_installed():
43
+ svc = WatchService()
44
+ with patch("cleepcli.watchsvc.os.path.isfile", return_value=False):
45
+ assert svc.status() == "not-installed"
46
+
47
+
48
+ def test_status_active():
49
+ svc = WatchService()
50
+ svc.console = Mock()
51
+ svc.console.command.return_value = {
52
+ "returncode": 0,
53
+ "stdout": ["active"],
54
+ "stderr": "",
55
+ }
56
+ with patch("cleepcli.watchsvc.os.path.isfile", return_value=True):
57
+ assert svc.status() == "active"
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
File without changes
File without changes
File without changes