pywebexec 1.6.16__py3-none-any.whl → 1.7.0__py3-none-any.whl

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.
pywebexec/pywebexec.py CHANGED
@@ -58,7 +58,7 @@ CONFDIR += "/.pywebexec"
58
58
  term_command_id = str(uuid.uuid4())
59
59
 
60
60
  # In-memory cache for command statuses
61
- command_status_cache = {}
61
+ status_cache = {}
62
62
 
63
63
  def generate_random_password(length=12):
64
64
  characters = string.ascii_letters + string.digits + string.punctuation
@@ -162,7 +162,6 @@ def generate_selfsigned_cert(hostname, ip_addresses=None, key=None):
162
162
  return cert_pem, key_pem
163
163
 
164
164
 
165
-
166
165
  class PyWebExec(Application):
167
166
 
168
167
  def __init__(self, app, options=None):
@@ -180,18 +179,6 @@ class PyWebExec(Application):
180
179
 
181
180
  def load(self):
182
181
  return self.application
183
-
184
- def get_visible_output(line):
185
- try:
186
- screen = pyte.Screen(len(line)+1, 2)
187
- stream = pyte.Stream(screen)
188
- stream.feed(line)
189
- visible_line = screen.display[1].strip(" ")
190
- if visible_line:
191
- return visible_line
192
- return screen.display[0].strip(" ")
193
- except:
194
- return ""
195
182
  #38;2;66;59;165m
196
183
  ANSI_ESCAPE = re.compile(br'(?:\x1B[@-Z\\-_]|\x1B([(]B|>)|(?:\x1B\[|\x9B)[0-?]*[ -/]*[@-~]|\x1B\[([0-9]{1,2};){0,4}[0-9]{1,3}[m|K]|\x1B\[[0-9;]*[mGKHF]|[\x00-\x1F\x7F])')
197
184
 
@@ -208,22 +195,34 @@ def decode_line(line: bytes) -> str:
208
195
  return ""
209
196
 
210
197
 
211
- def get_last_line(file_path, maxsize=1024):
198
+ def get_visible_output(line, cols, rows):
199
+ """pyte vt100 render to get last line"""
200
+ try:
201
+ screen = pyte.Screen(cols, rows)
202
+ stream = pyte.ByteStream(screen)
203
+ stream.feed(line)
204
+ visible_line = ""
205
+ row = rows - 1
206
+ while row > 0:
207
+ visible_line = screen.display[row].strip(" ")
208
+ if visible_line:
209
+ return visible_line
210
+ row -= 1
211
+ except:
212
+ return ""
213
+ return ""
214
+
215
+
216
+ def get_last_line(file_path, cols=None, rows=None, maxsize=2048):
212
217
  """Retrieve last non empty line after vt100 interpretation"""
218
+ cols = cols or 125
219
+ rows = rows or 24
213
220
  with open(file_path, 'rb') as fd:
214
221
  try:
215
222
  fd.seek(-maxsize, os.SEEK_END)
216
223
  except OSError:
217
224
  fd.seek(0)
218
- lines = fd.read().split(b"\n")
219
- if len(lines) == 1:
220
- return ""
221
- line = ""
222
- while True:
223
- line = decode_line(lines.pop())
224
- if line or not lines:
225
- break
226
- return line
225
+ return get_visible_output(fd.read(), cols, rows)
227
226
 
228
227
 
229
228
  def start_gunicorn(daemonized=False, baselog=None):
@@ -242,7 +241,8 @@ def start_gunicorn(daemonized=False, baselog=None):
242
241
  accesslog = None #"-"
243
242
  options = {
244
243
  'bind': '%s:%s' % (args.listen, args.port),
245
- 'workers': 4,
244
+ 'workers': 1,
245
+ 'threads': 4,
246
246
  'timeout': 600,
247
247
  'certfile': args.cert,
248
248
  'keyfile': args.key,
@@ -431,39 +431,33 @@ def get_output_file_path(command_id):
431
431
 
432
432
  def update_command_status(command_id, updates):
433
433
  status_file_path = get_status_file_path(command_id)
434
- status_data = read_command_status(command_id) or {}
435
- status_data.update(updates)
436
- if status_data['status'] != 'running':
434
+ status = read_command_status(command_id) or {}
435
+ status.update(updates)
436
+ if status.get('status') != 'running':
437
437
  output_file_path = get_output_file_path(command_id)
438
438
  if os.path.exists(output_file_path):
439
- status_data['last_output_line'] = get_last_line(output_file_path)
439
+ status['last_output_line'] = get_last_line(output_file_path, status.get('cols'), status.get('rows'))
440
+ status_cache[command_id] = status
440
441
  with open(status_file_path, 'w') as f:
441
- json.dump(status_data, f)
442
+ json.dump(status, f)
442
443
 
443
- # Update cache if status is not "running"
444
- if status_data['status'] != 'running':
445
- command_status_cache[command_id] = status_data
446
- elif command_id in command_status_cache:
447
- del command_status_cache[command_id]
448
-
449
444
  def read_command_status(command_id):
450
445
  # Return cached status if available
451
- if command_id in command_status_cache:
452
- return command_status_cache[command_id]
453
-
446
+ status_data = {}
447
+ if command_id in status_cache:
448
+ status_data = status_cache[command_id]
449
+ status = status_data.get('status')
450
+ if status and status != "running":
451
+ return status_data
454
452
  status_file_path = get_status_file_path(command_id)
455
453
  if not os.path.exists(status_file_path):
456
454
  return None
457
455
  with open(status_file_path, 'r') as f:
458
456
  try:
459
- status_data = json.load(f)
457
+ status_data.update(json.load(f))
460
458
  except json.JSONDecodeError:
461
459
  return None
462
-
463
- # Cache the status if it is not "running"
464
- if status_data['status'] != 'running':
465
- command_status_cache[command_id] = status_data
466
-
460
+ status_cache[command_id] = status_data
467
461
  return status_data
468
462
 
469
463
  def sigwinch_passthrough(sig, data):
@@ -567,6 +561,7 @@ def command_str(command, params):
567
561
 
568
562
 
569
563
  def read_commands():
564
+ global status_cache
570
565
  commands = []
571
566
  for filename in os.listdir(COMMAND_STATUS_DIR):
572
567
  if filename.endswith('.json'):
@@ -574,19 +569,25 @@ def read_commands():
574
569
  status = read_command_status(command_id)
575
570
  if status:
576
571
  command = command_str(status.get('command', '-'), status.get('params', []))
577
- last_line = status.get('last_output_line')
578
- if last_line is None:
572
+ if status.get('status') == 'running' and status.get('last_update',0)<datetime.now().timestamp()-5:
579
573
  output_file_path = get_output_file_path(command_id)
580
574
  if os.path.exists(output_file_path):
581
- last_line = get_last_line(output_file_path)
575
+ size = os.path.getsize(output_file_path)
576
+ if size != status.get('size'):
577
+ status.update({
578
+ 'size': size,
579
+ 'last_update': datetime.now().timestamp(),
580
+ 'last_output_line': get_last_line(output_file_path, status.get('cols'), status.get('rows')),
581
+ })
582
+ status_cache[command_id] = status
582
583
  commands.append({
583
584
  'command_id': command_id,
584
- 'status': status['status'],
585
+ 'status': status.get('status'),
585
586
  'start_time': status.get('start_time', 'N/A'),
586
587
  'end_time': status.get('end_time', 'N/A'),
587
588
  'command': command,
588
589
  'exit_code': status.get('exit_code', 'N/A'),
589
- 'last_output_line': last_line,
590
+ 'last_output_line': status.get('last_output_line'),
590
591
  })
591
592
  return commands
592
593
 
@@ -120,7 +120,7 @@ document.getElementById('launchForm').addEventListener('submit', async (event) =
120
120
  throw new Error('Failed to launch command');
121
121
  }
122
122
  const data = await response.json();
123
- await new Promise(r => setTimeout(r, 300));
123
+ //await new Promise(r => setTimeout(r, 300));// not ok
124
124
  fetchCommands();
125
125
  viewOutput(data.command_id);
126
126
  commandInput.focus()
pywebexec/version.py CHANGED
@@ -12,5 +12,5 @@ __version__: str
12
12
  __version_tuple__: VERSION_TUPLE
13
13
  version_tuple: VERSION_TUPLE
14
14
 
15
- __version__ = version = '1.6.16'
16
- __version_tuple__ = version_tuple = (1, 6, 16)
15
+ __version__ = version = '1.7.0'
16
+ __version_tuple__ = version_tuple = (1, 7, 0)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: pywebexec
3
- Version: 1.6.16
3
+ Version: 1.7.0
4
4
  Summary: Simple Python HTTP Exec Server
5
5
  Home-page: https://github.com/joknarf/pywebexec
6
6
  Author: Franck Jouvanceau
@@ -1,6 +1,6 @@
1
1
  pywebexec/__init__.py,sha256=4spIsVaF8RJt8S58AG_wWoORRNkws9Iwqprj27C3ljM,99
2
- pywebexec/pywebexec.py,sha256=bfE0QBiErt3k5GCsobOLZOP5OT6JJs-39GvLkzWqnB0,32650
3
- pywebexec/version.py,sha256=Krs1A6GFKeSDM3HlmJuBHtdakqIKMggtBrGf0aTD85E,413
2
+ pywebexec/pywebexec.py,sha256=J5cPqvF8OfdKAmuvNiRgVIGsiromBBdK1Nna80tNPro,33006
3
+ pywebexec/version.py,sha256=2fEqxujmrV2dsREie2BmOYFLu66FowyHtZT2AoLuIzU,411
4
4
  pywebexec/static/css/Consolas NF.ttf,sha256=DJEOzF0eqZ-kxu3Gs_VE8X0NJqiobBzmxWDGpdgGRxI,1313900
5
5
  pywebexec/static/css/style.css,sha256=MJHUBpjWL4sLxM7a7DxypmPKaFJQbmA_ESNXsbLviNI,8201
6
6
  pywebexec/static/css/xterm.css,sha256=uo5phWaUiJgcz0DAzv46uoByLLbJLeetYosL1xf68rY,5559
@@ -20,7 +20,7 @@ pywebexec/static/images/running.svg,sha256=fBCYwYb2O9K4N3waC2nURP25NRwZlqR4PbDZy
20
20
  pywebexec/static/images/success.svg,sha256=NVwezvVMplt46ElW798vqGfrL21Mw_DWHUp_qiD_FU8,489
21
21
  pywebexec/static/js/commands.js,sha256=h2fkd9qpypLBxvhEEbay23nwuqUwcKJA0vHugcyL8pU,7961
22
22
  pywebexec/static/js/popup.js,sha256=I5TLYUm5s2p12a0VfFONOxxzhXLBmnpVzM-B5W-GPis,8294
23
- pywebexec/static/js/script.js,sha256=8CcIn-O4I1ojbiAYOeQl3yTnykn0Lj3xdWdx6m6HyNs,17085
23
+ pywebexec/static/js/script.js,sha256=EPofQO0ec2bwIr2YdzzNY3dcXipW2_Mi2j34DIYvJeE,17096
24
24
  pywebexec/static/js/xterm/LICENSE,sha256=EU1P4eXTull-_T9I80VuwnJXubB-zLzUl3xpEYj2T1M,1083
25
25
  pywebexec/static/js/xterm/addon-canvas.js,sha256=ez6QTVvsmLVNJmdJlM-ZQ5bErwlxAQ_9DUmDIptl2TM,94607
26
26
  pywebexec/static/js/xterm/addon-canvas.js.map,sha256=ECBA4B-BqUpdFeRzlsEWLSQnudnhLP-yPQJ8_hKquMo,379537
@@ -33,9 +33,9 @@ pywebexec/static/js/xterm/xterm.js.map,sha256=Y7O2Pb-fIS7Z8AC1D5s04_aiW_Jf1f4mCf
33
33
  pywebexec/templates/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
34
34
  pywebexec/templates/index.html,sha256=5h8kLyzAhbvUDU9sEwrGIvD6FxAMcDZLXlN-ldlO8KU,2880
35
35
  pywebexec/templates/popup.html,sha256=3ZqQcE9mYs-RXv0Lfb24zntOlvR137ZYI9mtCZNVAo0,1407
36
- pywebexec-1.6.16.dist-info/LICENSE,sha256=gRJf0JPT_wsZJsUGlWPTS8Vypfl9vQ1qjp6sNbKykuA,1064
37
- pywebexec-1.6.16.dist-info/METADATA,sha256=sU0xp2d2_UIe1pvIdJKvnZ3zK7ThZ7jueOKNVssIu3o,8001
38
- pywebexec-1.6.16.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
39
- pywebexec-1.6.16.dist-info/entry_points.txt,sha256=l52GBkPCXRkmlHfEyoVauyfBdg8o-CAtC8qQpOIjJK0,55
40
- pywebexec-1.6.16.dist-info/top_level.txt,sha256=vHoHyzngrfGdm_nM7Xn_5iLmaCrf10XO1EhldgNLEQ8,10
41
- pywebexec-1.6.16.dist-info/RECORD,,
36
+ pywebexec-1.7.0.dist-info/LICENSE,sha256=gRJf0JPT_wsZJsUGlWPTS8Vypfl9vQ1qjp6sNbKykuA,1064
37
+ pywebexec-1.7.0.dist-info/METADATA,sha256=qEu0SeeVHBVgAhPPeM2Fe8Rhq6RhGNaPudJyJS1t-3k,8000
38
+ pywebexec-1.7.0.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
39
+ pywebexec-1.7.0.dist-info/entry_points.txt,sha256=l52GBkPCXRkmlHfEyoVauyfBdg8o-CAtC8qQpOIjJK0,55
40
+ pywebexec-1.7.0.dist-info/top_level.txt,sha256=vHoHyzngrfGdm_nM7Xn_5iLmaCrf10XO1EhldgNLEQ8,10
41
+ pywebexec-1.7.0.dist-info/RECORD,,