pywebexec 0.0.7__tar.gz → 0.1.0__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-0.0.7/pywebexec.egg-info → pywebexec-0.1.0}/PKG-INFO +5 -3
- {pywebexec-0.0.7 → pywebexec-0.1.0}/README.md +1 -1
- {pywebexec-0.0.7 → pywebexec-0.1.0}/pyproject.toml +3 -1
- {pywebexec-0.0.7 → pywebexec-0.1.0}/pywebexec/pywebexec.py +41 -8
- {pywebexec-0.0.7 → pywebexec-0.1.0}/pywebexec/templates/index.html +63 -6
- {pywebexec-0.0.7 → pywebexec-0.1.0}/pywebexec/version.py +2 -2
- {pywebexec-0.0.7 → pywebexec-0.1.0/pywebexec.egg-info}/PKG-INFO +5 -3
- pywebexec-0.1.0/pywebexec.egg-info/requires.txt +5 -0
- pywebexec-0.0.7/pywebexec.egg-info/requires.txt +0 -3
- {pywebexec-0.0.7 → pywebexec-0.1.0}/.github/workflows/python-publish.yml +0 -0
- {pywebexec-0.0.7 → pywebexec-0.1.0}/.gitignore +0 -0
- {pywebexec-0.0.7 → pywebexec-0.1.0}/LICENSE +0 -0
- {pywebexec-0.0.7 → pywebexec-0.1.0}/pywebexec/__init__.py +0 -0
- {pywebexec-0.0.7 → pywebexec-0.1.0}/pywebexec/static/images/aborted.svg +0 -0
- {pywebexec-0.0.7 → pywebexec-0.1.0}/pywebexec/static/images/copy.svg +0 -0
- {pywebexec-0.0.7 → pywebexec-0.1.0}/pywebexec/static/images/copy_ok.svg +0 -0
- {pywebexec-0.0.7 → pywebexec-0.1.0}/pywebexec/static/images/failed.svg +0 -0
- {pywebexec-0.0.7 → pywebexec-0.1.0}/pywebexec/static/images/running.svg +0 -0
- {pywebexec-0.0.7 → pywebexec-0.1.0}/pywebexec/static/images/success.svg +0 -0
- {pywebexec-0.0.7 → pywebexec-0.1.0}/pywebexec/templates/__init__.py +0 -0
- {pywebexec-0.0.7 → pywebexec-0.1.0}/pywebexec.egg-info/SOURCES.txt +0 -0
- {pywebexec-0.0.7 → pywebexec-0.1.0}/pywebexec.egg-info/dependency_links.txt +0 -0
- {pywebexec-0.0.7 → pywebexec-0.1.0}/pywebexec.egg-info/entry_points.txt +0 -0
- {pywebexec-0.0.7 → pywebexec-0.1.0}/pywebexec.egg-info/top_level.txt +0 -0
- {pywebexec-0.0.7 → pywebexec-0.1.0}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.2
|
|
2
2
|
Name: pywebexec
|
|
3
|
-
Version: 0.0
|
|
3
|
+
Version: 0.1.0
|
|
4
4
|
Summary: Simple Python HTTP Exec Server
|
|
5
5
|
Home-page: https://github.com/joknarf/pywebexec
|
|
6
6
|
Author: Franck Jouvanceau
|
|
@@ -53,9 +53,11 @@ Classifier: Topic :: System :: Systems Administration
|
|
|
53
53
|
Requires-Python: >=3.6
|
|
54
54
|
Description-Content-Type: text/markdown
|
|
55
55
|
License-File: LICENSE
|
|
56
|
+
Requires-Dist: python-daemon>=2.3.2
|
|
56
57
|
Requires-Dist: cryptography>=40.0.2
|
|
57
|
-
Requires-Dist: Flask>=
|
|
58
|
+
Requires-Dist: Flask>=2.0.3
|
|
58
59
|
Requires-Dist: Flask-HTTPAuth>=4.8.0
|
|
60
|
+
Requires-Dist: gunicorn>=21.2.0
|
|
59
61
|
|
|
60
62
|
[](https://pypi.org/project/pywebexec/)
|
|
61
63
|

|
|
@@ -127,5 +129,5 @@ $ pywebexec start
|
|
|
127
129
|
$ pywebexec status
|
|
128
130
|
$ pywebexec stop
|
|
129
131
|
```
|
|
130
|
-
* log of server are stored in
|
|
132
|
+
* log of server are stored in directory `[.config/].pywebexec/pywebexec_<listen>:<port>.log`
|
|
131
133
|
|
|
@@ -9,9 +9,11 @@ maintainers = [
|
|
|
9
9
|
|
|
10
10
|
description = "Simple Python HTTP Exec Server"
|
|
11
11
|
dependencies = [
|
|
12
|
+
"python-daemon>=2.3.2",
|
|
12
13
|
"cryptography>=40.0.2",
|
|
13
|
-
"Flask>=
|
|
14
|
+
"Flask>=2.0.3",
|
|
14
15
|
"Flask-HTTPAuth>=4.8.0",
|
|
16
|
+
"gunicorn>=21.2.0",
|
|
15
17
|
]
|
|
16
18
|
dynamic=["version"]
|
|
17
19
|
readme = "README.md"
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import sys
|
|
1
2
|
from flask import Flask, request, jsonify, render_template
|
|
2
3
|
from flask_httpauth import HTTPBasicAuth
|
|
3
4
|
import subprocess
|
|
@@ -10,13 +11,17 @@ import random
|
|
|
10
11
|
import string
|
|
11
12
|
from datetime import datetime
|
|
12
13
|
import shlex
|
|
13
|
-
from gunicorn.app.base import BaseApplication
|
|
14
|
+
from gunicorn.app.base import BaseApplication, Application
|
|
14
15
|
|
|
15
16
|
app = Flask(__name__)
|
|
16
17
|
auth = HTTPBasicAuth()
|
|
17
18
|
|
|
18
19
|
# Directory to store the script status and output
|
|
19
20
|
SCRIPT_STATUS_DIR = '.web_status'
|
|
21
|
+
CONFDIR = os.path.expanduser("~/")
|
|
22
|
+
if os.path.isdir(f"{CONFDIR}/.config"):
|
|
23
|
+
CONFDIR += '/.config'
|
|
24
|
+
CONFDIR += "/.pywebexec"
|
|
20
25
|
|
|
21
26
|
if not os.path.exists(SCRIPT_STATUS_DIR):
|
|
22
27
|
os.makedirs(SCRIPT_STATUS_DIR)
|
|
@@ -25,7 +30,7 @@ def generate_random_password(length=12):
|
|
|
25
30
|
characters = string.ascii_letters + string.digits + string.punctuation
|
|
26
31
|
return ''.join(random.choice(characters) for i in range(length))
|
|
27
32
|
|
|
28
|
-
class StandaloneApplication(
|
|
33
|
+
class StandaloneApplication(Application):
|
|
29
34
|
|
|
30
35
|
def __init__(self, app, options=None):
|
|
31
36
|
self.options = options or {}
|
|
@@ -44,13 +49,25 @@ class StandaloneApplication(BaseApplication):
|
|
|
44
49
|
return self.application
|
|
45
50
|
|
|
46
51
|
|
|
47
|
-
def start_gunicorn():
|
|
52
|
+
def start_gunicorn(daemon=False, baselog=None):
|
|
53
|
+
if daemon:
|
|
54
|
+
errorlog = f"{baselog}.log"
|
|
55
|
+
accesslog = None # f"{baselog}.access.log"
|
|
56
|
+
pidfile = f"{baselog}.pid"
|
|
57
|
+
else:
|
|
58
|
+
errorlog = "-"
|
|
59
|
+
accesslog = "-"
|
|
60
|
+
pidfile = None
|
|
48
61
|
options = {
|
|
49
62
|
'bind': '%s:%s' % (args.listen, args.port),
|
|
50
63
|
'workers': 4,
|
|
51
64
|
'timeout': 600,
|
|
52
65
|
'certfile': args.cert,
|
|
53
66
|
'keyfile': args.key,
|
|
67
|
+
'daemon': daemon,
|
|
68
|
+
'errorlog': errorlog,
|
|
69
|
+
'accesslog': accesslog,
|
|
70
|
+
'pidfile': pidfile,
|
|
54
71
|
}
|
|
55
72
|
StandaloneApplication(app, options=options).run()
|
|
56
73
|
|
|
@@ -86,7 +103,10 @@ def daemon_d(action, pidfilepath, hostname=None, args=None):
|
|
|
86
103
|
working_directory=os.getcwd(),
|
|
87
104
|
)
|
|
88
105
|
with daemon_context:
|
|
89
|
-
|
|
106
|
+
try:
|
|
107
|
+
start_gunicorn()
|
|
108
|
+
except Exception as e:
|
|
109
|
+
print(e)
|
|
90
110
|
|
|
91
111
|
def parseargs():
|
|
92
112
|
global app, args
|
|
@@ -114,6 +134,15 @@ def parseargs():
|
|
|
114
134
|
parser.add_argument("action", nargs="?", help="daemon action start/stop/restart/status", choices=["start","stop","restart","status"])
|
|
115
135
|
|
|
116
136
|
args = parser.parse_args()
|
|
137
|
+
if os.path.isdir(args.dir):
|
|
138
|
+
try:
|
|
139
|
+
os.chdir(args.dir)
|
|
140
|
+
except OSError:
|
|
141
|
+
print(f"Error: cannot chdir {args.dir}", file=sys.stderr)
|
|
142
|
+
sys.exit(1)
|
|
143
|
+
else:
|
|
144
|
+
print(f"Error: {args.dir} not found", file=sys.stderr)
|
|
145
|
+
sys.exit(1)
|
|
117
146
|
|
|
118
147
|
if args.user:
|
|
119
148
|
app.config['USER'] = args.user
|
|
@@ -171,7 +200,7 @@ def run_script(script_name, params, script_id):
|
|
|
171
200
|
output_file_path = get_output_file_path(script_id)
|
|
172
201
|
with open(output_file_path, 'w') as output_file:
|
|
173
202
|
# Run the script with parameters and redirect stdout and stderr to the file
|
|
174
|
-
process = subprocess.Popen([script_name] + params, stdout=output_file, stderr=output_file, text=True)
|
|
203
|
+
process = subprocess.Popen([script_name] + params, stdout=output_file, stderr=output_file, bufsize=0) #text=True)
|
|
175
204
|
update_script_status(script_id, 'running', pid=process.pid)
|
|
176
205
|
processes[script_id] = process
|
|
177
206
|
process.wait()
|
|
@@ -317,10 +346,14 @@ def verify_password(username, password):
|
|
|
317
346
|
return username == app.config['USER'] and password == app.config['PASSWORD']
|
|
318
347
|
|
|
319
348
|
def main():
|
|
349
|
+
basef = f"{CONFDIR}/pywebexec_{args.listen}:{args.port}"
|
|
350
|
+
if not os.path.exists(CONFDIR):
|
|
351
|
+
os.mkdir(CONFDIR, mode=0o700)
|
|
352
|
+
if args.action == "start":
|
|
353
|
+
return start_gunicorn(daemon=True, baselog=basef)
|
|
320
354
|
if args.action:
|
|
321
|
-
daemon_d(args.action, pidfilepath=
|
|
322
|
-
|
|
323
|
-
start_gunicorn()
|
|
355
|
+
return daemon_d(args.action, pidfilepath=basef)
|
|
356
|
+
return start_gunicorn()
|
|
324
357
|
|
|
325
358
|
if __name__ == '__main__':
|
|
326
359
|
main()
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
<title>pywebexec</title>
|
|
6
6
|
<style>
|
|
7
7
|
body { font-family: Arial, sans-serif; }
|
|
8
|
-
.table-container {
|
|
8
|
+
.table-container { height: 380px; overflow-y: auto; position: relative; }
|
|
9
9
|
table { width: 100%; border-collapse: collapse; }
|
|
10
10
|
th, td { padding: 8px; text-align: left; border-bottom: 1px solid #ddd; }
|
|
11
11
|
th { background-color: #f2f2f2; position: sticky; top: 0; z-index: 1; }
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
border: 1px solid #ccc;
|
|
17
17
|
font-family: monospace;
|
|
18
18
|
border-radius: 15px;
|
|
19
|
+
overflow-y: auto;
|
|
19
20
|
}
|
|
20
21
|
.copy-icon { cursor: pointer; }
|
|
21
22
|
.monospace { font-family: monospace; }
|
|
@@ -85,6 +86,20 @@
|
|
|
85
86
|
.currentscript {
|
|
86
87
|
background-color: #eef;
|
|
87
88
|
}
|
|
89
|
+
.resizer {
|
|
90
|
+
width: 100%;
|
|
91
|
+
height: 5px;
|
|
92
|
+
background: #aaa;
|
|
93
|
+
cursor: ns-resize;
|
|
94
|
+
position: absolute;
|
|
95
|
+
bottom: 0;
|
|
96
|
+
left: 0;
|
|
97
|
+
}
|
|
98
|
+
.resizer-container {
|
|
99
|
+
position: relative;
|
|
100
|
+
height: 5px;
|
|
101
|
+
margin-bottom: 10px;
|
|
102
|
+
}
|
|
88
103
|
</style>
|
|
89
104
|
</head>
|
|
90
105
|
<body>
|
|
@@ -96,7 +111,7 @@
|
|
|
96
111
|
<input type="text" id="params" name="params">
|
|
97
112
|
<button type="submit">Launch</button>
|
|
98
113
|
</form>
|
|
99
|
-
<div class="table-container">
|
|
114
|
+
<div class="table-container" id="tableContainer">
|
|
100
115
|
<table>
|
|
101
116
|
<thead>
|
|
102
117
|
<tr>
|
|
@@ -112,6 +127,9 @@
|
|
|
112
127
|
<tbody id="scripts"></tbody>
|
|
113
128
|
</table>
|
|
114
129
|
</div>
|
|
130
|
+
<div class="resizer-container">
|
|
131
|
+
<div class="resizer" id="resizer"></div>
|
|
132
|
+
</div>
|
|
115
133
|
<div id="output" class="output"></div>
|
|
116
134
|
|
|
117
135
|
<script>
|
|
@@ -131,7 +149,7 @@
|
|
|
131
149
|
});
|
|
132
150
|
const data = await response.json();
|
|
133
151
|
fetchScripts();
|
|
134
|
-
viewOutput(data.script_id)
|
|
152
|
+
viewOutput(data.script_id);
|
|
135
153
|
});
|
|
136
154
|
|
|
137
155
|
async function fetchScripts() {
|
|
@@ -184,13 +202,15 @@
|
|
|
184
202
|
clearInterval(outputInterval);
|
|
185
203
|
} else {
|
|
186
204
|
outputDiv.innerHTML = data.output;
|
|
205
|
+
outputDiv.scrollTop = outputDiv.scrollHeight;
|
|
187
206
|
if (data.status != 'running') {
|
|
188
|
-
clearInterval(outputInterval)
|
|
207
|
+
clearInterval(outputInterval);
|
|
189
208
|
}
|
|
190
209
|
}
|
|
191
210
|
}
|
|
192
211
|
|
|
193
212
|
async function viewOutput(script_id) {
|
|
213
|
+
adjustOutputHeight();
|
|
194
214
|
currentScriptId = script_id;
|
|
195
215
|
clearInterval(outputInterval);
|
|
196
216
|
const response = await fetch(`/script_status/${script_id}`);
|
|
@@ -223,7 +243,7 @@
|
|
|
223
243
|
});
|
|
224
244
|
const relaunchData = await relaunchResponse.json();
|
|
225
245
|
fetchScripts();
|
|
226
|
-
viewOutput(relaunchData.script_id)
|
|
246
|
+
viewOutput(relaunchData.script_id);
|
|
227
247
|
}
|
|
228
248
|
|
|
229
249
|
async function stopScript(script_id) {
|
|
@@ -258,13 +278,50 @@
|
|
|
258
278
|
|
|
259
279
|
function copyToClipboard(text, element) {
|
|
260
280
|
navigator.clipboard.writeText(text).then(() => {
|
|
261
|
-
element.classList.add('copy_clip_ok')
|
|
281
|
+
element.classList.add('copy_clip_ok');
|
|
262
282
|
setTimeout(() => {
|
|
263
283
|
element.classList.remove('copy_clip_ok');
|
|
264
284
|
}, 2000);
|
|
265
285
|
});
|
|
266
286
|
}
|
|
267
287
|
|
|
288
|
+
function adjustOutputHeight() {
|
|
289
|
+
const outputDiv = document.getElementById('output');
|
|
290
|
+
const windowHeight = window.innerHeight;
|
|
291
|
+
const outputTop = outputDiv.getBoundingClientRect().top;
|
|
292
|
+
const maxHeight = windowHeight - outputTop - 30; // 20px for padding/margin
|
|
293
|
+
outputDiv.style.maxHeight = `${maxHeight}px`;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function initResizer() {
|
|
297
|
+
const resizer = document.getElementById('resizer');
|
|
298
|
+
const tableContainer = document.getElementById('tableContainer');
|
|
299
|
+
let startY, startHeight;
|
|
300
|
+
|
|
301
|
+
resizer.addEventListener('mousedown', (e) => {
|
|
302
|
+
startY = e.clientY;
|
|
303
|
+
startHeight = parseInt(document.defaultView.getComputedStyle(tableContainer).height, 10);
|
|
304
|
+
document.documentElement.addEventListener('mousemove', doDrag, false);
|
|
305
|
+
document.documentElement.addEventListener('mouseup', stopDrag, false);
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
function doDrag(e) {
|
|
309
|
+
tableContainer.style.height = `${startHeight + e.clientY - startY}px`;
|
|
310
|
+
adjustOutputHeight();
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function stopDrag() {
|
|
314
|
+
document.documentElement.removeEventListener('mousemove', doDrag, false);
|
|
315
|
+
document.documentElement.removeEventListener('mouseup', stopDrag, false);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
window.addEventListener('resize', adjustOutputHeight);
|
|
320
|
+
window.addEventListener('load', () => {
|
|
321
|
+
adjustOutputHeight();
|
|
322
|
+
initResizer();
|
|
323
|
+
});
|
|
324
|
+
|
|
268
325
|
fetchScripts();
|
|
269
326
|
fetchExecutables();
|
|
270
327
|
setInterval(fetchScripts, 5000);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.2
|
|
2
2
|
Name: pywebexec
|
|
3
|
-
Version: 0.0
|
|
3
|
+
Version: 0.1.0
|
|
4
4
|
Summary: Simple Python HTTP Exec Server
|
|
5
5
|
Home-page: https://github.com/joknarf/pywebexec
|
|
6
6
|
Author: Franck Jouvanceau
|
|
@@ -53,9 +53,11 @@ Classifier: Topic :: System :: Systems Administration
|
|
|
53
53
|
Requires-Python: >=3.6
|
|
54
54
|
Description-Content-Type: text/markdown
|
|
55
55
|
License-File: LICENSE
|
|
56
|
+
Requires-Dist: python-daemon>=2.3.2
|
|
56
57
|
Requires-Dist: cryptography>=40.0.2
|
|
57
|
-
Requires-Dist: Flask>=
|
|
58
|
+
Requires-Dist: Flask>=2.0.3
|
|
58
59
|
Requires-Dist: Flask-HTTPAuth>=4.8.0
|
|
60
|
+
Requires-Dist: gunicorn>=21.2.0
|
|
59
61
|
|
|
60
62
|
[](https://pypi.org/project/pywebexec/)
|
|
61
63
|

|
|
@@ -127,5 +129,5 @@ $ pywebexec start
|
|
|
127
129
|
$ pywebexec status
|
|
128
130
|
$ pywebexec stop
|
|
129
131
|
```
|
|
130
|
-
* log of server are stored in
|
|
132
|
+
* log of server are stored in directory `[.config/].pywebexec/pywebexec_<listen>:<port>.log`
|
|
131
133
|
|
|
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
|