diff --git a/Html/apps/extension_ase/ase_client/manager.py b/Html/apps/extension_ase/ase_client/manager.py
index fd295cb..03dae0f 100644
--- a/Html/apps/extension_ase/ase_client/manager.py
+++ b/Html/apps/extension_ase/ase_client/manager.py
@@ -1,14 +1,28 @@
+import signal
import sys
-import os
+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)
\ No newline at end of file
diff --git a/Html/apps/sockets/namespaces.py b/Html/apps/sockets/namespaces.py
index c7cc79a..03d3ebd 100644
--- a/Html/apps/sockets/namespaces.py
+++ b/Html/apps/sockets/namespaces.py
@@ -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))
\ No newline at end of file
diff --git a/Html/apps/views/vscode.py b/Html/apps/views/vscode.py
index 36d22bd..1e41d1d 100644
--- a/Html/apps/views/vscode.py
+++ b/Html/apps/views/vscode.py
@@ -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,19 +32,13 @@ 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"""
handle_path /vsc{user_uuid}/* {{
reverse_proxy localhost:{code_server_port}