pywebexec 0.0.3__py3-none-any.whl → 0.0.7__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 +67 -24
- pywebexec/templates/index.html +10 -2
- pywebexec/version.py +2 -2
- {pywebexec-0.0.3.dist-info → pywebexec-0.0.7.dist-info}/METADATA +5 -4
- {pywebexec-0.0.3.dist-info → pywebexec-0.0.7.dist-info}/RECORD +9 -9
- {pywebexec-0.0.3.dist-info → pywebexec-0.0.7.dist-info}/WHEEL +1 -1
- pywebexec-0.0.7.dist-info/entry_points.txt +2 -0
- pywebexec-0.0.3.dist-info/entry_points.txt +0 -2
- {pywebexec-0.0.3.dist-info → pywebexec-0.0.7.dist-info}/LICENSE +0 -0
- {pywebexec-0.0.3.dist-info → pywebexec-0.0.7.dist-info}/top_level.txt +0 -0
pywebexec/pywebexec.py
CHANGED
|
@@ -16,7 +16,7 @@ app = Flask(__name__)
|
|
|
16
16
|
auth = HTTPBasicAuth()
|
|
17
17
|
|
|
18
18
|
# Directory to store the script status and output
|
|
19
|
-
SCRIPT_STATUS_DIR = '
|
|
19
|
+
SCRIPT_STATUS_DIR = '.web_status'
|
|
20
20
|
|
|
21
21
|
if not os.path.exists(SCRIPT_STATUS_DIR):
|
|
22
22
|
os.makedirs(SCRIPT_STATUS_DIR)
|
|
@@ -48,11 +48,46 @@ def start_gunicorn():
|
|
|
48
48
|
options = {
|
|
49
49
|
'bind': '%s:%s' % (args.listen, args.port),
|
|
50
50
|
'workers': 4,
|
|
51
|
+
'timeout': 600,
|
|
51
52
|
'certfile': args.cert,
|
|
52
53
|
'keyfile': args.key,
|
|
53
54
|
}
|
|
54
55
|
StandaloneApplication(app, options=options).run()
|
|
55
56
|
|
|
57
|
+
def daemon_d(action, pidfilepath, hostname=None, args=None):
|
|
58
|
+
"""start/stop daemon"""
|
|
59
|
+
import signal
|
|
60
|
+
import daemon, daemon.pidfile
|
|
61
|
+
|
|
62
|
+
pidfile = daemon.pidfile.TimeoutPIDLockFile(pidfilepath+".pid", acquire_timeout=30)
|
|
63
|
+
if action == "stop":
|
|
64
|
+
if pidfile.is_locked():
|
|
65
|
+
pid = pidfile.read_pid()
|
|
66
|
+
print(f"Stopping server pid {pid}")
|
|
67
|
+
try:
|
|
68
|
+
os.kill(pid, signal.SIGINT)
|
|
69
|
+
except:
|
|
70
|
+
return False
|
|
71
|
+
return True
|
|
72
|
+
elif action == "status":
|
|
73
|
+
status = pidfile.is_locked()
|
|
74
|
+
if status:
|
|
75
|
+
print(f"pywebexec running pid {pidfile.read_pid()}")
|
|
76
|
+
return True
|
|
77
|
+
print("pywebexec not running")
|
|
78
|
+
return False
|
|
79
|
+
elif action == "start":
|
|
80
|
+
print(f"Starting server")
|
|
81
|
+
log = open(pidfilepath + ".log", "ab+")
|
|
82
|
+
daemon_context = daemon.DaemonContext(
|
|
83
|
+
stderr=log,
|
|
84
|
+
pidfile=pidfile,
|
|
85
|
+
umask=0o077,
|
|
86
|
+
working_directory=os.getcwd(),
|
|
87
|
+
)
|
|
88
|
+
with daemon_context:
|
|
89
|
+
start_gunicorn()
|
|
90
|
+
|
|
56
91
|
def parseargs():
|
|
57
92
|
global app, args
|
|
58
93
|
parser = argparse.ArgumentParser(description='Run the script execution server.')
|
|
@@ -76,6 +111,7 @@ def parseargs():
|
|
|
76
111
|
)
|
|
77
112
|
parser.add_argument("-c", "--cert", type=str, help="Path to https certificate")
|
|
78
113
|
parser.add_argument("-k", "--key", type=str, help="Path to https certificate key")
|
|
114
|
+
parser.add_argument("action", nargs="?", help="daemon action start/stop/restart/status", choices=["start","stop","restart","status"])
|
|
79
115
|
|
|
80
116
|
args = parser.parse_args()
|
|
81
117
|
|
|
@@ -99,7 +135,7 @@ def get_status_file_path(script_id):
|
|
|
99
135
|
def get_output_file_path(script_id):
|
|
100
136
|
return os.path.join(SCRIPT_STATUS_DIR, f'{script_id}_output.txt')
|
|
101
137
|
|
|
102
|
-
def update_script_status(script_id, status, script_name=None, params=None, start_time=None, end_time=None, exit_code=None):
|
|
138
|
+
def update_script_status(script_id, status, script_name=None, params=None, start_time=None, end_time=None, exit_code=None, pid=None):
|
|
103
139
|
status_file_path = get_status_file_path(script_id)
|
|
104
140
|
status_data = read_script_status(script_id) or {}
|
|
105
141
|
status_data['status'] = status
|
|
@@ -113,6 +149,8 @@ def update_script_status(script_id, status, script_name=None, params=None, start
|
|
|
113
149
|
status_data['end_time'] = end_time
|
|
114
150
|
if exit_code is not None:
|
|
115
151
|
status_data['exit_code'] = exit_code
|
|
152
|
+
if pid is not None:
|
|
153
|
+
status_data['pid'] = pid
|
|
116
154
|
with open(status_file_path, 'w') as f:
|
|
117
155
|
json.dump(status_data, f)
|
|
118
156
|
|
|
@@ -134,6 +172,7 @@ def run_script(script_name, params, script_id):
|
|
|
134
172
|
with open(output_file_path, 'w') as output_file:
|
|
135
173
|
# Run the script with parameters and redirect stdout and stderr to the file
|
|
136
174
|
process = subprocess.Popen([script_name] + params, stdout=output_file, stderr=output_file, text=True)
|
|
175
|
+
update_script_status(script_id, 'running', pid=process.pid)
|
|
137
176
|
processes[script_id] = process
|
|
138
177
|
process.wait()
|
|
139
178
|
processes.pop(script_id, None)
|
|
@@ -193,25 +232,26 @@ def run_script_endpoint():
|
|
|
193
232
|
@app.route('/stop_script/<script_id>', methods=['POST'])
|
|
194
233
|
@auth_required
|
|
195
234
|
def stop_script(script_id):
|
|
196
|
-
|
|
235
|
+
status = read_script_status(script_id)
|
|
236
|
+
if not status or 'pid' not in status:
|
|
237
|
+
return jsonify({'error': 'Invalid script_id or script not running'}), 400
|
|
238
|
+
|
|
239
|
+
pid = status['pid']
|
|
197
240
|
end_time = datetime.now().isoformat()
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
return jsonify({'error': 'Failed to terminate script'}), 500
|
|
213
|
-
update_script_status(script_id, 'failed', end_time=end_time, exit_code=1)
|
|
214
|
-
return jsonify({'error': 'Invalid script_id or script not running'}), 400
|
|
241
|
+
try:
|
|
242
|
+
os.kill(pid, 15) # Send SIGTERM
|
|
243
|
+
update_script_status(script_id, 'aborted', end_time=end_time, exit_code=-15)
|
|
244
|
+
return jsonify({'message': 'Script aborted'})
|
|
245
|
+
except Exception as e:
|
|
246
|
+
status_data = read_script_status(script_id) or {}
|
|
247
|
+
status_data['status'] = 'failed'
|
|
248
|
+
status_data['end_time'] = end_time
|
|
249
|
+
status_data['exit_code'] = 1
|
|
250
|
+
with open(get_status_file_path(script_id), 'w') as f:
|
|
251
|
+
json.dump(status_data, f)
|
|
252
|
+
with open(get_output_file_path(script_id), 'a') as output_file:
|
|
253
|
+
output_file.write(str(e))
|
|
254
|
+
return jsonify({'error': 'Failed to terminate script'}), 500
|
|
215
255
|
|
|
216
256
|
@app.route('/script_status/<script_id>', methods=['GET'])
|
|
217
257
|
@auth_required
|
|
@@ -276,9 +316,12 @@ def list_executables():
|
|
|
276
316
|
def verify_password(username, password):
|
|
277
317
|
return username == app.config['USER'] and password == app.config['PASSWORD']
|
|
278
318
|
|
|
279
|
-
|
|
280
|
-
|
|
319
|
+
def main():
|
|
320
|
+
if args.action:
|
|
321
|
+
daemon_d(args.action, pidfilepath=SCRIPT_STATUS_DIR+'/pywebexec')
|
|
322
|
+
else:
|
|
323
|
+
start_gunicorn()
|
|
281
324
|
|
|
282
325
|
if __name__ == '__main__':
|
|
283
|
-
|
|
284
|
-
#app.run(host='0.0.0.0', port=5000)
|
|
326
|
+
main()
|
|
327
|
+
# app.run(host='0.0.0.0', port=5000)
|
pywebexec/templates/index.html
CHANGED
|
@@ -79,7 +79,12 @@
|
|
|
79
79
|
.copy_clip_ok, .copy_clip_ok:hover {
|
|
80
80
|
background-image: url("/static/images/copy_ok.svg");
|
|
81
81
|
}
|
|
82
|
-
|
|
82
|
+
input {
|
|
83
|
+
width: 50%
|
|
84
|
+
}
|
|
85
|
+
.currentscript {
|
|
86
|
+
background-color: #eef;
|
|
87
|
+
}
|
|
83
88
|
</style>
|
|
84
89
|
</head>
|
|
85
90
|
<body>
|
|
@@ -126,6 +131,7 @@
|
|
|
126
131
|
});
|
|
127
132
|
const data = await response.json();
|
|
128
133
|
fetchScripts();
|
|
134
|
+
viewOutput(data.script_id)
|
|
129
135
|
});
|
|
130
136
|
|
|
131
137
|
async function fetchScripts() {
|
|
@@ -136,6 +142,7 @@
|
|
|
136
142
|
scriptsTbody.innerHTML = '';
|
|
137
143
|
scripts.forEach(script => {
|
|
138
144
|
const scriptRow = document.createElement('tr');
|
|
145
|
+
scriptRow.className = script.script_id === currentScriptId ? 'currentscript' : '';
|
|
139
146
|
scriptRow.innerHTML = `
|
|
140
147
|
<td class="monospace">
|
|
141
148
|
<span class="copy_clip" onclick="copyToClipboard('${script.script_id}', this)">${script.script_id.slice(0, 8)}</span>
|
|
@@ -194,6 +201,7 @@
|
|
|
194
201
|
} else {
|
|
195
202
|
fetchOutput(script_id);
|
|
196
203
|
}
|
|
204
|
+
fetchScripts(); // Refresh the script list to highlight the current script
|
|
197
205
|
}
|
|
198
206
|
|
|
199
207
|
async function relaunchScript(script_id) {
|
|
@@ -214,8 +222,8 @@
|
|
|
214
222
|
})
|
|
215
223
|
});
|
|
216
224
|
const relaunchData = await relaunchResponse.json();
|
|
217
|
-
alert(relaunchData.message);
|
|
218
225
|
fetchScripts();
|
|
226
|
+
viewOutput(relaunchData.script_id)
|
|
219
227
|
}
|
|
220
228
|
|
|
221
229
|
async function stopScript(script_id) {
|
pywebexec/version.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.2
|
|
2
2
|
Name: pywebexec
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.7
|
|
4
4
|
Summary: Simple Python HTTP Exec Server
|
|
5
5
|
Home-page: https://github.com/joknarf/pywebexec
|
|
6
6
|
Author: Franck Jouvanceau
|
|
@@ -78,12 +78,13 @@ $ pip install pywebexec
|
|
|
78
78
|
$ pywebexec
|
|
79
79
|
```
|
|
80
80
|
|
|
81
|
-
* Launch commands with params/view live output/Status using browser
|
|
81
|
+
* Launch commands with params/view live output/Status using browser
|
|
82
|
+

|
|
82
83
|
|
|
83
84
|
## features
|
|
84
85
|
|
|
85
86
|
* Serve executables in current directory
|
|
86
|
-
* Launch commands with params from web browser
|
|
87
|
+
* Launch commands with params from web browser or API call
|
|
87
88
|
* Follow live output
|
|
88
89
|
* Stop command
|
|
89
90
|
* Relaunch command
|
|
@@ -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=d6ZndGgs3dYbmq6L12tDsC73zPdkjHgCwpL_Rqv2ei0,11664
|
|
3
|
+
pywebexec/version.py,sha256=vZ9YJhtz2uaZyfeULJmmzg7nlru6jGE1i6ermqZWYAQ,411
|
|
4
4
|
pywebexec/static/images/aborted.svg,sha256=_mP43hU5QdRLFZIknBgjx-dIXrHgQG23-QV27ApXK2A,381
|
|
5
5
|
pywebexec/static/images/copy.svg,sha256=d9OwtGh5GzzZHzYcDrLfNxZYLth1Q64x7bRyYxu4Px0,622
|
|
6
6
|
pywebexec/static/images/copy_ok.svg,sha256=mEqUVUhSq8xaJK2msQkxRawnz_KwlCZ-tok8QS6hJ3g,451
|
|
@@ -8,10 +8,10 @@ pywebexec/static/images/failed.svg,sha256=ADZ7IKrUyOXtqpivnz3VcH0-Wru-I5MOi3OJAk
|
|
|
8
8
|
pywebexec/static/images/running.svg,sha256=vBpiG6ClNUNCArkwsyqK7O-qhIKJX1NI7MSjclNSp_8,1537
|
|
9
9
|
pywebexec/static/images/success.svg,sha256=PJDcCSTevJh7rkfSFLtc7P0pbeh8PVQBS8DaOLQemmc,489
|
|
10
10
|
pywebexec/templates/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
11
|
-
pywebexec/templates/index.html,sha256=
|
|
12
|
-
pywebexec-0.0.
|
|
13
|
-
pywebexec-0.0.
|
|
14
|
-
pywebexec-0.0.
|
|
15
|
-
pywebexec-0.0.
|
|
16
|
-
pywebexec-0.0.
|
|
17
|
-
pywebexec-0.0.
|
|
11
|
+
pywebexec/templates/index.html,sha256=2peDmBnxZQSw6OjDCjNqRCx1_grDlI_Xr1NMgAGv2OI,10163
|
|
12
|
+
pywebexec-0.0.7.dist-info/LICENSE,sha256=gRJf0JPT_wsZJsUGlWPTS8Vypfl9vQ1qjp6sNbKykuA,1064
|
|
13
|
+
pywebexec-0.0.7.dist-info/METADATA,sha256=G0m6NWlqabvhmf82xBS10deEaWA3Drn2o-a0i0Z1z8M,4698
|
|
14
|
+
pywebexec-0.0.7.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
|
|
15
|
+
pywebexec-0.0.7.dist-info/entry_points.txt,sha256=l52GBkPCXRkmlHfEyoVauyfBdg8o-CAtC8qQpOIjJK0,55
|
|
16
|
+
pywebexec-0.0.7.dist-info/top_level.txt,sha256=vHoHyzngrfGdm_nM7Xn_5iLmaCrf10XO1EhldgNLEQ8,10
|
|
17
|
+
pywebexec-0.0.7.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|