diff --git a/internal/client/tunnel/client.go b/internal/client/tunnel/client.go
index 839ed7e..e074894 100644
--- a/internal/client/tunnel/client.go
+++ b/internal/client/tunnel/client.go
@@ -55,9 +55,21 @@ type Client struct {
// NewClient 创建客户端
func NewClient(serverAddr, token, id string) *Client {
- // 默认数据目录
- home, _ := os.UserHomeDir()
- dataDir := filepath.Join(home, ".gotunnel")
+ // 默认数据目录:优先使用用户主目录,失败时回退到当前工作目录
+ var dataDir string
+ if home, err := os.UserHomeDir(); err == nil && home != "" {
+ dataDir = filepath.Join(home, ".gotunnel")
+ } else {
+ // UserHomeDir 失败(如 Android adb shell 环境),使用当前工作目录
+ if cwd, err := os.Getwd(); err == nil {
+ dataDir = filepath.Join(cwd, ".gotunnel")
+ log.Printf("[Client] UserHomeDir unavailable, using current directory: %s", dataDir)
+ } else {
+ // 最后回退到相对路径
+ dataDir = ".gotunnel"
+ log.Printf("[Client] Warning: using relative path for data directory")
+ }
+ }
// 确保数据目录存在
if err := os.MkdirAll(dataDir, 0755); err != nil {
@@ -889,58 +901,99 @@ func (c *Client) sendUpdateResult(stream net.Conn, success bool, message string)
func (c *Client) performSelfUpdate(downloadURL string) error {
c.logf("Starting self-update from: %s", downloadURL)
- // 使用共享的下载和解压逻辑
- binaryPath, cleanup, err := update.DownloadAndExtract(downloadURL, "client")
- if err != nil {
- return err
- }
- defer cleanup()
-
// 获取当前可执行文件路径
currentPath, err := os.Executable()
if err != nil {
- return fmt.Errorf("get executable: %w", err)
+ c.logErrorf("Update failed: cannot get executable path: %v", err)
+ return err
}
currentPath, _ = filepath.EvalSymlinks(currentPath)
+ // 预检查:验证是否有写权限(在下载前检查,避免浪费带宽)
+ if err := c.checkUpdatePermissions(currentPath); err != nil {
+ c.logErrorf("Update failed: %v", err)
+ c.logErrorf("Self-update is not supported in this environment. Please update manually.")
+ return err
+ }
+
+ // 使用共享的下载和解压逻辑
+ c.logf("Downloading update package...")
+ binaryPath, cleanup, err := update.DownloadAndExtract(downloadURL, "client")
+ if err != nil {
+ c.logErrorf("Update failed: download/extract error: %v", err)
+ return err
+ }
+ defer cleanup()
+
// Windows 需要特殊处理
if runtime.GOOS == "windows" {
return performWindowsClientUpdate(binaryPath, currentPath, c.ServerAddr, c.Token, c.ID)
}
- // Linux/Mac: 直接替换
+ // Linux/Mac/Android: 直接替换
backupPath := currentPath + ".bak"
// 停止所有插件
c.stopAllPlugins()
// 备份当前文件
+ c.logf("Backing up current binary...")
if err := os.Rename(currentPath, backupPath); err != nil {
- return fmt.Errorf("backup current: %w", err)
+ c.logErrorf("Update failed: cannot backup current binary: %v", err)
+ c.logErrorf("This may be due to insufficient permissions or read-only filesystem.")
+ return err
}
// 复制新文件(不能用 rename,可能跨文件系统)
+ c.logf("Installing new binary...")
if err := update.CopyFile(binaryPath, currentPath); err != nil {
os.Rename(backupPath, currentPath)
- return fmt.Errorf("replace binary: %w", err)
+ c.logErrorf("Update failed: cannot install new binary: %v", err)
+ return err
}
// 设置执行权限
if err := os.Chmod(currentPath, 0755); err != nil {
os.Rename(backupPath, currentPath)
- return fmt.Errorf("chmod: %w", err)
+ c.logErrorf("Update failed: cannot set execute permission: %v", err)
+ return err
}
// 删除备份
os.Remove(backupPath)
- c.logf("Update completed, restarting...")
+ c.logf("Update completed successfully, restarting...")
// 重启进程
restartClientProcess(currentPath, c.ServerAddr, c.Token, c.ID)
return nil
}
+// checkUpdatePermissions 检查是否有更新权限
+func (c *Client) checkUpdatePermissions(execPath string) error {
+ // 检查可执行文件所在目录是否可写
+ dir := filepath.Dir(execPath)
+ testFile := filepath.Join(dir, ".gotunnel_update_test")
+
+ f, err := os.Create(testFile)
+ if err != nil {
+ c.logErrorf("No write permission to directory: %s", dir)
+ return err
+ }
+ f.Close()
+ os.Remove(testFile)
+
+ // 检查可执行文件本身是否可写
+ f, err = os.OpenFile(execPath, os.O_WRONLY, 0)
+ if err != nil {
+ c.logErrorf("No write permission to executable: %s", execPath)
+ return err
+ }
+ f.Close()
+
+ return nil
+}
+
// stopAllPlugins 停止所有运行中的插件
func (c *Client) stopAllPlugins() {
c.pluginMu.Lock()
diff --git a/web/src/views/ClientView.vue b/web/src/views/ClientView.vue
index b4426d8..3669473 100644
--- a/web/src/views/ClientView.vue
+++ b/web/src/views/ClientView.vue
@@ -3,7 +3,7 @@ import { ref, onMounted, onUnmounted } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import {
ArrowBackOutline, CreateOutline, TrashOutline,
- PushOutline, AddOutline, StorefrontOutline, DocumentTextOutline,
+ PushOutline, AddOutline, StorefrontOutline,
ExtensionPuzzleOutline, SettingsOutline, RefreshOutline
} from '@vicons/ionicons5'
import GlassModal from '../components/GlassModal.vue'
@@ -19,7 +19,6 @@ import {
type UpdateInfo, type SystemStats
} from '../api'
import type { ProxyRule, ClientPlugin, ConfigField, StorePluginInfo, RuleSchemasMap } from '../types'
-import LogViewer from '../components/LogViewer.vue'
import InlineLogPanel from '../components/InlineLogPanel.vue'
const route = useRoute()
@@ -513,9 +512,6 @@ onUnmounted(() => {
}
})
-// Log Viewer
-const showLogViewer = ref(false)
-
// Plugin Menu
const activePluginMenu = ref('')
const togglePluginMenu = (pluginId: string) => {
@@ -578,10 +574,6 @@ const handleDeletePlugin = (plugin: ClientPlugin) => {
推送配置
-