feat: 完善模型平台相关功能
This commit is contained in:
+4
-1
@@ -1,6 +1,9 @@
|
||||
FROM python:3.12-slim-bookworm
|
||||
|
||||
ENV PYTHONDONTWRITEBYTECODE=1 PYTHONUNBUFFERED=1 PYTHONPATH=/app
|
||||
ENV PYTHONDONTWRITEBYTECODE=1 \
|
||||
PYTHONUNBUFFERED=1 \
|
||||
PYTHONPATH=/app \
|
||||
PATH="/app/.venv/bin:${PATH}"
|
||||
WORKDIR /app
|
||||
# 从官方 Rclone 镜像直接复制 rclone 二进制文件(高效、稳定)
|
||||
COPY --from=rclone/rclone:latest /usr/local/bin/rclone /usr/local/bin/rclone
|
||||
|
||||
@@ -21,18 +21,34 @@ REMOTE_BUCKET = settings.remote_bucket
|
||||
RCLONE_PROCESS: subprocess.Popen | None = None
|
||||
|
||||
|
||||
def is_mountpoint(path: Path) -> bool:
|
||||
result = subprocess.run(
|
||||
["mountpoint", "-q", str(path)],
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL,
|
||||
)
|
||||
return result.returncode == 0
|
||||
def is_rclone_mount(path: Path) -> bool:
|
||||
"""Return True only for the FUSE mount created by this service.
|
||||
|
||||
Docker volumes and bind mounts are mount points too, so ``mountpoint -q``
|
||||
cannot distinguish the writable mount target from an active rclone mount.
|
||||
Linux exposes the filesystem type after `` - `` in ``mountinfo``.
|
||||
"""
|
||||
target = str(path.resolve())
|
||||
try:
|
||||
lines = Path("/proc/self/mountinfo").read_text().splitlines()
|
||||
except OSError:
|
||||
return False
|
||||
for line in lines:
|
||||
fields = line.split()
|
||||
if len(fields) < 10 or fields[4].replace("\\040", " ") != target:
|
||||
continue
|
||||
try:
|
||||
separator = fields.index("-")
|
||||
except ValueError:
|
||||
continue
|
||||
if separator + 1 < len(fields) and fields[separator + 1] == "fuse.rclone":
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def start_rclone_mount() -> None:
|
||||
global RCLONE_PROCESS
|
||||
if is_mountpoint(WORKSPACES_ROOT):
|
||||
if is_rclone_mount(WORKSPACES_ROOT):
|
||||
logger.info(f"Mountpoint already exists: {WORKSPACES_ROOT}")
|
||||
return
|
||||
|
||||
@@ -62,7 +78,7 @@ def start_rclone_mount() -> None:
|
||||
|
||||
timeout = 20
|
||||
while timeout > 0:
|
||||
if is_mountpoint(WORKSPACES_ROOT):
|
||||
if is_rclone_mount(WORKSPACES_ROOT):
|
||||
logger.info(f"rclone mount ready: {WORKSPACES_ROOT}")
|
||||
return
|
||||
if RCLONE_PROCESS.poll() is not None:
|
||||
@@ -85,10 +101,10 @@ def stop_rclone_mount() -> None:
|
||||
logger.warning("Force killing rclone")
|
||||
RCLONE_PROCESS.kill()
|
||||
|
||||
if is_mountpoint(WORKSPACES_ROOT):
|
||||
if is_rclone_mount(WORKSPACES_ROOT):
|
||||
logger.info(f"Unmount {WORKSPACES_ROOT}")
|
||||
result = subprocess.run(["fusermount3", "-u", str(WORKSPACES_ROOT)])
|
||||
if result.returncode != 0:
|
||||
subprocess.run(["umount", "-l", str(WORKSPACES_ROOT)])
|
||||
|
||||
logger.info("rclone stopped")
|
||||
logger.info("rclone stopped")
|
||||
|
||||
@@ -13,6 +13,7 @@ import os
|
||||
import secrets
|
||||
import subprocess
|
||||
import time
|
||||
from pathlib import Path
|
||||
from typing import TypedDict
|
||||
|
||||
from fastapi import HTTPException
|
||||
@@ -23,6 +24,7 @@ from common.utils import get_free_port, start_process
|
||||
from runtime.mount import WORKSPACES_ROOT
|
||||
|
||||
PUBLIC_BASE_URL = settings.public_base_url
|
||||
JUPYTER_PROCESS_CWD = Path("/app")
|
||||
|
||||
|
||||
class JupyterProcessRecord(TypedDict):
|
||||
@@ -106,7 +108,7 @@ async def start_workspace(ws_id: str) -> dict:
|
||||
"--allow-root",
|
||||
f"--ServerApp.token={token}",
|
||||
f"--ServerApp.base_url={base_path}",
|
||||
"--notebook-dir=.",
|
||||
f"--ServerApp.root_dir={workspace_path}",
|
||||
"--ServerApp.terminals_enabled=False",
|
||||
"--NotebookApp.terminals_enabled=False",
|
||||
"--ServerApp.allow_origin=*",
|
||||
@@ -116,7 +118,11 @@ async def start_workspace(ws_id: str) -> dict:
|
||||
]
|
||||
|
||||
try:
|
||||
process, log_file = start_process(cmd, workspace_path)
|
||||
# Keep the server process cwd off the rclone/FUSE mount. Remote
|
||||
# S3 directory refreshes can replace a virtual directory inode,
|
||||
# leaving a long-lived cwd marked "(deleted)" and making
|
||||
# os.getcwd() fail while Jupyter starts kernels or nbconvert.
|
||||
process, log_file = start_process(cmd, JUPYTER_PROCESS_CWD)
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to start Jupyter for {ws_id}: {e}")
|
||||
raise HTTPException(
|
||||
@@ -264,4 +270,4 @@ async def scan_workspaces() -> None:
|
||||
except Exception as err:
|
||||
logger.error(f"Startup failed for workspace '{entry}': {err}")
|
||||
|
||||
await asyncio.gather(*[_start(entry) for entry in entries])
|
||||
await asyncio.gather(*[_start(entry) for entry in entries])
|
||||
|
||||
Reference in New Issue
Block a user