add code-server clean

This commit is contained in:
CakeCN
2025-09-12 14:55:08 +08:00
parent baf7271ce2
commit e025ac6a9c
3 changed files with 133 additions and 23 deletions

View File

@@ -1,14 +1,28 @@
import signal
import sys import sys
import os import os
import time
import uuid
current_directory = os.path.dirname(os.path.abspath(__file__)) current_directory = os.path.dirname(os.path.abspath(__file__))
sys.path.append(current_directory) sys.path.append(current_directory)
from flask_socketio import SocketIO, join_room, emit, Namespace from flask_socketio import SocketIO, join_room, emit, Namespace
import os import os
from ...function_tools.function_manager import FunctionManager from ...function_tools.function_manager import FunctionManager
from dataclasses import dataclass, field
from typing import Dict, Optional
import subprocess
import threading
from ...utils import * from ...utils import *
EMPTY_CHAPTER_CHAIN = [ Chapter(1, CHAPTER_FOCUS, "本章是未打开某个具体章节时的默认章节。", "处于本章节时不会有任何章节跳转。请作为一名经验丰富的算法教师,回答用户的问题。") ] 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: class ChatManager:
@@ -24,6 +38,13 @@ class ChatManager:
self.chapter_chain = load_chapters(raw_markdown, raw_markdown_prompts, raw_score_prompts) self.chapter_chain = load_chapters(raw_markdown, raw_markdown_prompts, raw_score_prompts)
self.function_manager = FunctionManager() self.function_manager = FunctionManager()
self.bb = None 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): def next_chapter(self):
assert self.chapter_chain_now + 1 < len(self.chapter_chain), "chapter_chain_now out of range" 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"): if (str(e) == "no path specified"):
self.socketio.emit("system_message", "未加载IDE文件同步请尝试重新打开IDE文件或刷新页面并确保插件已连接", room=id, namespace='/agent') 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)

View File

@@ -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(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}) 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 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}') 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) chatmanager.bb = backboard_manager.get_backboard(user_uuid)
@@ -143,5 +141,10 @@ class AgentNamespace(Namespace):
def on_disconnect(self,data): 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("VSCode client disconnected")
print("Disconnect reason:"+str(data)) print("Disconnect reason:"+str(data))

View File

@@ -11,20 +11,12 @@ def desktop(user_uuid, course_id, chapter_name, lesson_name):
username2uuid = current_app.extensions["username2uuid"] username2uuid = current_app.extensions["username2uuid"]
uuid2username = current_app.extensions["uuid2username"] uuid2username = current_app.extensions["uuid2username"]
userid_recorder = current_app.extensions["userid_recorder"] userid_recorder = current_app.extensions["userid_recorder"]
# session 中放 uuid访客也能进则给一个
if "user_uuid" not in session: if "user_uuid" not in session:
user_uuid = "user_" + str(uuid.uuid4()) return redirect(url_for("auth.login"))
session["user_uuid"] = user_uuid
uuid2username[user_uuid] = "guest"+str(uuid.uuid4())[0:5]
username2uuid[uuid2username[user_uuid]] = user_uuid
# 全局映射 # 全局映射
user_id = uuid2username[user_uuid] user_id = uuid2username[user_uuid]
userid_recorder[f"{user_id}&{course_id}"] = session["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) user_home = os.path.join("/home", user_id)
try: try:
subprocess.run(["sudo", "useradd", "-m", "-d", user_home, user_id]) # 创建用户 subprocess.run(["sudo", "useradd", "-m", "-d", user_home, user_id]) # 创建用户
@@ -40,19 +32,13 @@ def desktop(user_uuid, course_id, chapter_name, lesson_name):
print(f"Error creating directory {path_dir} details {str(e)}") 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) code_server_port = 10000 + int(uuid.uuid4().int % 10000)
try: chatmanager.start_code_server(user_uuid, user_id, path_dir, code_server_port)
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}")
caddy_conf = f""" caddy_conf = f"""
handle_path /vsc{user_uuid}/* {{ handle_path /vsc{user_uuid}/* {{
reverse_proxy localhost:{code_server_port} reverse_proxy localhost:{code_server_port}