diff --git a/godo/.gitignore b/godo/.gitignore index 5ae554c..8df4bb7 100644 --- a/godo/.gitignore +++ b/godo/.gitignore @@ -3,3 +3,7 @@ deps/dist deps/*.zip godo testdata +deps/darwin/*.zip +deps/linux/*.zip +deps/windows/*.zip +.DS_Store \ No newline at end of file diff --git a/godo/deps/app_embed.go b/godo/deps/app_embed.go new file mode 100644 index 0000000..451eb32 --- /dev/null +++ b/godo/deps/app_embed.go @@ -0,0 +1,65 @@ +package deps + +import ( + _ "embed" + "errors" + "godo/libs" +) + +//go:embed darwin/amd64.zip +var embeddedDarwinAMD64Zip []byte + +//go:embed darwin/arm64.zip +var embeddedDarwinARM64Zip []byte + +//go:embed linux/amd64.zip +var embeddedLinuxAMD64Zip []byte + +//go:embed linux/arm64.zip +var embeddedLinuxARM64Zip []byte + +//go:embed windows/amd64.zip +var embeddedWindowsAMD64Zip []byte + +//go:embed windows/arm64.zip +var embeddedWindowsARM64Zip []byte + +const ( + FRPCAPP = "frpc" +) + +var ( + ErrUnsupportedOSArch = errors.New("unsupported OS and architecture combination") + embeddedZips = map[string]map[string]map[string][]byte{ + libs.OSDarwin: { + libs.ArchAMD64: {FRPCAPP: embeddedDarwinAMD64Zip}, + libs.ArchARM64: {FRPCAPP: embeddedDarwinARM64Zip}, + libs.ArchARM: {FRPCAPP: embeddedDarwinARM64Zip}, + }, + libs.OSWindows: { + libs.ArchAMD64: {FRPCAPP: embeddedWindowsAMD64Zip}, + libs.ArchARM64: {FRPCAPP: embeddedWindowsARM64Zip}, + libs.ArchARM: {FRPCAPP: embeddedWindowsARM64Zip}, + }, + libs.OSLinux: { + libs.ArchAMD64: {FRPCAPP: embeddedLinuxAMD64Zip}, + libs.ArchARM64: {FRPCAPP: embeddedLinuxARM64Zip}, + libs.ArchARM: {FRPCAPP: embeddedLinuxARM64Zip}, + }, + } +) + +func ExtractZip(appName, targetDir string) error { + os, arch := libs.GetOSAndArch() + archMap, ok := embeddedZips[os] + if !ok { + return ErrUnsupportedOSArch + } + + zipData, ok := archMap[arch][appName] + if !ok { + return ErrUnsupportedOSArch + } + + return extractZip(zipData, targetDir) +} diff --git a/godo/deps/extract.go b/godo/deps/extract.go new file mode 100644 index 0000000..b0d152c --- /dev/null +++ b/godo/deps/extract.go @@ -0,0 +1,86 @@ +package deps + +import ( + "archive/zip" + "bytes" + "fmt" + "io" + "log" + "os" + "path/filepath" + "strings" +) + +// extractZip 解压嵌入的 zip 文件到目标目录 +func extractZip(zipData []byte, targetDir string) error { + log.Println("Extracting embedded ZIP file...") + // 使用内存缓冲区来读取嵌入的ZIP数据 + reader := bytes.NewReader(zipData) + zipReader, err := zip.NewReader(reader, int64(len(zipData))) + if err != nil { + return fmt.Errorf("failed to create zip reader: %v", err) + } + + // 遍历ZIP文件中的每个条目并解压 + for _, zipEntry := range zipReader.File { + // 忽略 __MACOSX 文件夹 + if strings.HasPrefix(zipEntry.Name, "__MACOSX") { + continue + } + + // 检查条目名称是否以"."开头,如果是,则跳过 + if strings.HasPrefix(zipEntry.Name, ".") { + fmt.Printf("Skipping hidden entry: %s\n", zipEntry.Name) + continue + } + + // 去掉 ZIP 文件中的顶层目录(zip文件名本身) + entryName := zipEntry.Name + if idx := strings.Index(entryName, "/"); idx != -1 { + entryName = entryName[idx+1:] // 去掉顶层目录 + } + + // 如果 entryName 为空,说明是顶层目录本身,跳过 + if entryName == "" { + continue + } + + // 构建解压后的文件或目录路径 + entryPath := filepath.Join(targetDir, entryName) + fmt.Println("Extracting:", entryName) + + // 如果是目录,则创建目录 + if zipEntry.FileInfo().IsDir() { + if err := os.MkdirAll(entryPath, zipEntry.Mode()); err != nil { + return fmt.Errorf("failed to create directory: %v", err) + } + continue + } + + // 如果是文件,则解压文件 + zipFile, err := zipEntry.Open() + if err != nil { + return fmt.Errorf("failed to open zip file entry: %v", err) + } + defer zipFile.Close() + + // 确保目标文件的父目录存在 + if err := os.MkdirAll(filepath.Dir(entryPath), 0755); err != nil { + return fmt.Errorf("failed to create parent directory: %v", err) + } + + dstFile, err := os.OpenFile(entryPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0755) + if err != nil { + return fmt.Errorf("failed to create destination file: %v", err) + } + defer dstFile.Close() + + if _, err := io.Copy(dstFile, zipFile); err != nil { + return fmt.Errorf("failed to copy content to destination file: %v", err) + } + } + + log.Println("Embedded ZIP extracted to", targetDir) + + return nil +} diff --git a/godo/libs/osinfo.go b/godo/libs/osinfo.go new file mode 100644 index 0000000..bc09d6f --- /dev/null +++ b/godo/libs/osinfo.go @@ -0,0 +1,42 @@ +package libs + +import "runtime" + +const ( + OSDarwin = "macOS" + OSWindows = "Windows" + OSLinux = "Linux" + + ArchAMD64 = "x86_64" + ArchARM64 = "ARM64" + ArchARM = "ARM" +) + +// GetOSAndArch 返回当前操作系统的名称和架构的名称 +func GetOSAndArch() (osName, archName string) { + // 获取操作系统 + switch runtime.GOOS { + case "darwin": + osName = OSDarwin + case "windows": + osName = OSWindows + case "linux": + osName = OSLinux + default: + osName = "Unknown OS" + } + + // 获取架构 + switch runtime.GOARCH { + case "amd64": + archName = ArchAMD64 + case "arm64": + archName = ArchARM64 + case "arm": + archName = ArchARM + default: + archName = "Unknown Architecture" + } + + return osName, archName +} diff --git a/godo/progress/linux.go b/godo/progress/linux.go new file mode 100644 index 0000000..a755018 --- /dev/null +++ b/godo/progress/linux.go @@ -0,0 +1,12 @@ +//go:build !windows + +package progress + +import ( + // 导入包 + "os/exec" +) + +func SetHideConsoleCursor(cmd *exec.Cmd) *exec.Cmd { + return cmd +} diff --git a/godo/progress/progress.go b/godo/progress/progress.go new file mode 100644 index 0000000..1c98a9e --- /dev/null +++ b/godo/progress/progress.go @@ -0,0 +1,149 @@ +package progress + +import ( + "errors" + "fmt" + "godo/deps" + "godo/libs" + "log" + "os" + "os/exec" + "path/filepath" + "runtime" + "sync" +) + +type Process struct { + Name string + Running bool + ExitCode int + Cmd *exec.Cmd +} + +var ( + processesMu sync.RWMutex + processes = make(map[string]*Process) +) + +func RegisterProcess(name string, cmdstr *exec.Cmd) { + processesMu.Lock() + defer processesMu.Unlock() + processes[name] = &Process{ + Name: name, + Running: true, + Cmd: cmdstr, + } +} +func GetCmd(name string) *Process { + processesMu.Lock() + defer processesMu.Unlock() + return processes[name] +} + +// StartCmd 执行指定名称的脚本。 +// 参数: +// name - 脚本的名称。 +// 返回值: +// 返回可能遇到的错误,如果执行成功,则返回nil。 +func StartCmd(name string) error { + info, ok := processes[name] + if ok && info.Running { + return fmt.Errorf("process information for '%s' is runing", name) + } + + path := name + appName := name + scriptPath, err := libs.GetCmdPath(path, name) + if err != nil { + // 不存在,解压应用 + switch name { + case deps.FRPCAPP: + appName = deps.FRPCAPP + default: + return errors.New("app not found") + } + err = deps.ExtractZip(appName, libs.GetAppExecDir()) + if err != nil { + return fmt.Errorf("failed to extract zip file: %v", err) + } + } + + // 设置并启动脚本执行命令 + var cmd *exec.Cmd + switch name { + case "frpc": + // 检查配置文件 + configPath := filepath.Join(libs.GetAppExecDir(), appName, "frpc.ini") + if !libs.PathExists(configPath) { + return fmt.Errorf("frpc config file not found") + } + + params := []string{ + "-c", + configPath, + } + cmd = exec.Command(scriptPath, params...) + default: + cmd = exec.Command(scriptPath) + } + + if runtime.GOOS == "windows" { + // 在Windows上,通过设置CreationFlags来隐藏窗口 + cmd = SetHideConsoleCursor(cmd) + } + go func() { + // 启动脚本命令并返回可能的错误 + if err := cmd.Start(); err != nil { + log.Printf("failed to start process %s: %v", name, err) + return + } + RegisterProcess(name, cmd) + // 等待命令完成 + if err := cmd.Wait(); err != nil { + log.Printf("command failed for %s: %v", name, err) + return + } else { + log.Printf("%s command completed successfully", name) + } + + // 命令完成后,更新进程信息 + processesMu.Lock() + defer processesMu.Unlock() + if p, ok := processes[name]; ok { + p.Running = false + p.ExitCode = cmd.ProcessState.ExitCode() + } + }() + return nil +} +func StopCmd(name string) error { + cmd, ok := processes[name] + if !ok { + return fmt.Errorf("process information for '%s' not found", name) + } + + // 停止进程并更新status + if err := cmd.Cmd.Process.Kill(); err != nil { + return fmt.Errorf("failed to kill process %s: %v", name, err) + } + //delete(processes, name) // 更新status,表示进程已停止 + cmd.Running = false + return nil +} +func RestartCmd(name string) error { + if err := StopCmd(name); err != nil { + return err + } + return StartCmd(name) +} +func StopAllCmd() error { + processesMu.Lock() + defer processesMu.Unlock() + + for name, cmd := range processes { + if err := cmd.Cmd.Process.Signal(os.Interrupt); err != nil { + return fmt.Errorf("failed to stop process %s: %v", name, err) + } + } + return nil +} diff --git a/godo/progress/windows.go b/godo/progress/windows.go new file mode 100644 index 0000000..b28192e --- /dev/null +++ b/godo/progress/windows.go @@ -0,0 +1,15 @@ +//go:build windows +// +build windows + +package progress + +import ( + // 导入包 + "os/exec" + "syscall" +) + +func SetHideConsoleCursor(cmd *exec.Cmd) *exec.Cmd { + cmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: true} + return cmd +}