package main import ( "bufio" "embed" "errors" "flag" "fmt" "io" "os" "os/exec" "path/filepath" "regexp" "runtime" "strings" ) // deploymentAssets contains everything needed to register and manage the services. // //go:embed .env.example install.sh start.sh stop.sh restart.sh deploy/*.service var deploymentAssets embed.FS var validServicePrefix = regexp.MustCompile(`^[A-Za-z0-9][A-Za-z0-9_.-]*$`) var assetModes = map[string]os.FileMode{ "install.sh": 0o755, "start.sh": 0o755, "stop.sh": 0o755, "restart.sh": 0o755, ".env.example": 0o644, "deploy/server.service": 0o644, "deploy/cli.service": 0o644, "deploy/scheduler.service": 0o644, } func main() { prefixFlag := flag.String("prefix", "", "systemd 服务名前缀;未指定时交互输入") flag.Parse() if runtime.GOOS != "linux" { fatalf("初始化工具只能在 Linux 服务器上运行") } if err := requireRoot(); err != nil { fatalf("%v", err) } appDir, err := executableDir() if err != nil { fatalf("获取程序目录失败: %v", err) } prefix, err := resolvePrefix(*prefixFlag, os.Stdin, os.Stdout) if err != nil { fatalf("读取项目前缀失败: %v", err) } if err := extractAssets(appDir); err != nil { fatalf("释放部署文件失败: %v", err) } envCreated, err := initializeEnv(appDir) if err != nil { fatalf("初始化 .env 失败: %v", err) } if err := os.WriteFile(filepath.Join(appDir, ".service-prefix"), []byte(prefix+"\n"), 0o644); err != nil { fatalf("保存项目前缀失败: %v", err) } fmt.Printf("部署文件已释放到 %s\n", appDir) if envCreated { fmt.Println("已根据 .env.example 初始化 .env,请按实际环境修改配置") } else { fmt.Println("检测到已有 .env,已保留现有配置") } fmt.Printf("正在注册服务: %s-server, %s-cli, %s-scheduler\n", prefix, prefix, prefix) install := exec.Command("bash", filepath.Join(appDir, "install.sh")) install.Dir = appDir install.Stdin = os.Stdin install.Stdout = os.Stdout install.Stderr = os.Stderr if err := install.Run(); err != nil { fatalf("install.sh 执行失败: %v", err) } } func requireRoot() error { uid, err := exec.Command("id", "-u").Output() if err != nil { return fmt.Errorf("无法检查当前用户权限: %w", err) } if strings.TrimSpace(string(uid)) != "0" { return errors.New("请使用 root 权限运行: sudo ./initializer") } return nil } func executableDir() (string, error) { filename, err := os.Executable() if err != nil { return "", err } filename, err = filepath.Abs(filename) if err != nil { return "", err } return filepath.Dir(filename), nil } func resolvePrefix(value string, in io.Reader, out io.Writer) (string, error) { prefix := strings.TrimSpace(value) if prefix == "" { if _, err := fmt.Fprint(out, "请输入项目前缀(例如 myapp): "); err != nil { return "", err } line, err := bufio.NewReader(in).ReadString('\n') if err != nil && !errors.Is(err, io.EOF) { return "", err } prefix = strings.TrimSpace(line) } if !validServicePrefix.MatchString(prefix) { return "", fmt.Errorf("%q 不是有效前缀,只允许字母、数字、点、下划线和连字符,且必须以字母或数字开头", prefix) } return prefix, nil } func extractAssets(appDir string) error { for name, mode := range assetModes { content, err := deploymentAssets.ReadFile(name) if err != nil { return fmt.Errorf("读取 %s: %w", name, err) } target := filepath.Join(appDir, filepath.FromSlash(name)) if err := os.MkdirAll(filepath.Dir(target), 0o755); err != nil { return fmt.Errorf("创建 %s: %w", filepath.Dir(target), err) } if err := os.WriteFile(target, content, mode); err != nil { return fmt.Errorf("写入 %s: %w", target, err) } if err := os.Chmod(target, mode); err != nil { return fmt.Errorf("设置 %s 权限: %w", target, err) } } return nil } func initializeEnv(appDir string) (bool, error) { target := filepath.Join(appDir, ".env") if _, err := os.Stat(target); err == nil { return false, nil } else if !errors.Is(err, os.ErrNotExist) { return false, err } content, err := deploymentAssets.ReadFile(".env.example") if err != nil { return false, err } if err := os.WriteFile(target, content, 0o600); err != nil { return false, err } return true, nil } func fatalf(format string, args ...any) { fmt.Fprintf(os.Stderr, "错误: "+format+"\n", args...) os.Exit(1) }