Skip to content

Commit f42915a

Browse files
committed
feat: 便携模式 + Windows 版本检测
- 便携模式:程序同级目录放 portable 空文件,配置/bin/日志/PID 全部就地存储 - Windows 版本检测:启动时检测 OS 版本,低于 Win10 友好提示退出 - install 命令便携模式下拒绝注册系统服务 - reset 命令便携模式下安全清理(不删程序自身) - pidFile 包级变量改为函数调用,消除初始化时序隐患 Closes #8
1 parent 7729c2d commit f42915a

8 files changed

Lines changed: 95 additions & 9 deletions

File tree

cmd/install.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@ var installCmd = &cobra.Command{
1818
Use: "install",
1919
Short: "注册为系统服务(开机自启)",
2020
RunE: func(cmd *cobra.Command, args []string) error {
21+
if config.Portable() {
22+
return fmt.Errorf("便携模式下不支持注册系统服务(路径不固定),请使用 cftunnel up 手动启动")
23+
}
2124
cfg, err := config.Load()
2225
if err != nil {
2326
return err

cmd/logs.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"runtime"
99
"time"
1010

11+
"github.com/qingchencloud/cftunnel/internal/config"
1112
"github.com/spf13/cobra"
1213
)
1314

@@ -20,6 +21,11 @@ func init() {
2021

2122
// logFilePath 根据操作系统返回日志文件路径
2223
func logFilePath() string {
24+
// 便携模式:日志放在程序同级目录
25+
if config.Portable() {
26+
return filepath.Join(config.Dir(), "cftunnel.log")
27+
}
28+
// 普通模式:按 OS 惯例
2329
home, _ := os.UserHomeDir()
2430
switch runtime.GOOS {
2531
case "darwin":

cmd/reset.go

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"bufio"
55
"fmt"
66
"os"
7+
"path/filepath"
78
"strings"
89

910
"github.com/qingchencloud/cftunnel/internal/config"
@@ -41,10 +42,17 @@ var resetCmd = &cobra.Command{
4142
}
4243
}
4344

44-
// 删除整个配置目录
45+
// 删除配置目录
4546
dir := config.Dir()
46-
if err := os.RemoveAll(dir); err != nil {
47-
return fmt.Errorf("清除配置目录失败: %w", err)
47+
if config.Portable() {
48+
// 便携模式:只清理数据文件,不删程序自身和 portable 标记
49+
for _, name := range []string{"config.yml", "bin", "cloudflared.pid", "cftunnel.log"} {
50+
os.RemoveAll(filepath.Join(dir, name))
51+
}
52+
} else {
53+
if err := os.RemoveAll(dir); err != nil {
54+
return fmt.Errorf("清除配置目录失败: %w", err)
55+
}
4856
}
4957

5058
fmt.Printf("已清除 %s,回到全新状态\n", dir)

cmd/root.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
package cmd
22

33
import (
4+
"fmt"
45
"os"
56

7+
"github.com/qingchencloud/cftunnel/internal/config"
68
"github.com/spf13/cobra"
79
)
810

@@ -12,6 +14,12 @@ var rootCmd = &cobra.Command{
1214
Use: "cftunnel",
1315
Short: "Cloudflare Tunnel 一键管理工具",
1416
Version: Version,
17+
PersistentPreRun: func(cmd *cobra.Command, args []string) {
18+
checkWindowsVersion()
19+
if config.Portable() {
20+
fmt.Printf("[便携模式] 数据目录: %s\n", config.Dir())
21+
}
22+
},
1523
}
1624

1725
func Execute() {

cmd/wincheck_other.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
//go:build !windows
2+
3+
package cmd
4+
5+
// checkWindowsVersion 非 Windows 平台空实现
6+
func checkWindowsVersion() {}

cmd/wincheck_windows.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
//go:build windows
2+
3+
package cmd
4+
5+
import (
6+
"fmt"
7+
"os"
8+
9+
"golang.org/x/sys/windows"
10+
)
11+
12+
// checkWindowsVersion 检测 Windows 版本,低于 Win10 则警告退出
13+
func checkWindowsVersion() {
14+
ver := windows.RtlGetVersion()
15+
if ver.MajorVersion < 10 {
16+
fmt.Fprintf(os.Stderr, "错误: 当前系统 Windows NT %d.%d 不受支持\n",
17+
ver.MajorVersion, ver.MinorVersion)
18+
fmt.Fprintln(os.Stderr, "cftunnel 依赖的 cloudflared 和 Go 运行时均要求 Windows 10 或更高版本。")
19+
fmt.Fprintln(os.Stderr, "Windows 7/8/8.1 用户请参考: https://github.com/qingchencloud/cftunnel/issues/8")
20+
os.Exit(1)
21+
}
22+
}

internal/config/config.go

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package config
33
import (
44
"os"
55
"path/filepath"
6+
"sync"
67

78
"gopkg.in/yaml.v3"
89
)
@@ -44,9 +45,37 @@ type SelfUpdateConfig struct {
4445
AutoCheck bool `yaml:"auto_check"` // 启动时自动检查 cftunnel 更新
4546
}
4647

48+
var (
49+
dirOnce sync.Once
50+
dirPath string
51+
isPortable bool
52+
)
53+
54+
// Dir 返回配置目录路径
55+
// 便携模式:程序同级目录存在 portable 文件时,使用程序所在目录
56+
// 普通模式:~/.cftunnel/
4757
func Dir() string {
48-
home, _ := os.UserHomeDir()
49-
return filepath.Join(home, ".cftunnel")
58+
dirOnce.Do(func() {
59+
if exe, err := os.Executable(); err == nil {
60+
if real, err := filepath.EvalSymlinks(exe); err == nil {
61+
exeDir := filepath.Dir(real)
62+
if _, err := os.Stat(filepath.Join(exeDir, "portable")); err == nil {
63+
dirPath = exeDir
64+
isPortable = true
65+
return
66+
}
67+
}
68+
}
69+
home, _ := os.UserHomeDir()
70+
dirPath = filepath.Join(home, ".cftunnel")
71+
})
72+
return dirPath
73+
}
74+
75+
// Portable 返回当前是否处于便携模式
76+
func Portable() bool {
77+
Dir() // 确保 dirOnce 已执行
78+
return isPortable
5079
}
5180

5281
func Path() string {

internal/daemon/manager.go

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,17 @@ import (
44
"fmt"
55
"os"
66
"os/exec"
7+
"path/filepath"
78
"strconv"
89
"strings"
910

1011
"github.com/qingchencloud/cftunnel/internal/config"
1112
)
1213

13-
var pidFile = config.Dir() + "/cloudflared.pid"
14+
// pidFilePath 返回 PID 文件路径(函数调用替代包级变量,确保便携模式正确生效)
15+
func pidFilePath() string {
16+
return filepath.Join(config.Dir(), "cloudflared.pid")
17+
}
1418

1519
// Start 启动 cloudflared(token 模式)
1620
func Start(token string) error {
@@ -30,7 +34,7 @@ func Start(token string) error {
3034
}
3135

3236
os.MkdirAll(config.Dir(), 0700)
33-
os.WriteFile(pidFile, []byte(strconv.Itoa(cmd.Process.Pid)), 0600)
37+
os.WriteFile(pidFilePath(), []byte(strconv.Itoa(cmd.Process.Pid)), 0600)
3438
fmt.Printf("cloudflared 已启动 (PID: %d)\n", cmd.Process.Pid)
3539
return nil
3640
}
@@ -44,7 +48,7 @@ func Stop() error {
4448
if err := processKill(pid); err != nil {
4549
return fmt.Errorf("停止 cloudflared 失败: %w", err)
4650
}
47-
os.Remove(pidFile)
51+
os.Remove(pidFilePath())
4852
fmt.Println("cloudflared 已停止")
4953
return nil
5054
}
@@ -65,7 +69,7 @@ func PID() int {
6569
}
6670

6771
func readPID() (int, error) {
68-
data, err := os.ReadFile(pidFile)
72+
data, err := os.ReadFile(pidFilePath())
6973
if err != nil {
7074
return 0, err
7175
}

0 commit comments

Comments
 (0)