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 {
|
2024-10-25 14:21:14 +08:00
|
|
|
|
err := util.KillProcess(p.backendPid)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
2024-10-22 17:06:36 +08:00
|
|
|
|
}
|
|
|
|
|
p.backendPid = 0
|
|
|
|
|
}
|
2024-10-25 14:21:14 +08:00
|
|
|
|
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
|
2024-10-25 14:21:14 +08:00
|
|
|
|
//尝试删除相关进程避免启动失败
|
|
|
|
|
util.FindProcessByPortAndKill(port)
|
2024-10-22 17:06:36 +08:00
|
|
|
|
hadeAddress := fmt.Sprintf(":" + port)
|
|
|
|
|
// 使用命令行启动后端进程
|
|
|
|
|
|
|
|
|
|
// 根据系统设置输出文件名
|
2024-10-25 14:21:14 +08:00
|
|
|
|
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
|
|
|
|
|
}
|
|
|
|
|
|
2024-10-25 14:21:14 +08:00
|
|
|
|
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
|
2024-10-25 14:21:14 +08:00
|
|
|
|
//尝试删除相关进程避免启动失败
|
|
|
|
|
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)
|
2024-10-25 14:21:14 +08:00
|
|
|
|
//尝试删除相关进程避免启动失败
|
|
|
|
|
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)
|
2024-10-25 14:21:14 +08:00
|
|
|
|
//记录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
|
|
|
|
|
},
|
|
|
|
|
}
|
2024-10-25 14:21:14 +08:00
|
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
}
|
|
|
|
|
}
|