220 lines
5.7 KiB
Go
220 lines
5.7 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"flag"
|
|
"fmt"
|
|
"os"
|
|
"os/signal"
|
|
"path/filepath"
|
|
"runtime"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"mysql-tunnel/internal/app"
|
|
"mysql-tunnel/internal/config"
|
|
"mysql-tunnel/internal/gui"
|
|
"mysql-tunnel/internal/logger"
|
|
"mysql-tunnel/internal/winsvc"
|
|
)
|
|
|
|
const version = "0.5.0"
|
|
|
|
func main() {
|
|
os.Exit(run(os.Args[1:]))
|
|
}
|
|
|
|
func run(args []string) int {
|
|
fs := flag.NewFlagSet("mysql-tunnel", flag.ContinueOnError)
|
|
fs.SetOutput(os.Stderr)
|
|
configPath := fs.String("config", "config.yaml", "path to config.yaml")
|
|
listenHost := fs.String("host", "", "override listen.host")
|
|
listenPort := fs.Int("port", 0, "override listen.port")
|
|
doTest := fs.Bool("test", false, "POST actn=C and SELECT 1 through the HTTP tunnel")
|
|
showVersion := fs.Bool("version", false, "print version and exit")
|
|
doGUI := fs.Bool("gui", false, "open the configuration window (Windows)")
|
|
doConsole := fs.Bool("console", false, "listen in the foreground without a window")
|
|
doInstall := fs.Bool("install", false, "install Windows service (Administrator)")
|
|
doUninstall := fs.Bool("uninstall", false, "uninstall Windows service (Administrator)")
|
|
doStart := fs.Bool("start", false, "start Windows service")
|
|
doStop := fs.Bool("stop", false, "stop Windows service")
|
|
doRestart := fs.Bool("restart", false, "restart Windows service")
|
|
if err := fs.Parse(args); err != nil {
|
|
return 2
|
|
}
|
|
|
|
if *showVersion {
|
|
fmt.Println("mysql-tunnel", version)
|
|
return 0
|
|
}
|
|
if *doInstall {
|
|
return cmdInstall(*configPath)
|
|
}
|
|
if *doUninstall {
|
|
return report(winsvc.Uninstall())
|
|
}
|
|
if *doStart {
|
|
return report(winsvc.Start())
|
|
}
|
|
if *doStop {
|
|
return report(winsvc.Stop())
|
|
}
|
|
if *doRestart {
|
|
_ = winsvc.Stop()
|
|
return report(winsvc.Start())
|
|
}
|
|
|
|
if winsvc.IsService() {
|
|
return runHeadless(*configPath, *listenHost, *listenPort, false)
|
|
}
|
|
|
|
if wantGUI(fs, *doGUI, *doConsole, *doTest) {
|
|
return gui.Run(*configPath)
|
|
}
|
|
|
|
return runHeadless(*configPath, *listenHost, *listenPort, *doTest)
|
|
}
|
|
|
|
func wantGUI(fs *flag.FlagSet, gui, console, doTest bool) bool {
|
|
if console || doTest {
|
|
return false
|
|
}
|
|
if gui {
|
|
return true
|
|
}
|
|
if runtime.GOOS != "windows" {
|
|
return false
|
|
}
|
|
// Double-click / no flags → GUI. `--config` without `--gui` stays CLI.
|
|
if flagWasSet(fs, "config") || flagWasSet(fs, "host") || flagWasSet(fs, "port") {
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
func flagWasSet(fs *flag.FlagSet, name string) bool {
|
|
found := false
|
|
fs.Visit(func(f *flag.Flag) {
|
|
if f.Name == name {
|
|
found = true
|
|
}
|
|
})
|
|
return found
|
|
}
|
|
|
|
func runHeadless(configPath, listenHost string, listenPort int, doTest bool) int {
|
|
resolved, err := config.ResolveConfigPath(configPath)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "config error: %s\n", err)
|
|
return 1
|
|
}
|
|
cfg, err := config.Load(resolved, config.Overrides{
|
|
ListenHost: listenHost,
|
|
ListenPort: listenPort,
|
|
})
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "config error: %s\n", logger.RedactError(err))
|
|
return 1
|
|
}
|
|
cfg.ResolvePaths()
|
|
|
|
log, err := logger.New(cfg.LogFile, cfg.LogLevel, cfg.LogSQL)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "logger error: %v\n", err)
|
|
return 1
|
|
}
|
|
defer log.Close()
|
|
|
|
for _, w := range cfg.Warnings {
|
|
log.Warn(w)
|
|
}
|
|
|
|
if doTest {
|
|
fmt.Println("mysql-tunnel --test")
|
|
r := app.Test(context.Background(), cfg, log)
|
|
fmt.Println(strings.ReplaceAll(r.Text(), "\r\n", "\n"))
|
|
if !r.OK() {
|
|
return 1
|
|
}
|
|
return 0
|
|
}
|
|
|
|
runSrv := func(ctx context.Context) error {
|
|
return app.Serve(ctx, cfg, log)
|
|
}
|
|
|
|
if winsvc.IsService() {
|
|
if err := winsvc.Run(runSrv); err != nil {
|
|
log.Error("service stopped", logger.RedactError(err))
|
|
return 1
|
|
}
|
|
return 0
|
|
}
|
|
|
|
ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt)
|
|
defer cancel()
|
|
fmt.Printf("mysql-tunnel listening on %s:%d\n", cfg.ListenHost, cfg.ListenPort)
|
|
fmt.Println("Yingdao: Host=127.0.0.1 Port=" + strconv.Itoa(cfg.ListenPort) + " User=" + cfg.LocalUsername)
|
|
fmt.Println("Tunnel:", config.RedactedTunnelURL(cfg.TunnelURL))
|
|
fmt.Println("Config:", cfg.Path)
|
|
fmt.Println("Log:", cfg.LogFile)
|
|
if err := runSrv(ctx); err != nil {
|
|
log.Error("server stopped", logger.RedactError(err))
|
|
return 1
|
|
}
|
|
log.Info("server stopped")
|
|
return 0
|
|
}
|
|
|
|
func cmdInstall(configFlag string) int {
|
|
exe, err := winsvc.ExePath()
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "install: cannot locate exe: %v\n", err)
|
|
return 1
|
|
}
|
|
src, err := config.ResolveConfigPath(configFlag)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "install: %s\n", err)
|
|
fmt.Fprintln(os.Stderr, "put a filled config.yaml next to mysql-tunnel.exe or pass --config")
|
|
return 1
|
|
}
|
|
if _, err := config.Load(src, config.Overrides{}); err != nil {
|
|
fmt.Fprintf(os.Stderr, "install: invalid config: %s\n", logger.RedactError(err))
|
|
return 1
|
|
}
|
|
dest := filepath.Join(filepath.Dir(exe), "config.yaml")
|
|
srcAbs, _ := filepath.Abs(src)
|
|
destAbs, _ := filepath.Abs(dest)
|
|
if filepath.Clean(srcAbs) != filepath.Clean(destAbs) {
|
|
if err := winsvc.CopyFile(srcAbs, destAbs); err != nil {
|
|
fmt.Fprintf(os.Stderr, "install: copy config: %v\n", err)
|
|
return 1
|
|
}
|
|
fmt.Println("copied config.yaml ->", destAbs)
|
|
}
|
|
if err := winsvc.Install(exe, destAbs); err != nil {
|
|
fmt.Fprintf(os.Stderr, "install: %v\n", err)
|
|
return 1
|
|
}
|
|
if err := winsvc.Start(); err != nil {
|
|
fmt.Fprintf(os.Stderr, "service installed but start failed: %v\n", err)
|
|
fmt.Println("try: mysql-tunnel.exe --start")
|
|
return 1
|
|
}
|
|
fmt.Println("Windows service installed and started:", winsvc.Name)
|
|
fmt.Println(" exe: ", exe)
|
|
fmt.Println(" config:", destAbs)
|
|
fmt.Println(" stop: mysql-tunnel.exe --stop")
|
|
fmt.Println(" uninstall: mysql-tunnel.exe --uninstall")
|
|
return 0
|
|
}
|
|
|
|
func report(err error) int {
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "%v\n", err)
|
|
return 1
|
|
}
|
|
fmt.Println("ok")
|
|
return 0
|
|
}
|