add code-server clean
This commit is contained in:
@@ -1,14 +1,28 @@
|
||||
import signal
|
||||
import sys
|
||||
import os
|
||||
import time
|
||||
import uuid
|
||||
current_directory = os.path.dirname(os.path.abspath(__file__))
|
||||
sys.path.append(current_directory)
|
||||
from flask_socketio import SocketIO, join_room, emit, Namespace
|
||||
import os
|
||||
from ...function_tools.function_manager import FunctionManager
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Dict, Optional
|
||||
import subprocess
|
||||
import threading
|
||||
from ...utils import *
|
||||
EMPTY_CHAPTER_CHAIN = [ Chapter(1, CHAPTER_FOCUS, "本章是未打开某个具体章节时的默认章节。", "处于本章节时不会有任何章节跳转。请作为一名经验丰富的算法教师,回答用户的问题。") ]
|
||||
|
||||
@dataclass
|
||||
class ProcRecord:
|
||||
popen: subprocess.Popen
|
||||
user_id: str
|
||||
path_dir: str
|
||||
port: int
|
||||
deadline_ts: Optional[float] = None
|
||||
timer: Optional[threading.Timer] = field(default=None, repr=False)
|
||||
|
||||
|
||||
class ChatManager:
|
||||
@@ -24,6 +38,13 @@ class ChatManager:
|
||||
self.chapter_chain = load_chapters(raw_markdown, raw_markdown_prompts, raw_score_prompts)
|
||||
self.function_manager = FunctionManager()
|
||||
self.bb = None
|
||||
self._procs: Dict[str, ProcRecord] = {}
|
||||
self._lock = threading.RLock()
|
||||
|
||||
def disconnect(self, uuid):
|
||||
self.stop(uuid)
|
||||
self.ase_client.disconnect()
|
||||
|
||||
|
||||
def next_chapter(self):
|
||||
assert self.chapter_chain_now + 1 < len(self.chapter_chain), "chapter_chain_now out of range"
|
||||
@@ -110,3 +131,103 @@ class ChatManager:
|
||||
if (str(e) == "no path specified"):
|
||||
self.socketio.emit("system_message", "未加载IDE文件同步,请尝试重新打开IDE文件或刷新页面,并确保插件已连接", room=id, namespace='/agent')
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
def start_code_server(self, key: str, user_id: str, path_dir: str, code_server_port: int, ttl_seconds: Optional[int] = None) -> ProcRecord:
|
||||
# key is user_uuid
|
||||
# 注意:建议使用 start_new_session=True 方便后续 kill 进程组
|
||||
# 如果必须 sudo,最好加 -n(非交互)避免卡住;也可以考虑用 systemd-run 或专用守护
|
||||
cmd = [
|
||||
"sudo", "-n", "-u", user_id,
|
||||
"code-server",
|
||||
"--bind-addr", f"0.0.0.0:{code_server_port}",
|
||||
"--auth", "none",
|
||||
path_dir,
|
||||
"--extensions-dir", "/share/extension/"
|
||||
]
|
||||
|
||||
pop = subprocess.Popen(
|
||||
cmd,
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.STDOUT,
|
||||
start_new_session=True # 等价 setsid,新建进程组,方便整体结束
|
||||
)
|
||||
|
||||
record = ProcRecord(
|
||||
popen=pop,
|
||||
user_id=user_id,
|
||||
path_dir=path_dir,
|
||||
port=code_server_port,
|
||||
deadline_ts=(time.time() + ttl_seconds) if ttl_seconds else None
|
||||
)
|
||||
|
||||
# 设置了到期停
|
||||
if ttl_seconds:
|
||||
t = threading.Timer(ttl_seconds, self.stop, kwargs={"key": key})
|
||||
t.daemon = True
|
||||
t.start()
|
||||
record.timer = t
|
||||
|
||||
with self._lock:
|
||||
self._procs[key] = record
|
||||
|
||||
return record
|
||||
|
||||
def stop(self, key: str, grace_seconds: int = 5) -> bool:
|
||||
with self._lock:
|
||||
rec = self._procs.pop(key, None)
|
||||
|
||||
if not rec:
|
||||
return False
|
||||
|
||||
# 先取消到期定时器(如果是手动停)
|
||||
if rec.timer:
|
||||
try:
|
||||
rec.timer.cancel()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
pop = rec.popen
|
||||
if pop.poll() is not None:
|
||||
return True # 已退出
|
||||
|
||||
try:
|
||||
# 优雅:向进程组发 SIGTERM
|
||||
os.killpg(pop.pid, signal.SIGTERM)
|
||||
except ProcessLookupError:
|
||||
return True
|
||||
|
||||
# 等待优雅退出
|
||||
try:
|
||||
pop.wait(timeout=grace_seconds)
|
||||
return True
|
||||
except subprocess.TimeoutExpired:
|
||||
pass
|
||||
|
||||
# 兜底:强杀进程组
|
||||
try:
|
||||
os.killpg(pop.pid, signal.SIGKILL)
|
||||
except ProcessLookupError:
|
||||
pass
|
||||
finally:
|
||||
try:
|
||||
pop.wait(timeout=1)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return True
|
||||
|
||||
def get(self, key: str) -> Optional[ProcRecord]:
|
||||
with self._lock:
|
||||
return self._procs.get(key)
|
||||
|
||||
def stop_all(self):
|
||||
with self._lock:
|
||||
keys = list(self._procs.keys())
|
||||
for k in keys:
|
||||
self.stop(k)
|
||||
@@ -83,8 +83,6 @@ class AgentNamespace(Namespace):
|
||||
chatmanager.function_manager.add_function(judge, {'course_id': course_id, 'lesson_name': lesson_name, 'root_path': f'../../study/{user_id}/{course_id}/{chapter_name}/{lesson_name}'})
|
||||
chatmanager.function_manager.add_function(next_chapter, {'user_uuid': user_uuid})
|
||||
|
||||
user_uuid2ase_client[user_uuid].loadmarkdown(fmd, fmdp, fsp)
|
||||
|
||||
join_room(user_uuid, namespace='/agent') # 将该用户加入以user_id为名的room
|
||||
backboard_manager.add_backboard(user_uuid, user_id, course_id, lesson_name, root_path=f'../../study/{user_id}/{course_id}/{chapter_name}/{lesson_name}')
|
||||
chatmanager.bb = backboard_manager.get_backboard(user_uuid)
|
||||
@@ -143,5 +141,10 @@ class AgentNamespace(Namespace):
|
||||
|
||||
|
||||
def on_disconnect(self,data):
|
||||
ex = current_app.extensions
|
||||
uuid = session.get('user_uuid')
|
||||
user_uuid2chatmanager = ex["user_uuid2chatmanager"]
|
||||
user_uuid2chatmanager[uuid].disconnect(uuid)
|
||||
|
||||
print("VSCode client disconnected")
|
||||
print("Disconnect reason:"+str(data))
|
||||
@@ -11,20 +11,12 @@ def desktop(user_uuid, course_id, chapter_name, lesson_name):
|
||||
username2uuid = current_app.extensions["username2uuid"]
|
||||
uuid2username = current_app.extensions["uuid2username"]
|
||||
userid_recorder = current_app.extensions["userid_recorder"]
|
||||
# session 中放 uuid(访客也能进则给一个)
|
||||
if "user_uuid" not in session:
|
||||
user_uuid = "user_" + str(uuid.uuid4())
|
||||
session["user_uuid"] = user_uuid
|
||||
uuid2username[user_uuid] = "guest"+str(uuid.uuid4())[0:5]
|
||||
username2uuid[uuid2username[user_uuid]] = user_uuid
|
||||
|
||||
return redirect(url_for("auth.login"))
|
||||
# 全局映射
|
||||
user_id = uuid2username[user_uuid]
|
||||
userid_recorder[f"{user_id}&{course_id}"] = session["user_uuid"]
|
||||
|
||||
# 按课程/章节创建工作目录
|
||||
base_root = current_app.config["STUDENT_WORKSPACE_ROOT"]
|
||||
|
||||
user_home = os.path.join("/home", user_id)
|
||||
try:
|
||||
subprocess.run(["sudo", "useradd", "-m", "-d", user_home, user_id]) # 创建用户
|
||||
@@ -40,17 +32,11 @@ def desktop(user_uuid, course_id, chapter_name, lesson_name):
|
||||
print(f"Error creating directory {path_dir} details {str(e)}")
|
||||
|
||||
|
||||
|
||||
chatmanager = current_app.extensions["user_uuid2chatmanager"][user_uuid]
|
||||
code_server_port = 10000 + int(uuid.uuid4().int % 10000)
|
||||
try:
|
||||
subprocess.Popen([
|
||||
"sudo", "-u", user_id,
|
||||
"code-server", "--bind-addr", f"0.0.0.0:{code_server_port}",
|
||||
"--auth", "none", path_dir,
|
||||
"--extensions-dir", "/share/extension/"
|
||||
])
|
||||
except subprocess.CalledProcessError as e:
|
||||
print(f"Error start code-server at {code_server_port} with path{path_dir} {e}")
|
||||
chatmanager.start_code_server(user_uuid, user_id, path_dir, code_server_port)
|
||||
|
||||
|
||||
|
||||
|
||||
caddy_conf = f"""
|
||||
|
||||
Reference in New Issue
Block a user