Skip to content

Commit 5ac3452

Browse files
committed
stage 1 clear
1 parent 330a7d0 commit 5ac3452

File tree

13 files changed

+490
-3
lines changed

13 files changed

+490
-3
lines changed
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
2+
from .core import Runner
3+
__all__ = ["Runner"]

build/lib/pystealthrunner/audit.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
import os, sys
2+
3+
def check_consent():
4+
pass

build/lib/pystealthrunner/comms.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import os
2+
import sys
3+
import socket
4+
import json
5+
6+
PORT = int(os.environ.get("PYSTEALTH_PORT", 50506))
7+
8+
def get_socket():
9+
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
10+
s.settimeout(5)
11+
s.connect(("127.0.0.1", PORT))
12+
return s
13+
14+
def send_command(cmd):
15+
s = get_socket()
16+
s.sendall(json.dumps(cmd).encode())
17+
s.shutdown(socket.SHUT_WR)
18+
try:
19+
data = s.recv(4096)
20+
except socket.timeout:
21+
s.close()
22+
raise TimeoutError("Timed out waiting for response from background service.")
23+
s.close()
24+
if data:
25+
try:
26+
return json.loads(data.decode())
27+
except Exception:
28+
return data.decode()
29+
return None

build/lib/pystealthrunner/core.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import os
2+
import sys
3+
import inspect
4+
from .service import ensure_service_running
5+
from .comms import send_command
6+
from .audit import check_consent
7+
8+
class Runner:
9+
def __init__(self):
10+
check_consent()
11+
ensure_service_running()
12+
13+
def run_script(self, script_path=None):
14+
if os.environ.get("PYSTEALTH_BG") == "1":
15+
return
16+
if script_path is None:
17+
# Find the outermost script with __name__ == '__main__'
18+
frame = inspect.currentframe()
19+
main_script = None
20+
while frame:
21+
if frame.f_globals.get("__name__") == "__main__":
22+
filename = frame.f_globals.get("__file__")
23+
if filename and filename.endswith('.py') and os.path.isfile(filename):
24+
main_script = os.path.abspath(filename)
25+
frame = frame.f_back
26+
if not main_script:
27+
raise RuntimeError("Could not determine script file to run in background.")
28+
script_path = main_script
29+
if not script_path or not script_path.endswith('.py') or not os.path.isfile(script_path):
30+
raise RuntimeError(f"Background execution target is not a .py file: {script_path}")
31+
abs_path = os.path.abspath(script_path)
32+
result = send_command({"action": "run", "script": abs_path})
33+
return result.get("pid")
34+
35+
def stop_script(self, pid):
36+
return send_command({"action": "stop", "pid": pid})
37+
38+
def status(self):
39+
return send_command({"action": "status"})
Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
import multiprocessing, socket, threading, subprocess, sys, os, json, time, atexit
2+
3+
if __name__ == "__main__":
4+
from multiprocessing import freeze_support
5+
freeze_support()
6+
os.environ["PYSTEALTH_BG"] = "1"
7+
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '../..')))
8+
else:
9+
pass
10+
11+
PORT = int(os.environ.get("PYSTEALTH_PORT", 50506))
12+
PIDFILE = os.path.expanduser('~/.pystealthrunner.pid')
13+
14+
def cleanup_pidfile():
15+
if os.path.exists(PIDFILE):
16+
try:
17+
os.remove(PIDFILE)
18+
except Exception:
19+
pass
20+
21+
def service_main():
22+
atexit.register(cleanup_pidfile)
23+
if sys.platform == "win32":
24+
bind_addr = ("127.0.0.1", PORT)
25+
else:
26+
bind_addr = ("127.0.0.1", PORT)
27+
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
28+
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
29+
for attempt in range(20):
30+
try:
31+
s.bind(bind_addr)
32+
break
33+
except OSError as e:
34+
if attempt == 19:
35+
sys.exit(1)
36+
time.sleep(0.5)
37+
s.listen(5)
38+
with open(PIDFILE, 'w') as f:
39+
f.write(str(os.getpid()))
40+
manager = ScriptManager()
41+
while True:
42+
conn, _ = s.accept()
43+
data = b""
44+
while True:
45+
chunk = conn.recv(4096)
46+
if not chunk:
47+
break
48+
data += chunk
49+
try:
50+
cmd = json.loads(data.decode())
51+
except Exception as e:
52+
conn.sendall(json.dumps({"error": "Invalid command"}).encode())
53+
conn.close()
54+
continue
55+
if cmd.get("action") == "run":
56+
result = manager.run_script(cmd.get("script"))
57+
elif cmd.get("action") == "stop":
58+
result = manager.stop_script(cmd.get("pid"))
59+
elif cmd.get("action") == "status":
60+
result = manager.status()
61+
else:
62+
result = {"error": "Unknown action"}
63+
conn.sendall(json.dumps(result).encode())
64+
conn.close()
65+
66+
class ScriptManager:
67+
def __init__(self):
68+
self.processes = {}
69+
70+
def run_script(self, script_path):
71+
if not script_path or not os.path.isfile(script_path) or not script_path.endswith('.py'):
72+
return {"error": "Not a .py file"}
73+
env = os.environ.copy()
74+
env["PYSTEALTH_BG"] = "1"
75+
cwd = os.path.dirname(script_path)
76+
try:
77+
if sys.platform == "win32":
78+
DETACHED_PROCESS = 0x00000008
79+
CREATE_NO_WINDOW = 0x08000000
80+
creationflags = DETACHED_PROCESS | CREATE_NO_WINDOW
81+
# Use pythonw.exe if available to suppress all windows
82+
pythonw = sys.executable.replace("python.exe", "pythonw.exe")
83+
if os.path.exists(pythonw):
84+
exe = pythonw
85+
else:
86+
exe = sys.executable
87+
proc = subprocess.Popen([exe, script_path], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, env=env, cwd=cwd, creationflags=creationflags)
88+
else:
89+
proc = subprocess.Popen([sys.executable, script_path], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, env=env, cwd=cwd, start_new_session=True)
90+
self.processes[proc.pid] = proc
91+
return {"pid": proc.pid, "status": "started"}
92+
except Exception as e:
93+
return {"error": str(e)}
94+
95+
def stop_script(self, pid):
96+
try:
97+
pid = int(pid)
98+
except Exception:
99+
return {"error": "Invalid PID"}
100+
proc = self.processes.get(pid)
101+
if proc:
102+
proc.terminate()
103+
return {"stopped": pid}
104+
return {"error": "PID not found"}
105+
106+
def status(self):
107+
running = [pid for pid, proc in self.processes.items() if proc.poll() is None]
108+
return {"running": running}
109+
110+
def ensure_service_running():
111+
import time
112+
def is_pid_running(pid):
113+
try:
114+
os.kill(pid, 0)
115+
except OSError:
116+
return False
117+
else:
118+
return True
119+
120+
if os.path.exists(PIDFILE):
121+
try:
122+
with open(PIDFILE, 'r') as f:
123+
pid = int(f.read().strip())
124+
if not is_pid_running(pid):
125+
os.remove(PIDFILE)
126+
except Exception:
127+
os.remove(PIDFILE)
128+
129+
if not os.path.exists(PIDFILE):
130+
ctx = multiprocessing.get_context("spawn")
131+
p = ctx.Process(target=service_main, daemon=True)
132+
p.start()
133+
for _ in range(20):
134+
try:
135+
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
136+
s.settimeout(0.2)
137+
s.connect(('127.0.0.1', PORT))
138+
s.close()
139+
break
140+
except Exception:
141+
time.sleep(0.1)
142+
143+
if __name__ == "__main__":
144+
service_main()
7.81 KB
Binary file not shown.
15.2 KB
Binary file not shown.

0 commit comments

Comments
 (0)