diff --git a/.gitignore b/.gitignore index af1051b..45133ad 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,4 @@ home.repo testdata/ /amd64/ artifacts/ +/lib diff --git a/.vscode/settings.json b/.vscode/settings.json index b6b4747..6beae10 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,5 +1,6 @@ { - "go.toolsEnvVars": { - "GOOS": "linux" - } + "go.toolsEnvVars": { + "GOOS": "linux", + "CGO_ENABLED": "0" + } } diff --git a/Dockerfile b/Dockerfile index 63e3c38..7c3595d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,18 +1,23 @@ -FROM ubuntu:jammy +FROM debian:stable-slim ARG TARGETARCH ENV DEBIAN_FRONTEND=noninteractive RUN apt update -RUN apt install --no-install-recommends -y ca-certificates tzdata curl xz-utils +RUN apt install --no-install-recommends -y ca-certificates tzdata curl wget xz-utils + +# ENV spk=/tmp/xl.spk spk_tmp=/tmp/xl-tmp rootfs=/rootfs +# RUN mkdir -p ${spk_tmp} ${rootfs}/etc/ssl/certs +# RUN wget -O ${spk} "https://down.sandai.net/nas/nasxunlei-DSM7-$([ "${TARGETARCH}" = "arm64" ] && echo x86_64 || echo armv8).spk" +# RUN tar -xvOf ${spk} package.tgz | tar -xvJC ${spk_tmp} ui/index.cgi bin +# RUN ldd ${spk_tmp}/ui/index.cgi ${spk_tmp}/bin/bin/xunlei-pan-cli* 2>/dev/null | grep '=>' | awk '{printf "cp %s /rootfs/lib/%s;\n", $3, $1}' | sh + +ENV rootfs=/rootfs +RUN mkdir -p ${rootfs}/etc/ssl/certs ${rootfs}/lib +RUN find /usr/lib \( -name libdl.so.2 -o -name libgcc_s.so.1 -o -name libstdc++.so.6 \) -exec cp -Lr {} ${rootfs}/lib/ \; -ENV spk=/tmp/xl.spk spk_tmp=/tmp/xl-tmp rootfs=/rootfs -RUN mkdir -p ${spk_tmp} ${rootfs}/etc/ssl/certs -RUN curl -kLo ${spk} "https://down.sandai.net/nas/nasxunlei-DSM7-$([ "${TARGETARCH}" = "arm64" ] && echo x86_64 || echo armv8).spk" -RUN tar -xvOf ${spk} package.tgz | tar -xvJC ${spk_tmp} ui/index.cgi bin -RUN ldd ${spk_tmp}/ui/index.cgi ${spk_tmp}/bin/bin/* 2>/dev/null | grep -v 'not found' | awk '{print $3}' | sort | uniq | xargs -I '{}' cp -v '{}' ${rootfs}/lib RUN cp -Lr /usr/share/zoneinfo/Asia/Chongqing ${rootfs}/etc/localtime RUN echo "Asia/Chongqing" >${rootfs}/etc/timezone -RUN cp -Lr --parents /etc/ssl/certs/ca-certificates.crt ${rootfs} +RUN cp -Lr --parents /etc/ssl/certs/ca-certificates.crt ${rootfs}/ COPY artifacts/xlp-${TARGETARCH} /rootfs/xlp RUN chmod +x /rootfs/xlp @@ -32,17 +37,16 @@ LABEL org.opencontainers.image.source=https://github.com/cnk3x/xunlei COPY --from=1 /rootfs / ENV XL_DASHBOARD_PORT=2345 \ - XL_DASHBOARD_IP= \ - XL_DASHBOARD_USERNAME= \ - XL_DIR_DOWNLOAD=/xunlei/downloads \ - XL_PREVENT_UPDATE= \ - XL_SPK_URL= \ - XL_UID= \ - XL_GID= \ - XL_DEBUG= + XL_DASHBOARD_IP= \ + XL_DASHBOARD_USERNAME= \ + XL_DIR_DOWNLOAD=/xunlei/downloads \ + XL_PREVENT_UPDATE= \ + XL_SPK_URL= \ + XL_UID= \ + XL_GID= \ + XL_DEBUG= VOLUME [ "/xunlei/data", "/xunlei/var/packages/pan-xunlei-com" ] - EXPOSE 2345 -CMD [ "/xlp", "-r", "/xunlei" ] +CMD [ "/xlp" ] diff --git a/Makefile b/Makefile index c5d1dd5..83d4920 100644 --- a/Makefile +++ b/Makefile @@ -46,10 +46,13 @@ wsl:: GOARCH=amd64 $(GoBuild) -v -o /usr/local/bin/xlp ./cmd/xlp home:: amd64 - docker buildx build --push -t $(HOME_REPO)$(NAME):$(VERSION) . + $(DBuildBase) --push -t $(HOME_REPO)$(NAME):$(VERSION) . load:: amd64 - docker buildx build --load -t $(NAME):$(VERSION) . + $(DBuildBase) --load --platform linux/amd64,linux/arm64 -t $(NAME):$(VERSION) . -ubuntu:: - $(DBuildBase) --load -t $(NAME)-ubuntu:$(VERSION) -f ubuntu.Dockerfile . +ubuntu:: amd64 + $(DBuildBase) --load --platform linux/amd64,linux/arm64 -t $(NAME)-ubuntu:$(VERSION) -f ubuntu.Dockerfile . + +debian:: amd64 + $(DBuildBase) --load --platform linux/amd64,linux/arm64 -t $(NAME)-debian:$(VERSION) -f debian.Dockerfile . diff --git a/README.md b/README.md index 1e345ed..6b03133 100644 --- a/README.md +++ b/README.md @@ -55,22 +55,26 @@ XL_DASHBOARD_IP= XL_DASHBOARD_USERNAME= # 网页访问的密码 XL_DASHBOARD_PASSWORD= -# 下载保存文件夹,多个用冒号`:`隔开 +# 如果需要指定多个下载目录,手动指定XL_DIR_DOWNLOAD +# 多个以冒号`:`隔开,在容器内,都必须以 /xunlei 开头,迅雷面板选择保存路径显示会去掉/xunlei前缀 +# 指定后可以在 volumes 中绑定宿主机实际目录 +# 迅雷云盘的缓存会使用第一个目录会缓存 +# /xunlei/后面可以用中文 +# 不设置默认一个目录 /xunlei/downloads XL_DIR_DOWNLOAD=/xunlei/downloads -# 程序数据保存文件夹,存储了登录的账号,下载进度等信息 +# 程序数据保存文件夹,存储了登录的账号,下载进度等信息,容器内不要更改 XL_DIR_DATA=/xunlei/data -# CHROOT主目录,这个不要改 -XL_CHROOT=/xunlei # 阻止更新 -XL_PREVENT_UPDATE=true -# SPK下载链接 -XL_SPK_URL=https://down.sandai.net/nas/nasxunlei-DSM7-(x86_64或者armv8).spk -# 运行迅雷的用户ID -XL_UID= +XL_PREVENT_UPDATE=false +# SPK下载链接, 可以使用 file:/// 访问本地文件, 真实使用路径会去掉 file://, 所以如果是绝对路径, 三个斜杠不能少 +XL_SPK=https://down.sandai.net/nas/nasxunlei-DSM7-(x86_64或者armv8).spk +# 运行迅雷的用户ID, 默认0,即 root 账号 +# 推荐使用当前账号的UID和GID, 一般来说是 1000, 以免出现下载后普通账号无法处理文件的情况 +XL_UID=0 # 运行迅雷的用户GID -XL_GID= +XL_GID=0 # 是否开启调试日志 -XL_DEBUG= +XL_DEBUG=false ``` #### 在容器中运行 @@ -100,33 +104,33 @@ cnk3x/xunlei ```yaml services: - xunlei: - container_name: xunlei - image: cnk3x/xunlei:latest - restart: unless-stopped + xunlei: + container_name: xunlei + image: cnk3x/xunlei:latest + restart: unless-stopped - # 宿主机名,迅雷远程控制的名称与此相关,一般是 `群晖-${hostname}` - hostname: my_storage - # 必须, cap_add: [SYS_ADMIN] 和 privileged: true 二选一 - cap_add: [SYS_ADMIN] - ports: [2345:2345] # 面板访问端口,如需更改,替换前面的2345即可 - environment: - # 如果需要指定多个下载目录,手动指定XL_DIR_DOWNLOAD - # 多个以冒号`:`隔开,都必须以 /xunlei 开头,迅雷面板选择保存路径显示会去掉/xunlei前缀 - # 指定后可以在 volumes 中绑定宿主机实际目录 - # 迅雷云盘的缓存会使用第一个目录会缓存 - # /xunlei/后面可以用中文 - # 不设置默认一个目录 /xunlei/downloads - - XL_DIR_DOWNLOAD=/xunlei/downloads:/xunlei/movies:/xunlei/apps - volumes: - # 对应 XL_DIR_DOWNLOAD 指定的目录, 请替换冒号前面的路径为实际路径 - - 实际【下载】文件夹路径:/xunlei/downloads - - 实际【电影】文件夹路径:/xunlei/movies - - 实际【软件】文件夹路径:/xunlei/apps - # 数据目录,必须,迅雷运行时,插件,升级,包括登录数据都在这 - - ./data:/xunlei/data - # 可选,不配置每次启动都会从远程下载spk - - ./cache:/xunlei/var/packages/pan-xunlei-com + # 宿主机名,迅雷远程控制的名称与此相关,一般是 `群晖-${hostname}` + hostname: my_storage + # 必须, cap_add: [SYS_ADMIN] 和 privileged: true 二选一 + cap_add: [SYS_ADMIN] + ports: [2345:2345] # 面板访问端口,如需更改,替换前面的2345即可 + environment: + # 如果需要指定多个下载目录,手动指定XL_DIR_DOWNLOAD + # 多个以冒号`:`隔开,都必须以 /xunlei 开头,迅雷面板选择保存路径显示会去掉/xunlei前缀 + # 指定后可以在 volumes 中绑定宿主机实际目录 + # 迅雷云盘的缓存会使用第一个目录会缓存 + # /xunlei/后面可以用中文 + # 不设置默认一个目录 /xunlei/downloads + - XL_DIR_DOWNLOAD=/xunlei/downloads:/xunlei/movies:/xunlei/apps + volumes: + # 对应 XL_DIR_DOWNLOAD 指定的目录, 请替换冒号前面的路径为实际路径 + - 实际【下载】文件夹路径:/xunlei/downloads + - 实际【电影】文件夹路径:/xunlei/movies + - 实际【软件】文件夹路径:/xunlei/apps + # 数据目录,必须,迅雷运行时,插件,升级,包括登录数据都在这 + - ./data:/xunlei/data + # 可选,不配置每次启动都会从远程下载spk + - ./cache:/xunlei/var/packages/pan-xunlei-com ``` ## Used By diff --git a/cmd.go b/cmd.go index 8e32484..6e6f672 100644 --- a/cmd.go +++ b/cmd.go @@ -1,7 +1,6 @@ package xunlei import ( - "bufio" "context" "io" "log/slog" @@ -11,6 +10,7 @@ import ( "syscall" "github.com/cnk3x/xunlei/pkg/log" + "github.com/cnk3x/xunlei/pkg/utils" ) func cmdRun(ctx context.Context, name string, args []string, dir string, env []string, uid, gid uint32) (err error) { @@ -40,45 +40,39 @@ func wrapConsole(ctx context.Context) io.WriteCloser { lv := slog.LevelDebug re0 := regexp.MustCompile(`^\d{2}/\d{2} \d{2}:\d{2}:\d{2}(\.\d+)? (INFO|ERROR|WARNING)\s*>?\s*`) re1 := regexp.MustCompile(`^[\dTZ:\.-]?\s*(INFO|ERROR|WARNING)\s*(\[\d+\])?\s*`) - r, w := io.Pipe() - go func() { - for scan := bufio.NewScanner(r); scan.Scan(); { - s := scan.Text() - if strings.Contains(s, `filter not match`) { - continue - } - - if strings.Contains(s, `DetectPlatform err:`) { - continue - } - - if strings.Contains(s, `detect err:key file lost`) { - continue - } - - switch { - case strings.HasPrefix(s, "panic:"): - lv = slog.LevelError - case strings.HasPrefix(s, "RunSafe panic:"): - lv = slog.LevelError - case re0.MatchString(s): - m := re0.FindStringSubmatch(s) - if lv = log.LevelFromString(m[2], slog.LevelDebug); lv == slog.LevelInfo { - lv = slog.LevelDebug - } - s = s[len(m[0]):] - case re1.MatchString(s): - m := re0.FindStringSubmatch(s) - if lv = log.LevelFromString(m[1], slog.LevelDebug); lv == slog.LevelInfo { - lv = slog.LevelDebug - } - s = s[len(m[0]):] - } - s = strings.ReplaceAll(s, `\u0000`, "") - slog.Log(ctx, lv, s) + return utils.LineWriter(func(s string) { + if strings.Contains(s, `filter not match`) { + return } - }() - return w + if strings.Contains(s, `DetectPlatform err:`) { + return + } + + if strings.Contains(s, `detect err:key file lost`) { + return + } + + switch { + case strings.HasPrefix(s, "panic:"): + lv = slog.LevelError + case strings.HasPrefix(s, "RunSafe panic:"): + lv = slog.LevelError + case re0.MatchString(s): + m := re0.FindStringSubmatch(s) + if lv = log.LevelFromString(m[2], slog.LevelDebug); lv == slog.LevelInfo { + lv = slog.LevelDebug + } + s = s[len(m[0]):] + case re1.MatchString(s): + m := re0.FindStringSubmatch(s) + if lv = log.LevelFromString(m[1], slog.LevelDebug); lv == slog.LevelInfo { + lv = slog.LevelDebug + } + s = s[len(m[0]):] + } + s = strings.ReplaceAll(s, `\u0000`, "") + slog.Log(ctx, lv, s) + }) } diff --git a/cmd/xlp/main.go b/cmd/xlp/main.go index feb02f6..cb2795c 100644 --- a/cmd/xlp/main.go +++ b/cmd/xlp/main.go @@ -47,7 +47,8 @@ func main() { slog.InfoContext(ctx, fmt.Sprintf("force_download: %t", cfg.ForceDownload)) if err := spk.Download(ctx, cfg.SpkUrl, filepath.Join(cfg.Chroot, xunlei.SYNOPKG_PKGDEST), cfg.ForceDownload); err != nil { - return + slog.ErrorContext(ctx, "exit", "err", err) + os.Exit(1) } if spkVer := utils.Cat(filepath.Join(cfg.Chroot, xunlei.PAN_XUNLEI_VER)); spkVer != "" { @@ -58,21 +59,14 @@ func main() { slog.InfoContext(ctx, fmt.Sprintf(`xunlei version: %s`, cliVer)) } - err := rootfs.Run(ctx, - cfg.Chroot, - xunlei.NewRun(cfg), - rootfs.Before(xunlei.BeforeChroot(cfg)), + if err := rootfs.Run( + log.Prefix(ctx, "boot"), + cfg.Chroot, xunlei.NewRun(cfg), rootfs.Basic, - rootfs.MountBindRoot("/lib", cfg.Chroot), - rootfs.MountBindRoot("/usr/lib", cfg.Chroot), - rootfs.MountBindRoot("/lib64", cfg.Chroot, rootfs.Optional()), - rootfs.MountBindRoot("/usr/lib64", cfg.Chroot, rootfs.Optional()), - rootfs.MountBindRoot("/etc/ssl", cfg.Chroot, rootfs.Optional()), - rootfs.LinkRoot("/etc/timezone", cfg.Chroot, 0666, true), - rootfs.LinkRoot("/etc/resolv.conf", cfg.Chroot, 0666, true), - ) - - if err != nil { + rootfs.Before(xunlei.BeforeChroot(cfg)), + rootfs.Binds(cfg.Chroot, "/lib", "/lib64", "/usr", "/sbin", "/bin", "/etc/ssl"), + rootfs.Links(cfg.Chroot, "/etc/timezone", "/etc/localtime", "/etc/resolv.conf", "/etc/passwd", "/etc/group", "/etc/shadow"), + ); err != nil { slog.ErrorContext(ctx, "exit", "err", err) os.Exit(1) } diff --git a/config.go b/config.go index edd5df8..81c99f4 100644 --- a/config.go +++ b/config.go @@ -52,7 +52,7 @@ func ConfigBind(cfg *Config) (err error) { flags.Var(&cfg.Gid, "gid", "g", "运行迅雷的用户组ID", "XL_GID", "GID") flags.Var(&cfg.PreventUpdate, "prevent_update", "", "阻止更新", "XL_PREVENT_UPDATE") flags.Var(&cfg.Chroot, "chroot", "r", "CHROOT主目录", "XL_CHROOT") - flags.Var(&cfg.SpkUrl, "spk", "", "SPK 下载链接", "XL_SPK_URL") + flags.Var(&cfg.SpkUrl, "spk", "", "SPK 下载链接", "XL_SPK") flags.Var(&cfg.ForceDownload, "force_download", "F", "强制下载") flags.Var(&cfg.Debug, "debug", "", "是否开启调试日志", "XL_DEBUG") diff --git a/cspell.config.yaml b/cspell.config.yaml index dc18c6a..3b549f6 100644 --- a/cspell.config.yaml +++ b/cspell.config.yaml @@ -49,6 +49,7 @@ words: - rootfs - shenzhen - softprops + - succ - SYNO - synobios - synoinfo diff --git a/debian.Dockerfile b/debian.Dockerfile new file mode 100644 index 0000000..8f3a4fd --- /dev/null +++ b/debian.Dockerfile @@ -0,0 +1,25 @@ +FROM debian:stable-slim +ARG TARGETARCH + +LABEL org.opencontainers.image.authors=cnk3x +LABEL org.opencontainers.image.source=https://github.com/cnk3x/xunlei + +RUN apt update && apt install --no-install-recommends -y ca-certificates tzdata && rm -rf /var/lib/apt/lists/* && \ + rm -f /etc/localtime /etc/timezone && \ + cp -Lr /usr/share/zoneinfo/Asia/Chongqing /etc/localtime && \ + echo "Asia/Chongqing" >/etc/timezone + +COPY artifacts/xlp-${TARGETARCH} /xlp + +ENV XL_DASHBOARD_PORT=2345 \ + XL_DASHBOARD_IP= \ + XL_DASHBOARD_USERNAME= \ + XL_DIR_DOWNLOAD=/xunlei/downloads \ + XL_PREVENT_UPDATE= \ + XL_SPK_URL= \ + XL_UID= \ + XL_GID= \ + XL_DEBUG= + +CMD [ "/xlp" ] + diff --git a/go.mod b/go.mod index 79d62b2..eecde84 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,6 @@ module github.com/cnk3x/xunlei go 1.25.4 require ( - github.com/go-chi/chi/v5 v5.2.3 github.com/samber/lo v1.52.0 github.com/spf13/pflag v1.0.10 github.com/ulikunitz/xz v0.5.15 diff --git a/go.sum b/go.sum index 743e082..0aa75f5 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,3 @@ -github.com/go-chi/chi/v5 v5.2.3 h1:WQIt9uxdsAbgIYgid+BpYc+liqQZGMHRaUwp0JUcvdE= -github.com/go-chi/chi/v5 v5.2.3/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops= github.com/samber/lo v1.52.0 h1:Rvi+3BFHES3A8meP33VPAxiBZX/Aws5RxrschYGjomw= github.com/samber/lo v1.52.0/go.mod h1:4+MXEGsJzbKGaUEQFKBq2xtfuznW9oz/WrgyzMzRoM0= github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= diff --git a/pkg/log/slog.go b/pkg/log/slog.go index a647c5a..2420c4c 100644 --- a/pkg/log/slog.go +++ b/pkg/log/slog.go @@ -38,6 +38,15 @@ func Prefix(ctx context.Context, prefix string) context.Context { return context.WithValue(ctx, ctxPrefixKey, prefix) } +func PrefixAttr(prefix string) slog.Attr { return slog.String(prefixKey, prefix) } + +func GetPrefix(ctx context.Context) slog.Attr { + if v := ctx.Value(ctxPrefixKey); v != nil { + return slog.String(prefixKey, v.(string)) + } + return slog.Attr{} +} + var ( defaultLevel = slog.LevelInfo defaultTimeFormat = "01/02 15:04:05" diff --git a/pkg/log/std.go b/pkg/log/std.go index 5d7501d..719f19d 100644 --- a/pkg/log/std.go +++ b/pkg/log/std.go @@ -3,14 +3,11 @@ package log import ( "context" "io" - "log" "log/slog" "runtime" "time" ) -func Std(w io.Writer, prefix string) *log.Logger { return log.New(w, prefix, 0) } - func Writer(ctx context.Context) io.Writer { return &stdWriter{ctx: ctx, h: slog.Default().Handler()} } diff --git a/pkg/rootfs/mount.go b/pkg/rootfs/mount.go deleted file mode 100644 index 601da70..0000000 --- a/pkg/rootfs/mount.go +++ /dev/null @@ -1,55 +0,0 @@ -package rootfs - -import ( - "cmp" - "fmt" -) - -type MountOption func(*MountOptions) - -func Mount(target string, options ...MountOption) MountOptions { - mount := MountOptions{Target: target} - for _, option := range options { - option(&mount) - } - return mount -} - -func MountBind(v ...bool) MountOption { - return func(mpo *MountOptions) { mpo.Bind = len(v) == 0 || cmp.Or(v...) } -} - -func MountOptional(v ...bool) MountOption { - return func(mpo *MountOptions) { mpo.Optional = len(v) == 0 || cmp.Or(v...) } -} - -func Optional(v ...bool) MountOption { - return func(mpo *MountOptions) { mpo.Optional = len(v) == 0 || cmp.Or(v...) } -} - -func Target(v string) MountOption { return func(mo *MountOptions) { mo.Target = v } } - -type MountSourceOption func(*MountSourceOptions) - -func MountSource(source string, options ...MountSourceOption) MountOption { - s := MountSourceOptions{Source: source} - for _, option := range options { - option(&s) - } - return func(mpo *MountOptions) { mpo.Source = append(mpo.Source, s) } -} - -func MountType(source string, options ...MountSourceOption) MountOption { - return MountSource(source, append(options, MountFstype(source))...) -} - -func MountFstype(v string) MountSourceOption { return func(ms *MountSourceOptions) { ms.Fstype = v } } -func MountFlags(v uintptr) MountSourceOption { return func(ms *MountSourceOptions) { ms.Flags = v } } -func MountData(v string) MountSourceOption { - return func(mso *MountSourceOptions) { - mso.Data += iif(mso.Data == "", "", ",") + v - } -} - -func MountDataMode(perm string) MountSourceOption { return MountData(fmt.Sprintf("mode=%s", perm)) } -func MountDataSize(size string) MountSourceOption { return MountData(fmt.Sprintf("size=%s", size)) } diff --git a/pkg/rootfs/options.go b/pkg/rootfs/options.go index b8ed412..737d420 100644 --- a/pkg/rootfs/options.go +++ b/pkg/rootfs/options.go @@ -5,21 +5,21 @@ import ( "context" "io/fs" "path/filepath" + + "github.com/cnk3x/xunlei/pkg/rootfs/sys" ) -const Separator = string(filepath.Separator) - -// func Force(force bool) Option { return func(ro *RunOptions) { ro.force = force } } func After(after func() error) Option { return func(ro *RunOptions) { ro.after = after } } + func Before(before func(ctx context.Context) error) Option { return func(ro *RunOptions) { ro.before = before } } func Link(source, target string, dirMode fs.FileMode, optional ...bool) Option { return func(ro *RunOptions) { - ro.links = append(ro.links, LinkOptions{ + ro.links = append(ro.links, sys.LinkOptions{ Target: target, - Source: []string{source}, + Source: source, DirMode: dirMode, Optional: cmp.Or(optional...), }) @@ -30,30 +30,42 @@ func LinkRoot(source, newRoot string, dirMode fs.FileMode, optional ...bool) Opt return Link(source, filepath.Join(newRoot, source), dirMode, optional...) } -func MountRoot(source, newRoot string, options ...MountOption) Option { - return func(ro *RunOptions) { - ro.mounts = append(ro.mounts, Mount(filepath.Join(newRoot, source), options...)) - } -} - -func MountBindRoot(source, newRoot string, options ...MountOption) Option { - return func(ro *RunOptions) { - ro.mounts = append(ro.mounts, - Mount( - filepath.Join(newRoot, source), - append(options, MountSource(source), MountBind())..., - ), - ) - } -} - func Basic(ro *RunOptions) { - optional := MountOptional(true) - ro.mounts = append(ro.mounts, - Mount(filepath.Join(ro.root, "proc"), MountType("proc")), // 挂载proc文件系统(必须) - Mount(filepath.Join(ro.root, "dev"), MountType("devtmpfs", MountDataMode("0755")), MountType("tmpfs", MountDataMode("0755"))), // 挂载devtmpfs到/dev(提供基础设备节点) - Mount(filepath.Join(ro.root, "sys"), MountType("sysfs"), optional), // 挂载sysfs文件系统(可选,但建议挂载) - Mount(filepath.Join(ro.root, "tmp"), MountType("tmpfs", MountDataSize("100m"), MountDataMode("0777")), optional), // 挂载tmpfs到/tmp(临时目录,可选) + // 挂载proc文件系统(必须) + sys.MountOptions{Target: filepath.Join(ro.root, "proc"), Source: "proc", Fstype: "proc"}, + // 挂载devtmpfs到/dev(提供基础设备节点) + sys.MountOptions{Target: filepath.Join(ro.root, "dev"), Source: "devtmpfs", Fstype: "devtmpfs", Data: "mode=0755"}, //tmpfs + // 挂载sysfs文件系统(可选,但建议挂载) + sys.MountOptions{Target: filepath.Join(ro.root, "sys"), Source: "sysfs", Fstype: "sysfs", Optional: true}, + // 挂载tmpfs到/tmp(临时目录,可选) + sys.MountOptions{Target: filepath.Join(ro.root, "tmp"), Source: "tmpfs", Fstype: "tmpfs", Data: "mode=0777,size=100m", Optional: true}, ) } + +func Links(root string, files ...string) Option { + return func(ro *RunOptions) { + for _, file := range files { + mOpts := sys.LinkOptions{ + Target: filepath.Join(root, file), + Source: file, + Optional: true, + DirMode: 0777, + } + ro.links = append(ro.links, mOpts) + } + } +} + +func Binds(root string, dirs ...string) func(ro *RunOptions) { + return func(ro *RunOptions) { + for _, dir := range dirs { + mOpts := sys.BindOptions{ + Target: filepath.Join(root, dir), + Source: dir, + Optional: true, + } + ro.binds = append(ro.binds, mOpts) + } + } +} diff --git a/pkg/rootfs/rootfs.go b/pkg/rootfs/rootfs.go index 4f228bd..7384cbb 100644 --- a/pkg/rootfs/rootfs.go +++ b/pkg/rootfs/rootfs.go @@ -1,62 +1,33 @@ package rootfs import ( - "cmp" "context" - "encoding/json" "errors" - "fmt" - "io/fs" "log/slog" - "os" "path/filepath" - "slices" - "strings" - "syscall" - "github.com/cnk3x/xunlei/pkg/fo" "github.com/cnk3x/xunlei/pkg/log" - "github.com/cnk3x/xunlei/pkg/utils" + "github.com/cnk3x/xunlei/pkg/rootfs/sys" "github.com/samber/lo" ) -type Undo func() +// type Undo func() type Option func(ro *RunOptions) +type Options []Option + +func (o Options) Add(options ...Option) Options { return append(o, options...) } + type RunOptions struct { root string - mounts []MountOptions - links []LinkOptions - // force bool - + mounts []sys.MountOptions + binds []sys.BindOptions + links []sys.LinkOptions before func(ctx context.Context) error after func() error } -type MountOptions struct { - Target string - Source []MountSourceOptions - Bind bool - Optional bool -} - -type MountSourceOptions struct { - Source string - Fstype string - Flags uintptr - Data string -} - -type LinkOptions struct { - Target string - Source []string - DirMode fs.FileMode - Optional bool -} - func Run(ctx context.Context, newRoot string, run func(ctx context.Context) error, options ...Option) (err error) { - ctx = log.Prefix(ctx, "boot") - slog.InfoContext(ctx, "start boot") defer func() { @@ -88,26 +59,26 @@ func Run(ctx context.Context, newRoot string, run func(ctx context.Context) erro // }() //undos - var undos []Undo - defer execUndo(makeUndos(&undos), nil) + var undos []sys.Undo + defer sys.ExecUndo(sys.Undos(&undos), nil) - var undo Undo + var undo sys.Undo //mkdirs - dirs := lo.Map(opts.mounts, func(m MountOptions, _ int) string { return m.Target }) - if undo, err = Mkdirs(ctx, dirs, 0777, true); err != nil { + dirs := lo.Map(opts.mounts, func(m sys.MountOptions, _ int) string { return m.Target }) + if undo, err = sys.Mkdirs(ctx, dirs, 0777); err != nil { return } undos = append(undos, undo) //mounts - if undo, err = mounts(ctx, opts.mounts); err != nil { + if undo, err = sys.Mounts(ctx, opts.mounts); err != nil { return } undos = append(undos, undo) //links - if undo, err = links(ctx, opts.links); err != nil { + if undo, err = sys.Links(ctx, opts.links); err != nil { return } undos = append(undos, undo) @@ -129,256 +100,6 @@ func Run(ctx context.Context, newRoot string, run func(ctx context.Context) erro } //chroot & run - err = chroot(log.Prefix(ctx, "prog"), opts.root, run) + err = sys.Chroot(log.Prefix(ctx, "prog"), opts.root, run) return } - -// chroot & run -func chroot(ctx context.Context, newRoot string, run func(ctx context.Context) error) (err error) { - wd, e := os.Getwd() - if err = e; err != nil { - return - } - - var rfd int - if rfd, err = syscall.Open("/", syscall.O_RDONLY, 0); err != nil { - return - } - - defer func() { - logWithErr(ctx, syscall.Close(rfd), "closeFd", "fd", rfd) - }() - - if err = logWithErr(ctx, syscall.Chdir(newRoot), "chdir", "dir", newRoot); err != nil { - return - } - - if err = logWithErr(ctx, syscall.Chroot("."), "chroot", "path", "."); err != nil { - return - } - - defer func() { - logWithErr(ctx, syscall.Fchdir(rfd), "fchdir", "fd", rfd) - logWithErr(ctx, syscall.Chroot("."), "chroot rollback", "path", ".") - logWithErr(ctx, syscall.Chdir(wd), "chdir", "dir", wd) - }() - - if err = logWithErr(ctx, syscall.Chdir("/"), "chdir", "dir", "/"); err != nil { - return - } - - //run - err = run(ctx) - return -} - -func Mkdirs(ctx context.Context, dirs []string, perm fs.FileMode, existsOk bool) (undo Undo, err error) { - var undos []Undo - undo = makeUndos(&undos) - defer execUndo(undo, &err) - for _, dir := range dirs { - u, e := Mkdir(ctx, dir, perm, existsOk) - if e != nil { - err = e - return - } - undos = append(undos, u) - } - return -} - -// 脱了裤子放个屁,为了能够方便回滚 -func Mkdir(ctx context.Context, dir string, perm fs.FileMode, existsOk bool) (undo Undo, err error) { - if dir, err = filepath.Abs(dir); err != nil { - return - } - - var undos []Undo - undo = makeUndos(&undos) - defer execUndo(undo, &err) - - vol := filepath.VolumeName(dir) - if vol != "" { - dir = dir[len(vol):] - } - dir = strings.Trim(dir, Separator) - - fields := strings.FieldsFunc(dir, func(r rune) bool { return r == filepath.Separator }) - for i := range fields { - full := filepath.Join(cmp.Or(vol, Separator), filepath.Join(fields[:i+1]...)) - switch stat, e := os.Stat(full); { - case e != nil && !os.IsNotExist(e): - err = e - case e != nil: - if err = os.Mkdir(full, perm); err != nil { - err = fmt.Errorf("mkdir fail: %w: %s", err, full) - } else { - slog.DebugContext(ctx, "mkdir", "path", full) - undos = append(undos, func() { logWithErr(ctx, os.Remove(full), "rmdir", "path", full) }) - } - case !stat.IsDir(): - err = fmt.Errorf("mkdir fail, parent is not a directory: %w: %s", fs.ErrExist, full) - case !existsOk: - err = fmt.Errorf("mkdir fail, parent is exists: %w: %s", fs.ErrExist, full) - } - - if err != nil { - slog.ErrorContext(ctx, "mkdir", "path", full, "err", err) - return - } - } - - return -} - -func mounts(ctx context.Context, mountPoints []MountOptions) (undo Undo, err error) { - var undos []Undo - undo = makeUndos(&undos) - defer execUndo(undo, &err) - - for _, m := range mountPoints { - r, e := mount(ctx, m) - if e != nil { - err = e - return - } - undos = append(undos, r) - } - return -} - -func mount(ctx context.Context, m MountOptions) (undo Undo, err error) { - defer func() { - if err != nil { - slog.DebugContext(ctx, string(utils.Eon(json.Marshal(m)))) - } - }() - - defer execUndo(undo, &err) - - if len(m.Source) == 0 { - slog.WarnContext(ctx, "mount", "target", m.Target, "optional", m.Optional, "err", "source not provided") - if !m.Optional { - err = fmt.Errorf("source not provided") - } - return - } - - for _, mm := range m.Source { - src := mm.Source - - flag := mm.Flags - if m.Bind { - if src, err = filepath.EvalSymlinks(src); err != nil { - continue - } - if flag == 0 { - flag = syscall.MS_BIND - // | syscall.MS_REC, //todo: 是否需要递归绑定? - } - } - - if err = syscall.Mount(src, m.Target, mm.Fstype, flag, mm.Data); err != nil { - slog.WarnContext(ctx, "mount", "target", m.Target, "source", src, "optional", m.Optional, "err", err) - continue - } - - unmount := func() error { return syscall.Unmount(m.Target, syscall.MNT_DETACH|syscall.MNT_FORCE) } - undo = func() { logWithErr(ctx, unmount(), "unmount", "target", m.Target) } - - slog.DebugContext(ctx, "mount", "target", m.Target, "source", src, "optional", m.Optional) - break - } - - return -} - -func links(ctx context.Context, links []LinkOptions) (undo Undo, err error) { - var undos []Undo - undo = makeUndos(&undos) - defer execUndo(undo, &err) - - for _, l := range links { - for _, source := range l.Source { - r, e := link(ctx, source, l.Target, l.DirMode) - if e != nil { - err = e - return - } - undos = append(undos, r) - } - } - - return -} - -// link hard link, only for file -func link(ctx context.Context, source string, linkFile string, dirMode fs.FileMode) (undo Undo, err error) { - var undos []Undo - undo = makeUndos(&undos) - defer execUndo(undo, &err) - - var realPath string - if realPath, err = filepath.EvalSymlinks(source); err != nil { - return - } - - var dirUndo Undo - if dirUndo, err = Mkdir(ctx, filepath.Dir(linkFile), dirMode, true); err != nil { - return - } - undos = append(undos, dirUndo) - - op := "link" - if err = os.Link(realPath, linkFile); errors.Is(err, syscall.EXDEV) { - op, err = "copy", fileCopy(realPath, linkFile) - } - - logWithErr(ctx, err, "link", "op", op, "link", linkFile, "source", source, "realpath", realPath) - undos = append(undos, func() { logWithErr(ctx, os.Remove(linkFile), "unlink", "link", linkFile) }) - return -} - -func fileCopy(source, target string) error { - return fo.OpenRead(source, func(src *os.File) (err error) { return fo.OpenWrite(target, fo.From(src), fo.PermFrom(src)) }) -} - -func iif[T any](c bool, t, f T) T { - if c { - return t - } - return f -} - -func execUndo(undo Undo, err *error) { - if err == nil || *err != nil && undo != nil { - undo() - } -} - -func makeUndos(undos *[]Undo) (undo Undo) { - return func() { - if undos == nil || len(*undos) == 0 { - return - } - for _, undo := range slices.Backward(*undos) { - if undo != nil { - undo() - } - } - } -} - -func logWithErr(ctx context.Context, err error, msg string, args ...any) error { - switch { - case errors.Is(err, syscall.ENOTEMPTY): - slog.Log(ctx, slog.LevelDebug, msg, append(args, "err", "skip because not empty")...) - case errors.Is(err, context.Canceled): - slog.Log(ctx, slog.LevelDebug, msg, append(args, "err", context.Cause(ctx))...) - case err != nil: - slog.Log(ctx, slog.LevelWarn, msg, append(args, "err", err)...) - default: - slog.Log(ctx, slog.LevelDebug, msg, args...) - } - return err -} diff --git a/pkg/rootfs/sys/bind.go b/pkg/rootfs/sys/bind.go new file mode 100644 index 0000000..7b0c053 --- /dev/null +++ b/pkg/rootfs/sys/bind.go @@ -0,0 +1,39 @@ +package sys + +import ( + "context" + "log/slog" + "path/filepath" + "syscall" +) + +type BindOptions struct { + Source string + Target string + Optional bool +} + +func Binds(ctx context.Context, items []BindOptions) (undo Undo, err error) { + return doMulti(ctx, items, Bind) +} + +// 绑定文件夹 +func Bind(ctx context.Context, m BindOptions) (undo Undo, err error) { + src := m.Source + if src, err = filepath.EvalSymlinks(src); err == nil { + err = syscall.Mount(src, m.Target, "", syscall.MS_BIND, "") + } + + if err == nil { + undo = mkUnmount(ctx, m.Target, "unbind") + } + + attrs := []slog.Attr{ + slog.String("target", m.Target), + slog.String("source", m.Source), + slog.String("source_real", src), + } + + err = logIt(ctx, err, m.Optional, "bind", attrs...) + return +} diff --git a/pkg/rootfs/sys/chroot.go b/pkg/rootfs/sys/chroot.go new file mode 100644 index 0000000..9ce0dc4 --- /dev/null +++ b/pkg/rootfs/sys/chroot.go @@ -0,0 +1,39 @@ +package sys + +import ( + "context" + "os" + "syscall" +) + +// chroot & run +func Chroot(ctx context.Context, newRoot string, run func(ctx context.Context) error) (err error) { + wd, e := os.Getwd() + if err = e; err != nil { + return + } + + if err = syscall.Chdir(newRoot); err != nil { + return + } + defer syscall.Chdir(wd) + + var rfd int + if rfd, err = syscall.Open("/", syscall.O_RDONLY, 0); err != nil { + return + } + defer syscall.Close(rfd) + + if err = syscall.Chroot("."); err != nil { + return + } + defer syscall.Chroot(".") + defer syscall.Fchdir(rfd) + + if err = syscall.Chdir("/"); err != nil { + return + } + + err = run(ctx) + return +} diff --git a/pkg/rootfs/sys/link.go b/pkg/rootfs/sys/link.go new file mode 100644 index 0000000..bd04cb2 --- /dev/null +++ b/pkg/rootfs/sys/link.go @@ -0,0 +1,88 @@ +package sys + +import ( + "context" + "errors" + "io/fs" + "log/slog" + "os" + "path/filepath" + "syscall" + + "github.com/cnk3x/xunlei/pkg/fo" +) + +type LinkOptions struct { + Target string + Source string + DirMode fs.FileMode + Optional bool + Copy bool +} + +func Links(ctx context.Context, links []LinkOptions) (undo Undo, err error) { + var undos []Undo + undo = Undos(&undos) + defer ExecUndo(undo, &err) + for _, m := range links { + u, e := Link(ctx, m) + if err = e; e != nil { + return + } + undos = append(undos, u) + } + return +} + +// link hard link, only for file +func Link(ctx context.Context, m LinkOptions) (undo Undo, err error) { + var undos []Undo + undo = Undos(&undos) + defer ExecUndo(undo, &err) + + real, e := filepath.EvalSymlinks(m.Source) + if err = e; err != nil { + return + } + + dirUndo, e := Mkdir(ctx, filepath.Dir(m.Target), m.DirMode) + if err = e; err != nil { + return + } + undos = append(undos, dirUndo) + + err = os.Link(real, m.Target) + if errors.Is(err, syscall.EXDEV) { + err = fo.OpenRead(real, func(src *os.File) (err error) { + return fo.OpenWrite(m.Target, fo.From(src), fo.PermFrom(src), fo.FlagExcl) + }) + } + + attrs := []slog.Attr{ + slog.String("target", m.Target), + slog.String("source", m.Source), + slog.String("source_real", real), + slog.Bool("optional", m.Optional), + } + + if err != nil { + attrs = append(attrs, slog.String("err", err.Error())) + } + + switch { + case os.IsExist(err): + slog.LogAttrs(ctx, slog.LevelDebug, "link skip", attrs...) + case err != nil && m.Optional: + slog.LogAttrs(ctx, slog.LevelDebug, "link skip", attrs...) + case err != nil && !m.Optional: + slog.LogAttrs(ctx, slog.LevelWarn, "link fail", attrs...) + default: + slog.LogAttrs(ctx, slog.LevelDebug, "link done", attrs...) + undos = append(undos, newRm(ctx, m.Target, "unlink")) + } + + if os.IsExist(err) { + err = nil + } + return +} diff --git a/pkg/rootfs/sys/mkdir.go b/pkg/rootfs/sys/mkdir.go new file mode 100644 index 0000000..4d49596 --- /dev/null +++ b/pkg/rootfs/sys/mkdir.go @@ -0,0 +1,93 @@ +package sys + +import ( + "cmp" + "context" + "fmt" + "io/fs" + "log/slog" + "os" + "path/filepath" + "strings" +) + +func Mkdirs(ctx context.Context, dirs []string, perm fs.FileMode) (undo Undo, err error) { + return doMulti(ctx, dirs, + func(ctx context.Context, dir string) (Undo, error) { + return Mkdir(ctx, dir, perm) + }, + ) +} + +// 脱了裤子放个屁,为了能够方便回滚 +func Mkdir(ctx context.Context, dir string, perm fs.FileMode) (undo Undo, err error) { + if dir, err = filepath.Abs(dir); err != nil { + return + } + + var undos []Undo + undo = Undos(&undos) + defer ExecUndo(undo, &err) + + vol := cmp.Or(filepath.VolumeName(dir), "/") + items := strings.FieldsFunc(strings.TrimPrefix(dir, vol), func(r rune) bool { return r == '/' }) + + var ok bool + for i := range items { + ok, err = mkdir(filepath.Join(vol, filepath.Join(items[:i+1]...)), perm) + } + + switch { + case err != nil: + slog.WarnContext(ctx, "mkdir fail", "dir", dir, "err", err) + return + case ok: + undos = append(undos, newRm(ctx, dir, "rmdir")) + slog.DebugContext(ctx, "mkdir done", "dir", dir) + // default: + // slog.DebugContext(ctx, "mkdir skip", "dir", dir) + } + + return +} + +func newRm(ctx context.Context, target, act string) func() { + return func() { + err := os.Remove(target) + if err != nil { + slog.LogAttrs(ctx, slog.LevelWarn, act, slog.String("target", target), slog.String("err", err.Error())) + return + } + slog.LogAttrs(ctx, slog.LevelDebug, act, slog.String("target", target)) + } +} + +func mkdir(dir string, perm fs.FileMode) (ok bool, err error) { + stat, e := os.Stat(dir) + //存在 + if err = e; err == nil { + if stat.IsDir() { //目录存在, 跳过 + return + } + + if stat.Mode()&os.ModeSymlink != 0 { //软链接, 删除 + err = os.Remove(dir) + } else { //文件存在, 报错 + err = fmt.Errorf("%w: %s", os.ErrExist, dir) + } + + if err != nil { + return + } + } + + //非文件不存在, 报错 + if !os.IsNotExist(err) { + return + } + + //创建目录, 上级目录存在 + err = os.Mkdir(dir, perm) + ok = err == nil + return +} diff --git a/pkg/rootfs/sys/mount.go b/pkg/rootfs/sys/mount.go new file mode 100644 index 0000000..a0bf09b --- /dev/null +++ b/pkg/rootfs/sys/mount.go @@ -0,0 +1,54 @@ +package sys + +import ( + "context" + "encoding/json" + "log/slog" + "syscall" + + "github.com/cnk3x/xunlei/pkg/utils" +) + +type MountOptions struct { + Target string + Source string + Fstype string + Flags uintptr + Data string + Optional bool +} + +func Mounts(ctx context.Context, mounts []MountOptions) (undo Undo, err error) { + return doMulti(ctx, mounts, Mount) +} + +// 完整的绑定 +func Mount(ctx context.Context, m MountOptions) (undo Undo, err error) { + defer func() { + if err != nil { + slog.DebugContext(ctx, string(utils.Eon(json.Marshal(m)))) + } + }() + + err = syscall.Mount(m.Source, m.Target, m.Fstype, m.Flags, m.Data) + if err == nil { + undo = mkUnmount(ctx, m.Target, "unmount") + } + + err = logIt(ctx, err, m.Optional, "mount", + slog.String("target", m.Target), + slog.String("source", m.Source), + slog.Bool("optional", m.Optional)) + return +} + +func mkUnmount(ctx context.Context, target, act string) Undo { + return func() { + err := syscall.Unmount(target, syscall.MNT_DETACH|syscall.MNT_FORCE) + if err != nil { + slog.LogAttrs(ctx, slog.LevelWarn, act, slog.String("target", target), slog.String("err", err.Error())) + return + } + slog.LogAttrs(ctx, slog.LevelDebug, act, slog.String("target", target)) + } +} diff --git a/pkg/rootfs/sys/sys.go b/pkg/rootfs/sys/sys.go new file mode 100644 index 0000000..47eb1d4 --- /dev/null +++ b/pkg/rootfs/sys/sys.go @@ -0,0 +1,58 @@ +package sys + +import ( + "context" + "log/slog" + "slices" + + "github.com/cnk3x/xunlei/pkg/utils" +) + +type Undo = func() + +func doMulti[O any](ctx context.Context, items []O, itemFn func(context.Context, O) (Undo, error)) (undo Undo, err error) { + var undos []Undo + undo = Undos(&undos) + defer ExecUndo(undo, &err) + for _, item := range items { + u, e := itemFn(ctx, item) + if err = e; e != nil { + return + } + undos = append(undos, u) + } + return +} + +func logIt(ctx context.Context, err error, optional bool, name string, attrs ...slog.Attr) error { + if err != nil { + attrs = append(attrs, slog.String("err", err.Error())) + if optional { + slog.LogAttrs(ctx, slog.LevelDebug, name+" skip", attrs...) + } else { + slog.LogAttrs(ctx, slog.LevelWarn, name+" fail", attrs...) + } + } else { + slog.LogAttrs(ctx, slog.LevelDebug, name+" done", attrs...) + } + return utils.Iif(optional, nil, err) +} + +func ExecUndo(undo Undo, err *error) { + if err == nil || *err != nil && undo != nil { + undo() + } +} + +func Undos(undos *[]Undo) (undo Undo) { + return func() { + if undos == nil || len(*undos) == 0 { + return + } + for _, undo := range slices.Backward(*undos) { + if undo != nil { + undo() + } + } + } +} diff --git a/pkg/utils/arrs.go b/pkg/utils/arrs.go index 5c4f3f8..6d5e2c4 100644 --- a/pkg/utils/arrs.go +++ b/pkg/utils/arrs.go @@ -1,6 +1,12 @@ package utils -import "cmp" +import ( + "bufio" + "cmp" + "io" + "iter" + "log" +) func CompactUniq[Slice ~[]T, T comparable](s Slice, inplace ...bool) Slice { result := s @@ -54,3 +60,43 @@ func Flat[T any](s [][]T) []T { l := Reduce(s, func(agg int, item []T, _ int) int { return agg + len(item) }, 0) return Reduce(s, func(agg []T, item []T, _ int) []T { return append(agg, item...) }, make([]T, 0, l)) } + +func ReduceSeq2[T, R any](seq iter.Seq2[int, T], walk func(agg R, item T, index int) R, init R) R { + for i, item := range seq { + init = walk(init, item, i) + } + return init +} + +func ReduceSeq[T, R any](seq iter.Seq[T], walk func(agg R, item T) R, init R) R { + for item := range seq { + init = walk(init, item) + } + return init +} + +func LineSeq(r io.Reader) iter.Seq[string] { + return func(yield func(string) bool) { + for scan := bufio.NewScanner(r); scan.Scan(); { + if !yield(scan.Text()) { + break + } + } + } +} + +func LineWalk(r io.Reader, f func(s string)) { + for scan := bufio.NewScanner(r); scan.Scan(); { + f(scan.Text()) + } +} + +func LineWriter(lineRead func(line string)) io.WriteCloser { + r, w := io.Pipe() + go LineWalk(r, lineRead) + return w +} + +func LogStd(w io.Writer) *log.Logger { return log.New(w, "", 0) } + +func Array[T any](s ...T) []T { return s } diff --git a/pkg/utils/chan.go b/pkg/utils/chan.go index 561098f..72c6331 100644 --- a/pkg/utils/chan.go +++ b/pkg/utils/chan.go @@ -1,11 +1,10 @@ package utils import ( - "reflect" "time" ) -// SelectOnce select `cSelect` chan, return it +// SelectOnce select `cSelect` chan, return it (blocked) // // v: if recv from cSelect // ok: if cSelect is not closed @@ -16,31 +15,89 @@ import ( // select{ // case <- cBreaks[...]: // v = nil, ok = false, breaked = true -// case v, ok:= cSeelct: +// case v, ok:= cSelect: // v = v, ok = ok, breaked = false // } -func SelectOnce[T any](cSelect <-chan T, cBreaks ...<-chan struct{}) (v T, ok, breaked bool) { - c2case := func(c <-chan struct{}, _ int) reflect.SelectCase { - return reflect.SelectCase{Dir: reflect.SelectRecv, Chan: reflect.ValueOf(c)} +func CSelect[T any](cSelect <-chan T, cBreaks ...<-chan struct{}) (v T, ok, breaked bool) { + cBreaked := make(chan struct{}) + + for _, c := range cBreaks { + go func() { + select { + case <-cBreaked: + case <-c: + close(cBreaked) + } + }() } - cases := append(Map(cBreaks, c2case), reflect.SelectCase{Dir: reflect.SelectRecv, Chan: reflect.ValueOf(cSelect)}) - if chosen, x, ok := reflect.Select(cases); chosen == len(cases)-1 { - v, _ = x.Interface().(T) - return v, ok, false + + select { + case <-cBreaked: + breaked = true + case v, ok = <-cSelect: + close(cBreaked) } - return v, false, true + return } -// SelectDo do if recv from sSelect -func SelectDo[T any](cSelect <-chan T, okFunc func(), cBreaks ...<-chan struct{}) { - if _, _, breaked := SelectOnce(cSelect, cBreaks...); !breaked { - okFunc() +// SelectDo do if recv from sSelect (blocked) +func SelectDo[T any](cSelect <-chan T, f func(T, bool), cBreaks ...<-chan struct{}) (breaked bool) { + cBreaked := make(chan struct{}) + + for _, c := range cBreaks { + go func() { + select { + case <-cBreaked: + case <-c: + close(cBreaked) + } + }() + } + + select { + case <-cBreaked: + return true + case v, ok := <-cSelect: + close(cBreaked) + f(v, ok) + return false } } +// Sleep breakable sleep (blocked) func Sleep(d time.Duration, cBreaks ...<-chan struct{}) (breaked bool) { t := time.NewTimer(d) defer t.Stop() - _, _, breaked = SelectOnce(t.C, cBreaks...) + _, _, breaked = CSelect(t.C, cBreaks...) return } + +// After do when done is closed (unblocked) +func After[T any, F ft[T]](done <-chan T, f F, cBreaks ...<-chan struct{}) { + go SelectDo(done, func(t T, ok bool) { + af := any(f) + if ft, ok := af.(func()); ok { + ft() + return + } + + if ft, ok := af.(func() error); ok { + _ = ft() + return + } + + if ft, ok := af.(func(T)); ok { + ft(t) + return + } + + if ft, ok := af.(func(T) error); ok { + _ = ft(t) + return + } + }, cBreaks...) +} + +type ft[T any] interface { + ~func() | ~func() error | ~func(T) | ~func(T) error +} diff --git a/pkg/utils/func.go b/pkg/utils/func.go index dcac2be..bf187b2 100644 --- a/pkg/utils/func.go +++ b/pkg/utils/func.go @@ -7,3 +7,8 @@ func Fig[I, T, R any](f func(T) R) func(T, I) R { func Fig2[I, T1, T2, R any](f func(T1, T2) R) func(T1, T2, I) R { return func(t1 T1, t2 T2, _ I) R { return f(t1, t2) } } + +func FIdx[T, R any](f func(T) R) func(T, int) R { return Fig[int](f) } + +func Fe(f func()) error { f(); return nil } +func Fne[E any](f func() E) func() { return func() { _ = f() } } diff --git a/pkg/utils/human.go b/pkg/utils/human.go index 50cc0fd..e7cd0c2 100644 --- a/pkg/utils/human.go +++ b/pkg/utils/human.go @@ -7,7 +7,7 @@ import ( "strconv" ) -func HumanBytes[T uintT | intT](n T, prec ...int) string { +func HumanBytes[T UintT | IntT](n T, prec ...int) string { if f := float64(n); f >= 1024 { for i, u := range slices.Backward([]rune("KMGTE")) { if base := float64(int64(1) << (10 * (i + 1))); float64(n) >= base { diff --git a/pkg/utils/parse.go b/pkg/utils/parse.go index 9c238d3..f509b65 100644 --- a/pkg/utils/parse.go +++ b/pkg/utils/parse.go @@ -2,15 +2,15 @@ package utils import "strconv" -type uintT interface { +type UintT interface { ~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 } -type intT interface { +type IntT interface { ~int | ~int8 | ~int16 | ~int32 | ~int64 } -func StrParser[R uintT | intT, T1, T2 any, R64 uint64 | int64](f func(string, T1, T2) (R64, error), t1 T1, t2 T2) func(string) (R, error) { +func StrParser[R UintT | IntT, T1, T2 any, R64 uint64 | int64](f func(string, T1, T2) (R64, error), t1 T1, t2 T2) func(string) (R, error) { return func(s string) (R, error) { r, err := f(s, t1, t2) if err != nil { @@ -32,3 +32,7 @@ var ( ParseInt32 = StrParser[int32](strconv.ParseInt, 0, 32) ParseInt64 = StrParser[int64](strconv.ParseInt, 0, 64) ) + +func String[T IntT | UintT](v T) string { + return strconv.FormatInt(int64(v), 10) +} diff --git a/pkg/utils/utils.go b/pkg/utils/utils.go index 91b275a..320d8da 100644 --- a/pkg/utils/utils.go +++ b/pkg/utils/utils.go @@ -9,10 +9,6 @@ func Iif[T any](c bool, t, f T) T { return f } -func Eol[T any](_ T, err error) error { return err } - -func Eon[T any, E any](v T, _ E) T { return v } - func First[T any](v []T) T { return FirstOr(v) } func FirstOr[T any](v []T, def ...T) T { if len(v) > 0 { @@ -31,3 +27,6 @@ func HasPrefix(s, prefix string, ignoreCase ...bool) bool { } return strings.HasPrefix(s, prefix) } + +func Eol[T any](_ T, err error) error { return err } +func Eon[T any, E any](v T, _ E) T { return v } diff --git a/pkg/web/web.go b/pkg/web/web.go new file mode 100644 index 0000000..257f946 --- /dev/null +++ b/pkg/web/web.go @@ -0,0 +1,239 @@ +// Package web provides a simple HTTP multiplexer with middleware support. +package web + +import ( + "cmp" + "context" + "errors" + "fmt" + "log/slog" + "net" + "net/http" + "slices" + "strings" + + "github.com/cnk3x/xunlei/pkg/utils" +) + +// Processor 定义中间件处理器函数类型,接收下一个处理器并返回包装后的处理器 +type Processor func(next http.Handler) http.Handler + +// Mux 表示一个HTTP请求多路复用器,支持中间件处理链 +type Mux struct { + cur *http.ServeMux // 当前多路复用器 + parent *Mux // 父级多路复用器,用于嵌套结构 + processors []Processor // 处理器链(中间件) + onShutdown func() // 关闭时执行的回调函数 +} + +// Router 接口定义路由的基本操作 +type Router interface { + Handle(pattern string, handler http.Handler) +} + +// NewMux 创建一个新的Mux实例,默认包含Recoverer中间件 +func NewMux() *Mux { + return &Mux{cur: http.NewServeMux()} +} + +// OnShutDown 设置服务器关闭时的回调函数 +// - fn: 服务器关闭时执行的函数 +func (mux *Mux) OnShutDown(fn func()) { mux.onShutdown = fn } + +// With 创建一个新的Mux实例,继承当前实例的父级和处理器,并可以添加新的处理器 +// - processor: 要添加的处理器列表 +// +// 返回一个新的Mux实例,其父级指向当前实例 +func (mux *Mux) With(processor ...Processor) *Mux { return &Mux{parent: mux} } + +func (mux *Mux) BasicAuth(user, pwd string) *Mux { return mux.With(BasicAuth(user, pwd)) } + +// Use 向当前mux添加中间件处理器到处理器链中 +// - processor: 要添加的处理器列表 +func (mux *Mux) Use(processor ...Processor) { mux.processors = append(mux.processors, processor...) } + +func (mux *Mux) UseBasicAuth(user, pwd string) { mux.Use(BasicAuth(user, pwd)) } + +func (mux *Mux) UseRecoverer() { mux.Use(Recoverer) } + +// Handle 注册HTTP处理器,根据是否有父级mux来决定注册位置 +// - pattern: 请求路径模式 +// - handler: HTTP处理器 +// - processors: 额外的处理器(中间件) +func (mux *Mux) Handle(pattern string, handler http.Handler) { + if mux.parent != nil { + mux.parent.Handle(pattern, applyProcessors(handler, mux.processors...)) + } else { + mux.cur.Handle(pattern, applyProcessors(handler, mux.processors...)) + } +} + +// Route 注册一个带有前缀模式和处理器的路由,允许通配符匹配。 +// 如果前缀不以'*'结尾,则根据情况添加'*'或'/*'以实现路径前缀匹配。 +// - prefix: 要匹配的路由前缀(可选尾随'*') +// - handler: 路由匹配时执行的HTTP处理器 +func (mux *Mux) Route(prefix string, handler http.Handler) { + if !strings.HasSuffix(prefix, "*") { + if strings.HasSuffix(prefix, "/") { + prefix += "*" + } else { + prefix += "/*" + } + } + mux.Handle(prefix, handler) +} + +// Get 注册GET请求处理器 +// - pattern: GET请求路径模式 +// - handler: HTTP处理器 +// - processors: 额外的处理器(中间件) +func (mux *Mux) Get(pattern string, handler http.Handler) { + mux.Handle("GET "+pattern, handler) +} + +// Post 注册POST请求处理器 +// - pattern: POST请求路径模式 +// - handler: HTTP处理器 +// - processors: 额外的处理器(中间件) +func (mux *Mux) Post(pattern string, handler http.Handler) { + mux.Handle("POST "+pattern, handler) +} + +// ServeHTTP 实现http.Handler接口,处理HTTP请求 +func (mux *Mux) ServeHTTP(w http.ResponseWriter, r *http.Request) { + cmp.Or[http.Handler](mux.parent, mux.cur).ServeHTTP(w, r) +} + +// Run 启动HTTP服务器并监听指定地址 +// - ctx: 上下文对象 +// - addr: 监听地址 +// +// 返回启动错误(如果有的话) +func (mux *Mux) Run(ctx context.Context, addr string) (err error) { + s := &http.Server{ + Addr: addr, Handler: cmp.Or(mux.parent, mux), + BaseContext: func(l net.Listener) context.Context { + slog.InfoContext(ctx, "web started", "listen", l.Addr().String()) + return ctx + }, + } + + if err = s.ListenAndServe(); errors.Is(err, http.ErrServerClosed) { + err = nil + } + + if mux.onShutdown != nil { + mux.onShutdown() + } + return +} + +// Start 在后台启动HTTP服务器 +// - ctx: 上下文对象 +// - addr: 监听地址 +// +// 返回完成通道,当服务器停止时该通道会被关闭 +func (mux *Mux) Start(ctx context.Context, addr string) (done <-chan struct{}) { + webDone := make(chan struct{}) + go func() { + defer close(webDone) + if err := mux.Run(ctx, addr); err != nil { + slog.ErrorContext(ctx, "web is done!", "err", err) + } else { + slog.InfoContext(ctx, "web is done!") + } + }() + return webDone +} + +// Redirect 创建重定向处理器 +// - to: 重定向目标URL +// - permanent: 是否永久重定向,默认临时重定向 +// +// 返回HTTP处理器函数 +func Redirect(to string, permanent ...bool) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + http.Redirect(w, r, to, utils.Iif(cmp.Or(permanent...), http.StatusPermanentRedirect, http.StatusTemporaryRedirect)) + } +} + +// Blob 创建返回字节切片或字符串内容的处理器 +// - body: 响应体内容 +// - contentType: 内容类型 +// - status: HTTP状态码 +// +// 返回HTTP处理器函数 +func Blob[T ~[]byte | ~string](body T, contentType string, status int) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", contentType) + w.WriteHeader(status) + w.Write([]byte(body)) + } +} + +// Recoverer 中间件,从panic中恢复并记录日志 +func Recoverer(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + defer func() { + if rvr := recover(); rvr != nil { + if rvr == http.ErrAbortHandler { + slog.DebugContext(r.Context(), "abort") + // panic(rvr) + } + if r.Header.Get("Connection") != "Upgrade" { + w.WriteHeader(http.StatusInternalServerError) + } + } + }() + next.ServeHTTP(w, r) + }) +} + +// BasicAuth 创建基础认证中间件 +// - username: 用户名 +// - password: 密码 +// +// 返回认证处理器 +func BasicAuth(username, password string) Processor { + if password != "" { + if username == "" { + username = "admin" + } + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + user, pass, ok := r.BasicAuth() + if !ok || user != username || pass != password { + w.Header().Add("WWW-Authenticate", fmt.Sprintf(`Basic realm="%s"`, "xlp")) + w.WriteHeader(http.StatusUnauthorized) + return + } + next.ServeHTTP(w, r) + }) + } + } + return func(next http.Handler) http.Handler { return next } +} + +// Address 组合IP地址和端口为网络地址字符串 +// - ip: IP地址 +// - port: 端口号 +// +// 返回组合的网络地址字符串 +func Address[T utils.UintT | utils.IntT](ip net.IP, port T) string { + sIp := ip.String() + return net.JoinHostPort(utils.Iif(sIp == "", "", sIp), utils.String(port)) +} + +// applyProcessors 按逆序应用处理器链 +// - h: 初始处理器 +// - mw: 处理器列表 +// +// 返回包装后的处理器 +func applyProcessors(h http.Handler, mw ...Processor) http.Handler { + for _, m := range slices.Backward(mw) { + if m != nil { + h = m(h) + } + } + return h +} diff --git a/spk/spk.go b/spk/spk.go index 5275fdb..d0f2d29 100644 --- a/spk/spk.go +++ b/spk/spk.go @@ -16,7 +16,7 @@ import ( ) // Extract 从迅雷SPK中提取需要的文件(存在则跳过) -func Extract(ctx context.Context, src io.Reader, dstDir string, overwrite bool) (err error) { +func Extract(ctx context.Context, src io.Reader, dstDir string) (err error) { return Walk(ctx, src, func(tr io.Reader, h *tar.Header) (err error) { if h.Name == "package.tgz" { err = cmp.Or(Walk(ctx, tr, func(tr io.Reader, h *tar.Header) (err error) { @@ -25,26 +25,20 @@ func Extract(ctx context.Context, src io.Reader, dstDir string, overwrite bool) case strings.HasPrefix(h.Name, "bin/bin/version"): perm = 0o666 case strings.HasPrefix(h.Name, "bin/bin/xunlei-pan-cli"): - perm = fs.ModePerm + perm = 0o777 case h.Name == "ui/index.cgi": - perm = fs.ModePerm + perm = 0o777 default: return } err = func() (err error) { target := filepath.Join(dstDir, h.Name) - if err = os.MkdirAll(filepath.Dir(target), 0755); err != nil { + if err = os.MkdirAll(filepath.Dir(target), 0o777); err != nil { return } - flag := os.O_RDWR | os.O_CREATE - if overwrite { - flag |= os.O_TRUNC - } else { - flag |= os.O_EXCL - } - f, e := os.OpenFile(target, flag, perm) + f, e := os.OpenFile(target, os.O_RDWR|os.O_CREATE|os.O_TRUNC, perm) if e != nil { if !os.IsExist(e) { err = e diff --git a/spk/url.go b/spk/url.go index bec31ff..adedc6c 100644 --- a/spk/url.go +++ b/spk/url.go @@ -2,6 +2,7 @@ package spk import ( "context" + "crypto/tls" "fmt" "log/slog" "net/http" @@ -37,15 +38,15 @@ func Download(ctx context.Context, spkUrl string, dir string, force bool) (err e } func download_file(ctx context.Context, spkUrl string, dir string) (err error) { + spkUrl = strings.TrimPrefix(spkUrl, "file://") slog.InfoContext(ctx, "download spk file", "url", spkUrl) - spkUrl = strings.TrimSuffix(spkUrl, "file://") f, e := os.Open(spkUrl) if err = e; err != nil { return } defer f.Close() - err = Extract(ctx, f, dir, true) + err = Extract(ctx, f, dir) return } @@ -55,17 +56,23 @@ func download_http(ctx context.Context, spkUrl string, dir string) (err error) { if req, err = http.NewRequestWithContext(ctx, http.MethodGet, spkUrl, nil); err != nil { return } - req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.0.0 Safari/537.36 Edg/143.0.0.0") - req.Header.Set("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7") - req.Header.Set("Accept-Language", "zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6,zh-TW;q=0.5") + req.Header.Set("accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7") + req.Header.Set("accept-encoding", "gzip, deflate, br, zstd") + req.Header.Set("accept-language", "zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6,zh-TW;q=0.5") + req.Header.Set("cache-control", "no-cache") + req.Header.Set("dnt", "1") + req.Header.Set("pragma", "no-cache") + req.Header.Set("priority", "u=0, i") + req.Header.Set("user-agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.0.0 Safari/537.36 Edg/143.0.0.0") + cli := &http.Client{Transport: &http.Transport{TLSClientConfig: &tls.Config{InsecureSkipVerify: true}}} var resp *http.Response - if resp, err = http.DefaultClient.Do(req); err != nil { + if resp, err = cli.Do(req); err != nil { return } defer resp.Body.Close() - err = Extract(ctx, resp.Body, dir, true) + err = Extract(ctx, resp.Body, dir) return } diff --git a/ubuntu.Dockerfile b/ubuntu.Dockerfile index a990f57..0a48b8f 100644 --- a/ubuntu.Dockerfile +++ b/ubuntu.Dockerfile @@ -5,20 +5,21 @@ LABEL org.opencontainers.image.authors=cnk3x LABEL org.opencontainers.image.source=https://github.com/cnk3x/xunlei RUN apt update && apt install --no-install-recommends -y ca-certificates tzdata && rm -rf /var/lib/apt/lists/* && \ - rm -f /etc/localtime /etc/timezone && \ - cp -Lr /usr/share/zoneinfo/Asia/Chongqing /etc/localtime && \ - echo "Asia/Chongqing" >/etc/timezone + rm -f /etc/localtime /etc/timezone && \ + cp -Lr /usr/share/zoneinfo/Asia/Chongqing /etc/localtime && \ + echo "Asia/Chongqing" >/etc/timezone -COPY bin/xlp-${TARGETARCH} /xlp +COPY artifacts/xlp-${TARGETARCH} /xlp ENV XL_DASHBOARD_PORT=2345 \ - XL_DASHBOARD_IP= \ - XL_DASHBOARD_USERNAME= \ - XL_DIR_DOWNLOAD=/xunlei/downloads \ - XL_PREVENT_UPDATE= \ - XL_SPK_URL= \ - XL_UID= \ - XL_GID= \ - XL_DEBUG= + XL_DASHBOARD_IP= \ + XL_DASHBOARD_USERNAME= \ + XL_DIR_DOWNLOAD=/xunlei/downloads \ + XL_PREVENT_UPDATE= \ + XL_SPK_URL= \ + XL_UID= \ + XL_GID= \ + XL_DEBUG= CMD [ "/xlp" ] + diff --git a/web.go b/web.go deleted file mode 100644 index e30da27..0000000 --- a/web.go +++ /dev/null @@ -1,72 +0,0 @@ -package xunlei - -import ( - "cmp" - "fmt" - "net/http" - - "github.com/cnk3x/xunlei/pkg/utils" -) - -func webRedirect(to string, permanent ...bool) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - http.Redirect(w, r, to, utils.Iif(cmp.Or(permanent...), http.StatusPermanentRedirect, http.StatusTemporaryRedirect)) - } -} - -func webBlob[T ~[]byte | ~string](body T, contentType string, status int) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", contentType) - w.WriteHeader(status) - w.Write([]byte(body)) - } -} - -type webMw = func(next http.Handler) http.Handler - -func webRecoverer(next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - defer func() { - if rvr := recover(); rvr != nil { - if rvr == http.ErrAbortHandler { - panic(rvr) - } - if r.Header.Get("Connection") != "Upgrade" { - w.WriteHeader(http.StatusInternalServerError) - } - } - }() - next.ServeHTTP(w, r) - }) -} - -// func webChain(h http.Handler, mws ...webMw) http.Handler { -// for _, mw := range slices.Backward(mws) { -// h = mw(h) -// } -// return h -// } - -// func webHandle(mux *http.ServeMux, pattern string, h http.Handler, mws ...webMw) { -// mux.Handle(pattern, webChain(h, slices.Insert(mws, 0, webRecoverer)...)) -// } - -func webBasicAuth(username, password string) webMw { - if password != "" { - if username == "" { - username = "admin" - } - return func(next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - user, pass, ok := r.BasicAuth() - if !ok || user != username || pass != password { - w.Header().Add("WWW-Authenticate", fmt.Sprintf(`Basic realm="%s"`, "xlp")) - w.WriteHeader(http.StatusUnauthorized) - return - } - next.ServeHTTP(w, r) - }) - } - } - return func(next http.Handler) http.Handler { return next } -} diff --git a/xlp.go b/xlp.go index 8fef4e1..7d1d3dc 100644 --- a/xlp.go +++ b/xlp.go @@ -6,13 +6,11 @@ import ( "fmt" "io/fs" "log/slog" - "net" "net/http" "net/http/cgi" "os" "path/filepath" "runtime" - "strconv" "strings" "syscall" @@ -20,7 +18,7 @@ import ( "github.com/cnk3x/xunlei/pkg/fo" "github.com/cnk3x/xunlei/pkg/log" "github.com/cnk3x/xunlei/pkg/utils" - "github.com/go-chi/chi/v5" + "github.com/cnk3x/xunlei/pkg/web" ) const Version = "3.21" @@ -46,6 +44,7 @@ const ( PATH_SYNO_INFO_CONF = "/etc/synoinfo.conf" //synoinfo.conf 文件路径 PATH_SYNO_AUTHENTICATE_CGI = "/usr/syno/synoman/webman/modules/authenticate.cgi" //syno...authenticate.cgi 文件路径 UPDATE_URL = "/webman/3rdparty/" + SYNOPKG_PKGNAME + "/version" + CGI_URL = "/webman/3rdparty/" + SYNOPKG_PKGNAME + "/index.cgi/" ) var ( @@ -108,9 +107,7 @@ func Run(ctx context.Context, cfg Config) (err error) { ctx, cancel := context.WithCancel(ctx) defer cancel() - defer func() { - os.RemoveAll(VAR_DIR) - }() + defer os.RemoveAll(VAR_DIR) var dirDownload []string if dirDownload, err = utils.NewRootPath(cfg.Chroot, cfg.DirDownload...); err != nil { @@ -130,12 +127,11 @@ func Run(ctx context.Context, cfg Config) (err error) { envs := mockEnv(dirDownload, dirData[0]) - var webDone <-chan struct{} - if webDone, err = mockWeb(ctx, envs, cfg); err != nil { + webDone, e := mockWeb(ctx, envs, cfg) + if err = e; err != nil { return } - - go utils.SelectDo(webDone, cancel, ctx.Done()) + utils.After(webDone, cancel) slog.DebugContext(ctx, "app start") args := []string{"-launcher_listen", "unix://" + LAUNCHER_LISTEN_PATH, "-pid", PID_FILE} @@ -153,60 +149,27 @@ func Run(ctx context.Context, cfg Config) (err error) { func mockWeb(ctx context.Context, env []string, cfg Config) (webDone <-chan struct{}, err error) { ctx = log.Prefix(ctx, "mock") - mux := chi.NewMux() - mux.Use(webRecoverer) + mux := web.NewMux() + mux.UseRecoverer() console := wrapConsole(ctx) hCgi := &cgi.Handler{ Dir: fmt.Sprintf("%s/bin", SYNOPKG_PKGDEST), Path: fmt.Sprintf("%s/ui/index.cgi", SYNOPKG_PKGDEST), Env: env, - Logger: log.Std(console, "wcgi"), + Logger: utils.LogStd(console), Stderr: console, } - indexPattern := fmt.Sprintf("/webman/3rdparty/%s/index.cgi/", SYNOPKG_PKGNAME) - mux.With(webBasicAuth(cfg.DashboardUsername, cfg.DashboardPassword)).Handle(indexPattern+"*", hCgi) + mux.Handle("/", web.Redirect(CGI_URL, true)) + mux.Handle("/web", web.Redirect(CGI_URL, true)) + mux.Handle("/webman", web.Redirect(CGI_URL, true)) + mux.Handle(UPDATE_URL, web.Blob(fmt.Sprintf("arch: %s\nversion: \"0.0.1\"\naccept: [\"9.9.9\"]", runtime.GOARCH), `text/vnd.yaml`, 200)) + mux.Handle("/webman/login.cgi", web.Blob(fmt.Sprintf(`{"SynoToken":%q,"result":"success","success":true}`, utils.RandText(13)), "application/json", http.StatusOK)) + mux.BasicAuth(cfg.DashboardUsername, cfg.DashboardPassword).Route(CGI_URL, hCgi) - mux.Handle("GET /", webRedirect(indexPattern, true)) - mux.Handle("GET /web", webRedirect(indexPattern, true)) - mux.Handle("GET /webman", webRedirect(indexPattern, true)) - mux.Handle("/webman/login.cgi", webBlob(fmt.Sprintf(`{"SynoToken":%q,"result":"success","success":true}`, utils.RandText(13)), "application/json", http.StatusOK)) - mux.HandleFunc("GET "+UPDATE_URL, func(w http.ResponseWriter, r *http.Request) { - webBlob(fmt.Sprintf("arch: %s\nversion: \"0.0.1\"\naccept: [\"9.9.9\"]", runtime.GOARCH), `text/vnd.yaml`, 200) - }) - - ip := cfg.Ip.String() - if ip == "" { - ip = "" - } - - err = func() (err error) { - s := &http.Server{Addr: net.JoinHostPort(ip, strconv.FormatUint(uint64(cfg.Port), 10)), Handler: mux} - - s.BaseContext = func(l net.Listener) context.Context { - slog.InfoContext(ctx, "ui started", "listen", l.Addr().String()) - return ctx - } - - done := make(chan struct{}) - webDone = done - - go func() { - defer close(done) - defer console.Close() - - if err := s.ListenAndServe(); err != nil && err != http.ErrServerClosed { - slog.ErrorContext(ctx, "ui is done!", "err", err) - } else { - slog.InfoContext(ctx, "ui is done!") - } - }() - - go utils.SelectDo(ctx.Done(), func() { s.Shutdown(context.Background()) }, done) - - return - }() + webDone = mux.Start(ctx, web.Address(cfg.Ip, cfg.Port)) + utils.After(webDone, utils.Fne(console.Close)) return }