cleepcli 1.43.10__tar.gz → 1.44.2__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 (33) hide show
  1. {cleepcli-1.43.10/cleepcli.egg-info → cleepcli-1.44.2}/PKG-INFO +1 -1
  2. {cleepcli-1.43.10 → cleepcli-1.44.2}/bin/ccli +33 -16
  3. {cleepcli-1.43.10 → cleepcli-1.44.2}/bin/cleep-cli +33 -16
  4. {cleepcli-1.43.10 → cleepcli-1.44.2}/cleepcli/ci.py +3 -1
  5. {cleepcli-1.43.10 → cleepcli-1.44.2}/cleepcli/cleepapi.py +158 -6
  6. {cleepcli-1.43.10 → cleepcli-1.44.2}/cleepcli/console.py +38 -48
  7. {cleepcli-1.43.10 → cleepcli-1.44.2}/cleepcli/file.py +67 -38
  8. {cleepcli-1.43.10 → cleepcli-1.44.2}/cleepcli/git.py +2 -2
  9. {cleepcli-1.43.10 → cleepcli-1.44.2}/cleepcli/module.py +1 -1
  10. {cleepcli-1.43.10 → cleepcli-1.44.2}/cleepcli/version.py +1 -1
  11. cleepcli-1.44.2/cleepcli/watch.py +573 -0
  12. {cleepcli-1.43.10 → cleepcli-1.44.2/cleepcli.egg-info}/PKG-INFO +1 -1
  13. {cleepcli-1.43.10 → cleepcli-1.44.2}/cleepcli.egg-info/SOURCES.txt +3 -1
  14. cleepcli-1.44.2/tests/test_console.py +51 -0
  15. cleepcli-1.44.2/tests/test_watch.py +314 -0
  16. cleepcli-1.43.10/cleepcli/watch.py +0 -375
  17. {cleepcli-1.43.10 → cleepcli-1.44.2}/LICENSE +0 -0
  18. {cleepcli-1.43.10 → cleepcli-1.44.2}/MANIFEST.in +0 -0
  19. {cleepcli-1.43.10 → cleepcli-1.44.2}/README.md +0 -0
  20. {cleepcli-1.43.10 → cleepcli-1.44.2}/cleepcli/__init__.py +0 -0
  21. {cleepcli-1.43.10 → cleepcli-1.44.2}/cleepcli/check.py +0 -0
  22. {cleepcli-1.43.10 → cleepcli-1.44.2}/cleepcli/config.py +0 -0
  23. {cleepcli-1.43.10 → cleepcli-1.44.2}/cleepcli/docs.py +0 -0
  24. {cleepcli-1.43.10 → cleepcli-1.44.2}/cleepcli/package.py +0 -0
  25. {cleepcli-1.43.10 → cleepcli-1.44.2}/cleepcli/test.py +0 -0
  26. {cleepcli-1.43.10 → cleepcli-1.44.2}/cleepcli/tools.py +0 -0
  27. {cleepcli-1.43.10 → cleepcli-1.44.2}/cleepcli.egg-info/dependency_links.txt +0 -0
  28. {cleepcli-1.43.10 → cleepcli-1.44.2}/cleepcli.egg-info/requires.txt +0 -0
  29. {cleepcli-1.43.10 → cleepcli-1.44.2}/cleepcli.egg-info/top_level.txt +0 -0
  30. {cleepcli-1.43.10 → cleepcli-1.44.2}/pyproject.toml +0 -0
  31. {cleepcli-1.43.10 → cleepcli-1.44.2}/requirements.txt +0 -0
  32. {cleepcli-1.43.10 → cleepcli-1.44.2}/setup.cfg +0 -0
  33. {cleepcli-1.43.10 → cleepcli-1.44.2}/setup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cleepcli
3
- Version: 1.43.10
3
+ Version: 1.44.2
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
@@ -90,7 +90,8 @@ def coreget():
90
90
 
91
91
  # force coresync to synchronize installation with repo
92
92
  f = File()
93
- res = f.core_sync()
93
+ if not f.core_sync():
94
+ sys.exit(1)
94
95
 
95
96
  # core modules
96
97
  if not get_core_mods():
@@ -290,12 +291,24 @@ def modssync(run_scripts):
290
291
  f = File()
291
292
 
292
293
  for module in config.MODULES_REPO_URL.keys():
293
- res = f.module_sync(module)
294
- if not res:
295
- sys.exit(1)
296
- res = f.module_run_install_scripts(module)
297
- if not res:
298
- sys.exit(1)
294
+ if run_scripts:
295
+ # Same order as package install: scripts + preinst, then files, then postinst
296
+ res = f.module_sync(module, backend=False, frontend=False, scripts=True)
297
+ if not res:
298
+ sys.exit(1)
299
+ res = f.module_run_install_script(module, "preinst.sh")
300
+ if not res:
301
+ sys.exit(1)
302
+ res = f.module_sync(module, backend=True, frontend=True, scripts=True)
303
+ if not res:
304
+ sys.exit(1)
305
+ res = f.module_run_install_script(module, "postinst.sh")
306
+ if not res:
307
+ sys.exit(1)
308
+ else:
309
+ res = f.module_sync(module)
310
+ if not res:
311
+ sys.exit(1)
299
312
 
300
313
  logging.info('Done')
301
314
 
@@ -899,17 +912,21 @@ def cli():
899
912
 
900
913
 
901
914
  if __name__ == '__main__':
902
- def is_raspbian():
903
- if platform.system()!='Linux':
915
+ def is_raspberry_pi_os():
916
+ if platform.system() != 'Linux':
904
917
  return False
905
- res = subprocess.Popen(u'cat /etc/os-release | grep -i raspbian | wc -l', stdout=subprocess.PIPE, shell=True)
906
- stdout = res.communicate()[0]
907
- if stdout.strip() == '0':
918
+ try:
919
+ with open('/etc/os-release', 'r', encoding='utf-8') as handle:
920
+ content = handle.read().lower()
921
+ except OSError:
908
922
  return False
909
- return True
923
+ return any(
924
+ token in content for token in ('raspbian', 'raspios', 'raspberry')
925
+ )
910
926
 
911
- # execute only on raspbian
912
- if is_raspbian():
927
+ # execute only on Raspberry Pi OS
928
+ if is_raspberry_pi_os():
913
929
  cli()
914
930
  else:
915
- click.echo('Cleep-cli runs only on RaspberrypiOS distribution')
931
+ click.echo('Cleep-cli runs only on Raspberry Pi OS')
932
+ sys.exit(1)
@@ -90,7 +90,8 @@ def coreget():
90
90
 
91
91
  # force coresync to synchronize installation with repo
92
92
  f = File()
93
- res = f.core_sync()
93
+ if not f.core_sync():
94
+ sys.exit(1)
94
95
 
95
96
  # core modules
96
97
  if not get_core_mods():
@@ -290,12 +291,24 @@ def modssync(run_scripts):
290
291
  f = File()
291
292
 
292
293
  for module in config.MODULES_REPO_URL.keys():
293
- res = f.module_sync(module)
294
- if not res:
295
- sys.exit(1)
296
- res = f.module_run_install_scripts(module)
297
- if not res:
298
- sys.exit(1)
294
+ if run_scripts:
295
+ # Same order as package install: scripts + preinst, then files, then postinst
296
+ res = f.module_sync(module, backend=False, frontend=False, scripts=True)
297
+ if not res:
298
+ sys.exit(1)
299
+ res = f.module_run_install_script(module, "preinst.sh")
300
+ if not res:
301
+ sys.exit(1)
302
+ res = f.module_sync(module, backend=True, frontend=True, scripts=True)
303
+ if not res:
304
+ sys.exit(1)
305
+ res = f.module_run_install_script(module, "postinst.sh")
306
+ if not res:
307
+ sys.exit(1)
308
+ else:
309
+ res = f.module_sync(module)
310
+ if not res:
311
+ sys.exit(1)
299
312
 
300
313
  logging.info('Done')
301
314
 
@@ -899,17 +912,21 @@ def cli():
899
912
 
900
913
 
901
914
  if __name__ == '__main__':
902
- def is_raspbian():
903
- if platform.system()!='Linux':
915
+ def is_raspberry_pi_os():
916
+ if platform.system() != 'Linux':
904
917
  return False
905
- res = subprocess.Popen(u'cat /etc/os-release | grep -i raspbian | wc -l', stdout=subprocess.PIPE, shell=True)
906
- stdout = res.communicate()[0]
907
- if stdout.strip() == '0':
918
+ try:
919
+ with open('/etc/os-release', 'r', encoding='utf-8') as handle:
920
+ content = handle.read().lower()
921
+ except OSError:
908
922
  return False
909
- return True
923
+ return any(
924
+ token in content for token in ('raspbian', 'raspios', 'raspberry')
925
+ )
910
926
 
911
- # execute only on raspbian
912
- if is_raspbian():
927
+ # execute only on Raspberry Pi OS
928
+ if is_raspberry_pi_os():
913
929
  cli()
914
930
  else:
915
- click.echo('Cleep-cli runs only on RaspberrypiOS distribution')
931
+ click.echo('Cleep-cli runs only on Raspberry Pi OS')
932
+ sys.exit(1)
@@ -12,7 +12,7 @@ import urllib.parse
12
12
  from . import config
13
13
  from .console import Console
14
14
  from .check import Check
15
- from .cleepapi import resolve_rpc_url
15
+ from .cleepapi import local_socket_available, request_over_local_socket, resolve_rpc_url
16
16
  import subprocess
17
17
 
18
18
  requests.packages.urllib3.disable_warnings()
@@ -284,6 +284,8 @@ class Ci:
284
284
  """
285
285
  POST JSON to Cleep /command (self-signed TLS accepted).
286
286
  """
287
+ if local_socket_available():
288
+ return request_over_local_socket('POST', self.command_url, json_body=payload)
287
289
  return requests.post(self.command_url, json=payload, verify=False)
288
290
 
289
291
  def _rpc_get(self, url):
@@ -17,6 +17,9 @@ DEFAULT_RPC_URLS = (
17
17
  'https://127.0.0.1:443',
18
18
  'http://127.0.0.1:80',
19
19
  )
20
+ # Local commands (frontend restart, docs, CI) use this socket so they
21
+ # stay available when TCP requires the Cleep password.
22
+ LOCAL_SOCKET_PATH = '/run/cleep/rpc.sock'
20
23
 
21
24
 
22
25
  def resolve_rpc_url(urls=None, timeout=3.0):
@@ -54,6 +57,144 @@ def resolve_rpc_url(urls=None, timeout=3.0):
54
57
  raise Exception('Unable to reach Cleep RPC (tried %s): %s' % (', '.join(candidates), last_error))
55
58
 
56
59
 
60
+ def local_socket_available(path=None):
61
+ """
62
+ Return True when the local Cleep RPC socket exists.
63
+ """
64
+ return os.path.exists(path or LOCAL_SOCKET_PATH)
65
+
66
+
67
+ def _http_path(url):
68
+ """
69
+ Return the path and query of an RPC URL.
70
+ """
71
+ parts = urllib.parse.urlsplit(url)
72
+ path = parts.path or '/'
73
+ if parts.query:
74
+ path += '?' + parts.query
75
+ return path
76
+
77
+
78
+ def _read_http_response(sock):
79
+ """
80
+ Read one HTTP/1.1 response from a socket.
81
+
82
+ Returns:
83
+ tuple: status code (int), body (bytes)
84
+ """
85
+ data = b''
86
+ while b'\r\n\r\n' not in data:
87
+ chunk = sock.recv(4096)
88
+ if not chunk:
89
+ break
90
+ data += chunk
91
+ if b'\r\n\r\n' not in data:
92
+ raise Exception('Incomplete HTTP response from local socket')
93
+
94
+ header_blob, rest = data.split(b'\r\n\r\n', 1)
95
+ lines = header_blob.split(b'\r\n')
96
+ status_code = int(lines[0].split()[1])
97
+ headers = {}
98
+ for line in lines[1:]:
99
+ if b':' not in line:
100
+ continue
101
+ name, value = line.split(b':', 1)
102
+ headers[name.decode('ascii').lower()] = value.strip().decode('ascii')
103
+
104
+ if headers.get('transfer-encoding', '').lower() == 'chunked':
105
+ body = _read_chunked(sock, rest)
106
+ elif 'content-length' in headers:
107
+ length = int(headers['content-length'])
108
+ while len(rest) < length:
109
+ chunk = sock.recv(4096)
110
+ if not chunk:
111
+ break
112
+ rest += chunk
113
+ body = rest[:length]
114
+ else:
115
+ while True:
116
+ chunk = sock.recv(4096)
117
+ if not chunk:
118
+ break
119
+ rest += chunk
120
+ body = rest
121
+ return status_code, body
122
+
123
+
124
+ def _read_chunked(sock, pending):
125
+ """
126
+ Decode a chunked HTTP body.
127
+ """
128
+ data = pending
129
+ body = b''
130
+ while True:
131
+ while b'\r\n' not in data:
132
+ chunk = sock.recv(4096)
133
+ if not chunk:
134
+ return body
135
+ data += chunk
136
+ size_line, data = data.split(b'\r\n', 1)
137
+ size = int(size_line.split(b';', 1)[0], 16)
138
+ if size == 0:
139
+ return body
140
+ while len(data) < size + 2:
141
+ chunk = sock.recv(4096)
142
+ if not chunk:
143
+ return body
144
+ data += chunk
145
+ body += data[:size]
146
+ data = data[size + 2:]
147
+
148
+
149
+ def request_over_local_socket(method, url, json_body=None, timeout=30.0, path=None):
150
+ """
151
+ Send one HTTP request through the local Cleep Unix socket.
152
+
153
+ Args:
154
+ method (str): HTTP method
155
+ url (str): request URL, used for the path when path is omitted
156
+ json_body (dict): JSON body for POST
157
+ timeout (float): socket timeout in seconds
158
+ path (str): socket path
159
+
160
+ Returns:
161
+ requests.Response
162
+ """
163
+ import socket
164
+ from requests import Response
165
+
166
+ socket_path = path or LOCAL_SOCKET_PATH
167
+ http_path = _http_path(url)
168
+ body = b''
169
+ headers = [
170
+ '%s %s HTTP/1.1' % (method, http_path),
171
+ 'Host: localhost',
172
+ 'Accept: application/json',
173
+ 'Connection: close',
174
+ ]
175
+ if json_body is not None:
176
+ body = json.dumps(json_body).encode('utf-8')
177
+ headers.append('Content-Type: application/json')
178
+ headers.append('Content-Length: %s' % len(body))
179
+ raw = ('\r\n'.join(headers) + '\r\n\r\n').encode('ascii') + body
180
+
181
+ sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
182
+ sock.settimeout(timeout)
183
+ try:
184
+ sock.connect(socket_path)
185
+ sock.sendall(raw)
186
+ status_code, payload = _read_http_response(sock)
187
+ finally:
188
+ sock.close()
189
+
190
+ response = Response()
191
+ response.status_code = status_code
192
+ response._content = payload
193
+ response.url = 'http://localhost' + http_path
194
+ response.encoding = 'utf-8'
195
+ return response
196
+
197
+
57
198
  class CleepApi():
58
199
  """
59
200
  Cleep api helper
@@ -81,9 +222,10 @@ class CleepApi():
81
222
 
82
223
  cmd = '/bin/systemctl restart cleep'
83
224
  c = Console()
84
- resp = c.command(cmd)
225
+ # Maximum wait. systemctl returns as soon as the service has restarted.
226
+ resp = c.command(cmd, timeout=60)
85
227
  self.logger.debug('Systemctl resp: %s' % resp)
86
- if resp['error'] or resp['killed']:
228
+ if resp['killed'] or resp['returncode'] != 0:
87
229
  self.logger.error('Error restarting cleep backend')
88
230
  return False
89
231
 
@@ -95,7 +237,11 @@ class CleepApi():
95
237
  """
96
238
  self.logger.info('Restarting frontend')
97
239
  data = {'to':'developer', 'command':'restart_frontend'}
98
- self.__post(self.command_url, data)
240
+ result = self.__post(self.command_url, data)
241
+ if not result or result[0] != 200:
242
+ self.logger.error('Error restarting cleep frontend')
243
+ return False
244
+ return True
99
245
 
100
246
  def get_documentation(self, module_name):
101
247
  """
@@ -132,7 +278,7 @@ class CleepApi():
132
278
  (status_code, resp) = self.__get(url)
133
279
 
134
280
  if status_code != 200:
135
- raise Exception("Unable to call cleep %s endpoint", url)
281
+ raise Exception("Unable to call cleep %s endpoint" % url)
136
282
  return resp
137
283
 
138
284
  def __post(self, url, data):
@@ -152,7 +298,10 @@ class CleepApi():
152
298
  """
153
299
  try:
154
300
  self.logger.debug("POST url: %s", url)
155
- resp = requests.post(url, json=data, verify=False)
301
+ if local_socket_available():
302
+ resp = request_over_local_socket('POST', url, json_body=data)
303
+ else:
304
+ resp = requests.post(url, json=data, verify=False)
156
305
  resp_data = resp.json()
157
306
  self.logger.debug('Response[%s]: %s', resp.status_code, resp_data)
158
307
  return (resp.status_code, resp_data)
@@ -178,7 +327,10 @@ class CleepApi():
178
327
  """
179
328
  try:
180
329
  self.logger.debug("GET url: %s", url)
181
- resp = requests.get(url, verify=False)
330
+ if local_socket_available():
331
+ resp = request_over_local_socket('GET', url)
332
+ else:
333
+ resp = requests.get(url, verify=False)
182
334
  resp_data = resp.json()
183
335
  self.logger.debug('Response[%s]: %s', resp.status_code, resp_data)
184
336
  return (resp.status_code, resp_data)
@@ -14,6 +14,9 @@ import signal
14
14
  import logging
15
15
  import re
16
16
 
17
+ ON_POSIX = os.name == "posix"
18
+
19
+
17
20
  class EndlessConsole(Thread):
18
21
  """
19
22
  Helper class to execute long command line (system update...)
@@ -322,65 +325,52 @@ class Console():
322
325
  stdout=subprocess.PIPE,
323
326
  stderr=subprocess.PIPE,
324
327
  close_fds=True,
325
- preexec_fn=os.setsid
328
+ preexec_fn=os.setsid if ON_POSIX else None,
326
329
  )
327
330
  pid = proc.pid
328
-
329
- # wait for end of command line
330
- done = False
331
- start = time.time()
332
331
  killed = False
333
- return_code = None
334
- while not done:
335
- # read stds to avoid deadlocks
336
- (stdout, stderr) = proc.communicate()
337
- if stdout:
338
- result['stdout'] += self.__process_lines(stdout.split(b'\n'))
339
- if stderr:
340
- result['stderr'] += self.__process_lines(stderr.split(b'\n'))
341
-
342
- # check if command has finished
343
- proc.poll()
344
- if proc.returncode is not None:
345
- # command terminated
346
- self.logger.trace('Command terminated with returncode %s' % proc.returncode)
347
- return_code = proc.returncode
348
- self.last_return_code = return_code
349
- done = True
350
- break
351
-
352
- # check timeout
353
- if time.time() > (start + timeout):
354
- # timeout is over, kill command
355
- pgid = os.getpgid(pid)
356
- self.logger.debug('Timeout over, kill command for PID=%s PGID=%s' % (pid, pgid))
357
- try:
358
- if ON_POSIX:
359
- os.killpg(pgid, signal.SIGKILL)
360
- else: # pragma: no cover
361
- proc.kill()
362
- except Exception: # pragma: no cover
363
- pass
364
- killed = True
365
- break
332
+ stdout = b""
333
+ stderr = b""
366
334
 
367
- # pause
368
- time.sleep(0.125)
369
-
370
- # prepare result
371
- result['returncode'] = return_code
372
- if not killed:
373
- result['error'] = len(result['stderr']) > 0
374
- self.logger.trace('Result: %s' % result)
335
+ try:
336
+ stdout, stderr = proc.communicate(timeout=timeout)
337
+ except subprocess.TimeoutExpired:
338
+ self.logger.debug("Timeout over, kill command for PID=%s" % pid)
339
+ try:
340
+ if ON_POSIX:
341
+ os.killpg(os.getpgid(pid), signal.SIGKILL)
342
+ else: # pragma: no cover
343
+ proc.kill()
344
+ except Exception: # pragma: no cover
345
+ pass
346
+ killed = True
347
+ try:
348
+ stdout, stderr = proc.communicate(timeout=1.0)
349
+ except Exception: # pragma: no cover
350
+ stdout, stderr = b"", b""
351
+
352
+ if stdout:
353
+ result["stdout"] = self.__process_lines(stdout.split(b"\n"))
354
+ if stderr:
355
+ result["stderr"] = self.__process_lines(stderr.split(b"\n"))
356
+
357
+ return_code = proc.returncode
358
+ self.last_return_code = return_code
359
+ result["returncode"] = return_code
360
+ result["killed"] = killed
361
+ # Prefer return code over stderr: tools like systemctl often write to stderr on success
362
+ result["error"] = killed or return_code not in (0, None)
363
+ self.logger.trace("Command terminated with returncode %s" % return_code)
364
+ self.logger.trace("Result: %s" % result)
375
365
 
376
366
  # make sure all stds are closed
377
367
  try:
378
368
  proc.stdout.close()
379
- except Exception: # pragma: no cover
369
+ except Exception: # pragma: no cover
380
370
  pass
381
371
  try:
382
372
  proc.stderr.close()
383
- except Exception: # pragma: no cover
373
+ except Exception: # pragma: no cover
384
374
  pass
385
375
 
386
376
  # trigger callback (used for delayed command)
@@ -31,37 +31,48 @@ class File():
31
31
  'MEDIA_DST': config.MEDIA_DST,
32
32
  'CONFIG_DIR': config.CONFIG_DIR,
33
33
  }
34
+ variables['MODULES_DST'] = config.MODULES_DST
34
35
  cmd = """
36
+ set -e
35
37
  mkdir -p "%(HTML_DST)s/"
36
- mkdir -p "%(CORE_DST)s/modules"
38
+ mkdir -p "%(CORE_DST)s"
39
+ mkdir -p "%(MODULES_DST)s"
37
40
  mkdir -p "%(MEDIA_DST)s/modules"
38
41
  mkdir -p "%(CONFIG_DIR)s"
42
+ # Python imports cleep.modules from CORE_DST/modules; it must point at the live apps tree
43
+ if [ -e "%(CORE_DST)s/modules" ] && [ ! -L "%(CORE_DST)s/modules" ]; then
44
+ rm -rf "%(CORE_DST)s/modules"
45
+ fi
46
+ ln -sfn "%(MODULES_DST)s" "%(CORE_DST)s/modules"
39
47
  rsync -av "%(REPO_DIR)s/cleep/" "%(CORE_DST)s/" --exclude "/tests/" --exclude "modules" --exclude "*__pycache__*" --delete --exclude "*.pyc" --keep-dirlinks
40
48
  rsync -av "%(REPO_DIR)s/html/" "%(HTML_DST)s/" --delete --exclude "js/modules/" --exclude "*node_modules*"
41
49
  rsync -av "%(REPO_DIR)s/bin/cleep" "%(BIN_DST)s/cleep"
42
50
  rsync -av "%(REPO_DIR)s/medias/sounds" "%(MEDIA_DST)s/sounds/" --delete
43
51
  """ % variables
44
52
  self.logger.debug('Coresync cmd: %s' % cmd)
45
- resp = c.command(cmd, 15)
53
+ resp = c.command(cmd, 120)
46
54
  self.logger.debug('Coresync stdout:\n%s' % '\n'.join(resp['stdout']))
47
55
  if resp['stderr']:
48
56
  self.logger.debug('Coresync stderr:\n%s' % '\n'.join(resp['stderr']))
49
- if resp['error'] or resp['killed']:
57
+ if resp['killed'] or resp['returncode'] != 0:
50
58
  self.logger.error('Error occured while sync core content: %s' % ('killed' if resp['killed'] else resp['stderr']))
51
59
  return False
52
60
 
53
61
  return True
54
62
 
55
- def module_sync(self, module_name):
56
- """
63
+ def module_sync(self, module_name, backend=True, frontend=True, scripts=True):
64
+ """
57
65
  Copy specified module content to valid execution location
66
+
67
+ Args:
68
+ module_name (str): module folder name under MODULES_SRC
69
+ backend (bool): sync backend/ when True
70
+ frontend (bool): sync frontend/ when True
71
+ scripts (bool): sync scripts/ when True
58
72
  """
59
73
  if not os.path.exists(os.path.join(config.MODULES_SRC, module_name)):
60
74
  self.logger.error('Module "%s" doesn\'t exist [%s]' % (module_name, os.path.join(config.MODULES_SRC, module_name)))
61
75
  return False
62
-
63
- if not os.path.exists(os.path.join(config.MODULES_DST, module_name)):
64
- os.makedirs(os.path.join(config.MODULES_DST, module_name))
65
76
 
66
77
  mod_backend_src = os.path.join(config.MODULES_SRC, module_name, 'backend/')
67
78
  mod_backend_dst = os.path.join(config.MODULES_DST, module_name)
@@ -70,61 +81,79 @@ rsync -av "%(REPO_DIR)s/medias/sounds" "%(MEDIA_DST)s/sounds/" --delete
70
81
  mod_scripts_src = os.path.join(config.MODULES_SRC, module_name, 'scripts/')
71
82
  mod_scripts_dst = os.path.join(config.MODULES_SCRIPTS_DST, module_name)
72
83
 
73
- c = Console()
74
- cmd = """
84
+ parts = []
85
+ if backend:
86
+ parts.append("""
75
87
  if [ -d "%(BACKEND_SRC)s" ]; then
76
88
  /bin/mkdir -p "%(BACKEND_DST)s"
77
89
  rsync -av "%(BACKEND_SRC)s" "%(BACKEND_DST)s" --delete --exclude "*.pyc" --exclude "*__pycache__*"
78
90
  fi
91
+ """ % {'BACKEND_SRC': mod_backend_src, 'BACKEND_DST': mod_backend_dst})
92
+ if frontend:
93
+ parts.append("""
79
94
  if [ -d "%(FRONTEND_SRC)s" ]; then
80
95
  /bin/mkdir -p "%(FRONTEND_DST)s"
81
96
  rsync -av "%(FRONTEND_SRC)s" "%(FRONTEND_DST)s" --delete --exclude "*node_modules*"
82
97
  fi
98
+ """ % {'FRONTEND_SRC': mod_frontend_src, 'FRONTEND_DST': mod_frontend_dst})
99
+ if scripts:
100
+ parts.append("""
83
101
  if [ -d "%(SCRIPTS_SRC)s" ]; then
84
102
  /bin/mkdir -p "%(SCRIPTS_DST)s"
85
103
  rsync -av "%(SCRIPTS_SRC)s" "%(SCRIPTS_DST)s" --delete --exclude "*__pycache__*"
86
104
  fi
87
- """ % {
88
- 'BACKEND_SRC': mod_backend_src, 'BACKEND_DST': mod_backend_dst,
89
- 'FRONTEND_SRC': mod_frontend_src, 'FRONTEND_DST': mod_frontend_dst,
90
- 'SCRIPTS_SRC': mod_scripts_src, 'SCRIPTS_DST': mod_scripts_dst
91
- }
105
+ """ % {'SCRIPTS_SRC': mod_scripts_src, 'SCRIPTS_DST': mod_scripts_dst})
106
+
107
+ if not parts:
108
+ return True
109
+
110
+ c = Console()
111
+ cmd = "set -e\n" + "\n".join(parts)
92
112
  self.logger.debug('Modsync cmd: %s' % cmd)
93
- resp = c.command(cmd, 15)
113
+ resp = c.command(cmd, 120)
94
114
 
95
115
  self.logger.debug('Modsync stdout:\n%s' % '\n'.join(resp['stdout']))
96
116
  if resp['stderr']:
97
117
  self.logger.debug('Modsync stderr:\n%s' % '\n'.join(resp['stderr']))
98
- if resp['error'] or resp[u'killed']:
99
- self.logger.error(u'Error occured while sync module content: %s' % (u'killed' if resp[u'killed'] else resp[u'stderr']))
118
+ if resp['killed'] or resp['returncode'] != 0:
119
+ self.logger.error('Error occured while sync module content: %s' % ('killed' if resp['killed'] else resp['stderr']))
100
120
  return False
101
121
 
102
122
  return True
103
123
 
104
- def module_run_install_scripts(self, module_name):
124
+ def module_run_install_script(self, module_name, script_name):
105
125
  """
106
- Run module installation scripts
126
+ Run one module installation script from the synced scripts directory
127
+
128
+ Args:
129
+ module_name (str): module name
130
+ script_name (str): script filename (preinst.sh, postinst.sh, ...)
107
131
  """
108
132
  if not os.path.exists(os.path.join(config.MODULES_SRC, module_name)):
109
133
  self.logger.error('Module "%s" doesn\'t exist [%s]' % (module_name, os.path.join(config.MODULES_SRC, module_name)))
110
134
  return False
111
135
 
112
- preinst_script = os.path.join(config.MODULES_SCRIPTS_DST, module_name, "preinst.sh")
113
- postinst_script = os.path.join(config.MODULES_SCRIPTS_DST, module_name, "postinst.sh")
114
- console = Console()
115
-
116
- if os.path.exists(preinst_script):
117
- self.logger.info('Running %s scripts', preinst_script)
118
- resp = console.command(preinst_script, timeout=300)
119
- if resp["returncode"] != 0:
120
- self.logger.error('Error running %s [%s]: %s', preinst_script, resp["returncode"], ''.join(resp["stderr"]))
121
- return False
122
-
123
- if os.path.exists(postinst_script):
124
- self.logger.info('Running %s scripts', postinst_script)
125
- resp = console.command(postinst_script, timeout=300)
126
- if resp["returncode"] != 0:
127
- self.logger.error('Error running %s [%s]: %s', postinst_script, resp["returncode"], ''.join(resp["stderr"]))
128
- return False
129
-
136
+ script_path = os.path.join(config.MODULES_SCRIPTS_DST, module_name, script_name)
137
+ if not os.path.exists(script_path):
138
+ return True
139
+
140
+ self.logger.info('Running %s', script_path)
141
+ resp = Console().command(script_path, timeout=300)
142
+ if resp["killed"] or resp["returncode"] != 0:
143
+ self.logger.error(
144
+ 'Error running %s [%s]: %s',
145
+ script_path,
146
+ resp["returncode"],
147
+ ''.join(resp["stderr"]),
148
+ )
149
+ return False
130
150
  return True
151
+
152
+ def module_run_install_scripts(self, module_name):
153
+ """
154
+ Run module installation scripts (preinst then postinst)
155
+ """
156
+ return (
157
+ self.module_run_install_script(module_name, "preinst.sh")
158
+ and self.module_run_install_script(module_name, "postinst.sh")
159
+ )