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 +51 -50
- pywebexec/static/js/script.js +1 -1
- pywebexec/version.py +2 -2
- {pywebexec-1.6.16.dist-info → pywebexec-1.7.0.dist-info}/METADATA +1 -1
- {pywebexec-1.6.16.dist-info → pywebexec-1.7.0.dist-info}/RECORD +9 -9
- {pywebexec-1.6.16.dist-info → pywebexec-1.7.0.dist-info}/LICENSE +0 -0
- {pywebexec-1.6.16.dist-info → pywebexec-1.7.0.dist-info}/WHEEL +0 -0
- {pywebexec-1.6.16.dist-info → pywebexec-1.7.0.dist-info}/entry_points.txt +0 -0
- {pywebexec-1.6.16.dist-info → pywebexec-1.7.0.dist-info}/top_level.txt +0 -0
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
|
-
|
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
|
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
|
-
|
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':
|
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
|
-
|
435
|
-
|
436
|
-
if
|
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
|
-
|
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(
|
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
|
-
|
452
|
-
|
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
|
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
|
-
|
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
|
-
|
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
|
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':
|
590
|
+
'last_output_line': status.get('last_output_line'),
|
590
591
|
})
|
591
592
|
return commands
|
592
593
|
|
pywebexec/static/js/script.js
CHANGED
@@ -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
@@ -1,6 +1,6 @@
|
|
1
1
|
pywebexec/__init__.py,sha256=4spIsVaF8RJt8S58AG_wWoORRNkws9Iwqprj27C3ljM,99
|
2
|
-
pywebexec/pywebexec.py,sha256=
|
3
|
-
pywebexec/version.py,sha256=
|
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=
|
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.
|
37
|
-
pywebexec-1.
|
38
|
-
pywebexec-1.
|
39
|
-
pywebexec-1.
|
40
|
-
pywebexec-1.
|
41
|
-
pywebexec-1.
|
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,,
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|