pywebexec 1.1.17__tar.gz → 1.1.19__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.
- {pywebexec-1.1.17/pywebexec.egg-info → pywebexec-1.1.19}/PKG-INFO +1 -1
- {pywebexec-1.1.17 → pywebexec-1.1.19}/pywebexec/pywebexec.py +34 -5
- {pywebexec-1.1.17 → pywebexec-1.1.19}/pywebexec/static/css/style.css +2 -1
- {pywebexec-1.1.17 → pywebexec-1.1.19}/pywebexec/static/js/script.js +1 -0
- {pywebexec-1.1.17 → pywebexec-1.1.19}/pywebexec/version.py +2 -2
- {pywebexec-1.1.17 → pywebexec-1.1.19/pywebexec.egg-info}/PKG-INFO +1 -1
- {pywebexec-1.1.17 → pywebexec-1.1.19}/.github/workflows/python-publish.yml +0 -0
- {pywebexec-1.1.17 → pywebexec-1.1.19}/.gitignore +0 -0
- {pywebexec-1.1.17 → pywebexec-1.1.19}/LICENSE +0 -0
- {pywebexec-1.1.17 → pywebexec-1.1.19}/README.md +0 -0
- {pywebexec-1.1.17 → pywebexec-1.1.19}/pyproject.toml +0 -0
- {pywebexec-1.1.17 → pywebexec-1.1.19}/pywebexec/__init__.py +0 -0
- {pywebexec-1.1.17 → pywebexec-1.1.19}/pywebexec/static/images/aborted.svg +0 -0
- {pywebexec-1.1.17 → pywebexec-1.1.19}/pywebexec/static/images/copy.svg +0 -0
- {pywebexec-1.1.17 → pywebexec-1.1.19}/pywebexec/static/images/copy_ok.svg +0 -0
- {pywebexec-1.1.17 → pywebexec-1.1.19}/pywebexec/static/images/failed.svg +0 -0
- {pywebexec-1.1.17 → pywebexec-1.1.19}/pywebexec/static/images/favicon.svg +0 -0
- {pywebexec-1.1.17 → pywebexec-1.1.19}/pywebexec/static/images/running.gif +0 -0
- {pywebexec-1.1.17 → pywebexec-1.1.19}/pywebexec/static/images/success.svg +0 -0
- {pywebexec-1.1.17 → pywebexec-1.1.19}/pywebexec/templates/__init__.py +0 -0
- {pywebexec-1.1.17 → pywebexec-1.1.19}/pywebexec/templates/index.html +0 -0
- {pywebexec-1.1.17 → pywebexec-1.1.19}/pywebexec.egg-info/SOURCES.txt +0 -0
- {pywebexec-1.1.17 → pywebexec-1.1.19}/pywebexec.egg-info/dependency_links.txt +0 -0
- {pywebexec-1.1.17 → pywebexec-1.1.19}/pywebexec.egg-info/entry_points.txt +0 -0
- {pywebexec-1.1.17 → pywebexec-1.1.19}/pywebexec.egg-info/requires.txt +0 -0
- {pywebexec-1.1.17 → pywebexec-1.1.19}/pywebexec.egg-info/top_level.txt +0 -0
- {pywebexec-1.1.17 → pywebexec-1.1.19}/setup.cfg +0 -0
@@ -15,6 +15,7 @@ from gunicorn.app.base import Application
|
|
15
15
|
import ipaddress
|
16
16
|
from socket import gethostname, gethostbyname_ex
|
17
17
|
import ssl
|
18
|
+
import re
|
18
19
|
if os.environ.get('PYWEBEXEC_LDAP_SERVER'):
|
19
20
|
from ldap3 import Server, Connection, ALL, SIMPLE, SUBTREE, Tls
|
20
21
|
|
@@ -136,10 +137,28 @@ class StandaloneApplication(Application):
|
|
136
137
|
return self.application
|
137
138
|
|
138
139
|
|
140
|
+
def strip_ansi_control_chars(text):
|
141
|
+
"""Remove ANSI and control characters from the text."""
|
142
|
+
# To clean
|
143
|
+
# ansi_escape = re.compile(r'''
|
144
|
+
# (?:\x1B[@-_]| # ANSI ESCape sequences
|
145
|
+
# \x1B\[.*?[ -/]*[@-~]| # ANSI CSI sequences
|
146
|
+
# \x1B\].*?\x07| # ANSI OSC sequences
|
147
|
+
# \x1B=P| # ANSI DCS sequences
|
148
|
+
# \x1B\\| # ANSI ST sequences
|
149
|
+
# \x1B\^| # ANSI PM sequences
|
150
|
+
# \x1B_.*?\x1B\\| # ANSI APC sequences
|
151
|
+
# [\x00-\x1F\x7F]) # Control characters
|
152
|
+
# ''', re.VERBOSE)
|
153
|
+
# ansi_escape = re.compile(r'\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~]|[(]B)|>')
|
154
|
+
ansi_escape = re.compile(br'(?:\x1B[@-Z\\-_]|[\x80-\x9A\x9C-\x9F]|(?:\x1B\[|\x9B)[0-?]*[ -/]*[@-~]|\x1B([(]B|>))')
|
155
|
+
return ansi_escape.sub(b'', text)
|
156
|
+
|
157
|
+
|
139
158
|
def decode_line(line: bytes) -> str:
|
140
159
|
"""try decode line exception on binary"""
|
141
160
|
try:
|
142
|
-
return line.decode()
|
161
|
+
return strip_ansi_control_chars(line).decode().strip(" ")
|
143
162
|
except UnicodeDecodeError:
|
144
163
|
return ""
|
145
164
|
|
@@ -149,17 +168,21 @@ def last_line(fd, maxline=1000):
|
|
149
168
|
line = "\n"
|
150
169
|
fd.seek(0, os.SEEK_END)
|
151
170
|
size = 0
|
152
|
-
|
171
|
+
last_pos = 0
|
172
|
+
while line in ["", "\n", "\r"] and size < maxline:
|
153
173
|
try: # catch if file empty / only empty lines
|
174
|
+
if last_pos:
|
175
|
+
fd.seek(last_pos-2, os.SEEK_SET)
|
154
176
|
while fd.read(1) not in [b"\n", b"\r"]:
|
155
177
|
fd.seek(-2, os.SEEK_CUR)
|
156
178
|
size += 1
|
179
|
+
print(size)
|
157
180
|
except OSError:
|
158
181
|
fd.seek(0)
|
159
182
|
line = decode_line(fd.readline())
|
160
183
|
break
|
184
|
+
last_pos = fd.tell()
|
161
185
|
line = decode_line(fd.readline())
|
162
|
-
fd.seek(-4, os.SEEK_CUR)
|
163
186
|
return line.strip()
|
164
187
|
|
165
188
|
|
@@ -317,6 +340,7 @@ def update_command_status(command_id, status, command=None, params=None, start_t
|
|
317
340
|
if status != 'running':
|
318
341
|
output_file_path = get_output_file_path(command_id)
|
319
342
|
if os.path.exists(output_file_path):
|
343
|
+
print(output_file_path)
|
320
344
|
status_data['last_output_line'] = get_last_non_empty_line_of_file(output_file_path)
|
321
345
|
with open(status_file_path, 'w') as f:
|
322
346
|
json.dump(status_data, f)
|
@@ -474,7 +498,7 @@ def stop_command(command_id):
|
|
474
498
|
end_time = datetime.now().isoformat()
|
475
499
|
try:
|
476
500
|
os.kill(pid, 15) # Send SIGTERM
|
477
|
-
update_command_status(command_id, 'aborted', end_time=end_time, exit_code=-15)
|
501
|
+
#update_command_status(command_id, 'aborted', end_time=end_time, exit_code=-15)
|
478
502
|
return jsonify({'message': 'Command aborted'})
|
479
503
|
except Exception as e:
|
480
504
|
status_data = read_command_status(command_id) or {}
|
@@ -518,6 +542,11 @@ def list_commands():
|
|
518
542
|
except AttributeError:
|
519
543
|
params = " ".join([shlex.quote(p) if " " in p else p for p in status['params']])
|
520
544
|
command = status.get('command', '-') + ' ' + params
|
545
|
+
last_line = status.get('last_output_line')
|
546
|
+
if last_line is None:
|
547
|
+
output_file_path = get_output_file_path(command_id)
|
548
|
+
if os.path.exists(output_file_path):
|
549
|
+
last_line = get_last_non_empty_line_of_file(output_file_path)
|
521
550
|
commands.append({
|
522
551
|
'command_id': command_id,
|
523
552
|
'status': status['status'],
|
@@ -525,7 +554,7 @@ def list_commands():
|
|
525
554
|
'end_time': status.get('end_time', 'N/A'),
|
526
555
|
'command': command,
|
527
556
|
'exit_code': status.get('exit_code', 'N/A'),
|
528
|
-
'last_output_line':
|
557
|
+
'last_output_line': last_line,
|
529
558
|
})
|
530
559
|
# Sort commands by start_time in descending order
|
531
560
|
commands.sort(key=lambda x: x['start_time'], reverse=True)
|
@@ -74,7 +74,8 @@ form {
|
|
74
74
|
.title-icon {
|
75
75
|
width: 30px;
|
76
76
|
height: 30px;
|
77
|
-
background-image: url("/static/images/favicon.svg")
|
77
|
+
background-image: url("/static/images/favicon.svg");
|
78
|
+
vertical-align: bottom;
|
78
79
|
}
|
79
80
|
.status-running {
|
80
81
|
background-image: url("/static/images/running.gif")
|
@@ -17,6 +17,7 @@ document.getElementById('launchForm').addEventListener('submit', async (event) =
|
|
17
17
|
throw new Error('Failed to launch command');
|
18
18
|
}
|
19
19
|
const data = await response.json();
|
20
|
+
await new Promise(r => setTimeout(r, 200));
|
20
21
|
fetchCommands();
|
21
22
|
viewOutput(data.command_id);
|
22
23
|
} catch (error) {
|
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
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|