framework1/framework/command/dev.go

436 lines
12 KiB
Go
Raw Normal View History

2024-10-22 17:06:36 +08:00
package command
import (
"errors"
"fmt"
"github.com/Superdanda/hade/framework"
"github.com/Superdanda/hade/framework/cobra"
"github.com/Superdanda/hade/framework/contract"
"github.com/Superdanda/hade/framework/util"
"github.com/fsnotify/fsnotify"
"net/http"
"net/http/httputil"
"net/url"
"os"
"os/exec"
"path/filepath"
"runtime"
"strconv"
"time"
)
// devConfig 代表调试模式的配置信息
type devConfig struct {
Port string // 调试模式最终监听的端口默认为8070
Backend struct { // 后端调试模式配置
RefreshTime int // 调试模式后端更新时间如果文件变更等待3s才进行一次更新能让频繁保存变更更为顺畅, 默认1s
Port string // 后端监听端口, 默认 8072
MonitorFolder string // 监听文件夹默认为AppFolder
}
Frontend struct { // 前端调试模式配置
Port string // 前端启动端口, 默认8071
}
}
// 初始化配置文件
func initDevConfig(c framework.Container) *devConfig {
devConfig := &devConfig{
Port: "8087",
Backend: struct {
RefreshTime int
Port string
MonitorFolder string
}{
1,
"8072",
"",
},
Frontend: struct {
Port string
}{
"8071",
},
}
configer := c.MustMake(contract.ConfigKey).(contract.Config)
if configer.IsExist("app.dev.port") {
devConfig.Port = configer.GetString("app.dev.port")
}
if configer.IsExist("app.dev.backend.refresh_time") {
devConfig.Backend.RefreshTime = configer.GetInt("app.dev.backend.refresh_time")
}
if configer.IsExist("app.dev.backend.port") {
devConfig.Backend.Port = configer.GetString("app.dev.backend.port")
}
monitorFolder := configer.GetString("app.dev.backend.monitor_folder")
if monitorFolder == "" {
appService := c.MustMake(contract.AppKey).(contract.App)
devConfig.Backend.MonitorFolder = appService.AppFolder()
}
if configer.IsExist("app.dev.frontend.port") {
devConfig.Frontend.Port = configer.GetString("app.dev.frontend.port")
}
return devConfig
}
// Proxy 代表serve启动的服务器代理
type Proxy struct {
devConfig *devConfig // 配置文件
proxyServer *http.Server // proxy的服务
backendPid int // 当前的backend服务的pid
frontendPid int // 当前的frontend服务的pid
2024-10-24 23:03:15 +08:00
container framework.Container
2024-10-22 17:06:36 +08:00
}
// NewProxy 初始化一个Proxy
func NewProxy(c framework.Container) *Proxy {
devConfig := initDevConfig(c)
return &Proxy{
devConfig: devConfig,
2024-10-24 23:03:15 +08:00
container: c,
2024-10-22 17:06:36 +08:00
}
}
func (p *Proxy) newProxyReverseProxy(frontend, backend *url.URL) *httputil.ReverseProxy {
if p.frontendPid == 0 && p.backendPid == 0 {
fmt.Println("前端和后端服务都不存在")
return nil
}
// 后端服务存在
if p.frontendPid == 0 && p.backendPid != 0 {
return httputil.NewSingleHostReverseProxy(backend)
}
// 前端服务存在
if p.backendPid == 0 && p.frontendPid != 0 {
return httputil.NewSingleHostReverseProxy(frontend)
}
// 两个都有进程
// 方式一: 后端服务的directory
directorBackend := func(req *http.Request) {
req.URL.Scheme = backend.Scheme
req.URL.Host = backend.Host
}
// 方式二后端服务的directory
//directorFrontend := func(req *http.Request) {
// req.URL.Scheme = frontend.Scheme
// req.URL.Host = frontend.Host
//}
// 定义一个NotFoundErr
NotFoundErr := errors.New("response is 404, need to redirect")
return &httputil.ReverseProxy{
Director: directorBackend, // 先转发到后端服务
ModifyResponse: func(response *http.Response) error {
// 如果后端服务返回了404我们返回NotFoundErr 会进入到errorHandler中
if response.StatusCode == 404 {
return NotFoundErr
}
return nil
},
ErrorHandler: func(writer http.ResponseWriter, request *http.Request, err error) {
if errors.Is(err, NotFoundErr) {
httputil.NewSingleHostReverseProxy(frontend).ServeHTTP(writer, request)
}
},
}
}
// rebuildBackend 重新编译后端
func (p *Proxy) rebuildBackend() error {
// 重新编译hade
fmt.Println("重新编译后端服务")
2024-10-24 23:03:15 +08:00
config := p.container.MustMake(contract.ConfigKey).(contract.Config)
cmdBuild := exec.Command("./"+config.GetAppName(), "build", "backend")
2024-10-22 17:06:36 +08:00
cmdBuild.Stdout = os.Stdout
cmdBuild.Stderr = os.Stderr
if err := cmdBuild.Start(); err == nil {
err = cmdBuild.Wait()
if err != nil {
return err
}
}
return nil
}
// restartBackend 启动后端服务
func (p *Proxy) restartBackend() error {
// 杀死之前的进程
if p.backendPid != 0 {
err := util.KillProcess(p.backendPid)
if err != nil {
return err
2024-10-22 17:06:36 +08:00
}
p.backendPid = 0
}
container := p.container
// 如果 发现之前有遗留并保存到本地的进程也要杀死
hadeApp := container.MustMake(contract.AppKey).(contract.App)
runtimeFolder := hadeApp.RuntimeFolder()
backendPidFile := filepath.Join(runtimeFolder, "backend.pid")
if util.Exists(backendPidFile) {
backendPid, _ := util.ReadFileToInt(backendPidFile)
if backendPid != p.backendPid {
util.KillProcess(p.backendPid)
}
}
2024-10-22 17:06:36 +08:00
// 设置随机端口,真实后端的端口
port := p.devConfig.Backend.Port
//尝试删除相关进程避免启动失败
util.FindProcessByPortAndKill(port)
2024-10-22 17:06:36 +08:00
hadeAddress := fmt.Sprintf(":" + port)
// 使用命令行启动后端进程
// 根据系统设置输出文件名
config := container.MustMake(contract.ConfigKey).(contract.Config)
2024-10-24 23:03:15 +08:00
execName := "./" + config.GetAppName()
2024-10-22 17:06:36 +08:00
if runtime.GOOS == "windows" {
execName += ".exe"
}
cmd := exec.Command(execName, "app", "start", "--address="+hadeAddress)
cmd.Stdout = os.NewFile(0, os.DevNull)
cmd.Stderr = os.Stderr
fmt.Println("启动后端服务: ", "http://127.0.0.1:"+port)
err := cmd.Start()
if err != nil {
fmt.Println(err)
}
p.backendPid = cmd.Process.Pid
fmt.Println("后端服务pid:", p.backendPid)
return nil
}
// 启动前端服务
func (p *Proxy) restartFrontend() error {
// 启动前端调试模式
// 如果已经开启了npm run serve 什么都不做
if p.frontendPid != 0 {
return nil
}
container := p.container
hadeApp := container.MustMake(contract.AppKey).(contract.App)
runtimeFolder := hadeApp.RuntimeFolder()
frontendPidFile := filepath.Join(runtimeFolder, "frontend.pid")
if util.Exists(frontendPidFile) {
frontendPid, _ := util.ReadFileToInt(frontendPidFile)
if frontendPid != p.frontendPid {
util.KillProcess(frontendPid)
}
}
2024-10-22 17:06:36 +08:00
// 否则开启npm run serve
port := p.devConfig.Frontend.Port
//尝试删除相关进程避免启动失败
util.FindProcessByPortAndKill(port)
2024-10-22 17:06:36 +08:00
path, err := exec.LookPath("npm")
if err != nil {
return err
}
cmd := exec.Command(path, "run", "dev", "--", "--port", port, "--host", "127.0.0.1")
cmd.Env = os.Environ()
cmd.Env = append(cmd.Env, fmt.Sprintf("%s%s", "PORT=", port))
cmd.Stdout = os.NewFile(0, os.DevNull)
cmd.Stderr = os.Stderr
// 因为npm run serve 是控制台挂起模式所以这里使用go routine启动
err = cmd.Start()
fmt.Println("启动前端服务: ", "http://127.0.0.1:"+port)
if err != nil {
fmt.Println(err)
}
p.frontendPid = cmd.Process.Pid
fmt.Println("前端服务pid:", p.frontendPid)
return nil
}
// 重启后端服务, 如果frontend为nil则没有包含后端
func (p *Proxy) startProxy(startFrontend, startBackend bool) error {
var backendURL, frontendURL *url.URL
var err error
// 启动后端
if startBackend {
if err := p.restartBackend(); err != nil {
return err
}
}
// 启动前端
if startFrontend {
if err := p.restartFrontend(); err != nil {
return err
}
}
// 如果已经启动过proxy了就不要进行设置了
if p.proxyServer != nil {
return nil
}
if frontendURL, err = url.Parse(fmt.Sprintf("%s%s", "http://127.0.0.1:", p.devConfig.Frontend.Port)); err != nil {
return err
}
if backendURL, err = url.Parse(fmt.Sprintf("%s%s", "http://127.0.0.1:", p.devConfig.Backend.Port)); err != nil {
return err
}
// 设置反向代理
proxyReverse := p.newProxyReverseProxy(frontendURL, backendURL)
//尝试删除相关进程避免启动失败
util.FindProcessByPortAndKill(p.devConfig.Port)
2024-10-22 17:06:36 +08:00
p.proxyServer = &http.Server{
Addr: "127.0.0.1:" + p.devConfig.Port,
Handler: proxyReverse,
}
fmt.Println("代理服务启动:", "http://"+p.proxyServer.Addr)
//记录pid信息到文件当中
recordPidToFile(p)
2024-10-22 17:06:36 +08:00
// 启动proxy服务
err = p.proxyServer.ListenAndServe()
if err != nil {
fmt.Println(err)
}
return nil
}
// monitorBackend 监听应用文件
func (p *Proxy) monitorBackend() error {
// 监听
watcher, err := fsnotify.NewWatcher()
if err != nil {
return err
}
defer watcher.Close()
appFolder := p.devConfig.Backend.MonitorFolder
fmt.Println("监控文件夹:", appFolder)
filepath.Walk(appFolder, func(path string, info os.FileInfo, err error) error {
if info != nil && !info.IsDir() {
return nil
}
if util.IsHiddenDirectory(path) {
return nil
}
return watcher.Add(path)
})
refreshTime := p.devConfig.Backend.RefreshTime
t := time.NewTimer(time.Duration(refreshTime) * time.Second)
t.Stop()
for {
select {
case <-t.C:
fmt.Println("...检测到文件更新,重启服务开始...")
if err := p.rebuildBackend(); err != nil {
fmt.Println("重新编译失败:", err.Error())
} else {
if err := p.restartBackend(); err != nil {
fmt.Println("重新启动失败:", err.Error())
}
}
fmt.Println("...检测到文件更新,重启服务结束...")
t.Stop()
case _, ok := <-watcher.Events:
if !ok {
continue
}
t.Reset(time.Duration(refreshTime) * time.Second)
case err, ok := <-watcher.Errors:
if !ok {
continue
}
fmt.Println("监听文件夹错误:", err.Error())
t.Reset(time.Duration(refreshTime) * time.Second)
}
}
return nil
}
// 初始化Dev命令
func initDevCommand() *cobra.Command {
devCommand.AddCommand(devBackendCommand)
devCommand.AddCommand(devFrontendCommand)
devCommand.AddCommand(devAllCommand)
return devCommand
}
// devCommand 为调试模式的一级命令
var devCommand = &cobra.Command{
Use: "dev",
Short: "调试模式",
RunE: func(c *cobra.Command, args []string) error {
c.Help()
return nil
},
}
// devBackendCommand 启动后端调试模式
var devBackendCommand = &cobra.Command{
Use: "backend",
Short: "启动后端调试模式",
RunE: func(c *cobra.Command, args []string) error {
proxy := NewProxy(c.GetContainer())
go proxy.monitorBackend()
if err := proxy.startProxy(false, true); err != nil {
return err
}
return nil
},
}
// devFrontendCommand 启动前端调试模式
var devFrontendCommand = &cobra.Command{
Use: "frontend",
Short: "前端调试模式",
RunE: func(c *cobra.Command, args []string) error {
// 启动前端服务
proxy := NewProxy(c.GetContainer())
return proxy.startProxy(true, false)
},
}
var devAllCommand = &cobra.Command{
Use: "all",
Short: "同时启动前端和后端调试",
RunE: func(c *cobra.Command, args []string) error {
proxy := NewProxy(c.GetContainer())
go proxy.monitorBackend()
if err := proxy.startProxy(true, true); err != nil {
return err
}
return nil
},
}
func recordPidToFile(proxy *Proxy) {
container := proxy.container
//记录后端的pid到本地文件
app := container.MustMake(contract.AppKey).(contract.App)
storageFolder := app.RuntimeFolder()
backendPidFile := filepath.Join(storageFolder, "backend.pid")
frontendPidFile := filepath.Join(storageFolder, "frontend.pid")
// 将 backendPid 写入文件
backendPid := strconv.Itoa(proxy.backendPid) // 将 pid 转为字符串
if err := os.WriteFile(backendPidFile, []byte(backendPid), 0666); err != nil {
fmt.Println("无法写入 PID 文件: %w", err)
}
frontendPid := strconv.Itoa(proxy.frontendPid) // 将 pid 转为字符串
if err := os.WriteFile(frontendPidFile, []byte(frontendPid), 0666); err != nil {
fmt.Println("无法写入 PID 文件: %w", err)
}
}