mirror of
https://github.com/truewhile/MeBox.git
synced 2026-09-28 03:06:38 +08:00
331 lines
7.4 KiB
Go
331 lines
7.4 KiB
Go
//go:build windows
|
|
|
|
package main
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"strings"
|
|
"sync/atomic"
|
|
"syscall"
|
|
|
|
"fyne.io/systray"
|
|
"golang.org/x/sys/windows"
|
|
"golang.org/x/sys/windows/registry"
|
|
|
|
"github.com/truewhile/MeBox/internal/brand"
|
|
)
|
|
|
|
const (
|
|
runRegistryKey = `Software\Microsoft\Windows\CurrentVersion\Run`
|
|
runRegistryValue = "MeBox"
|
|
singleInstanceName = `Local\MeBox-Server`
|
|
)
|
|
|
|
var errAlreadyRunning = errors.New("MeBox is already running")
|
|
|
|
func runProgram() {
|
|
if err := prepareWorkingDirectory(); err != nil {
|
|
reportError("MeBox 启动失败", fmt.Errorf("切换工作目录失败: %w", err))
|
|
return
|
|
}
|
|
|
|
instance, err := acquireSingleInstance()
|
|
if errors.Is(err, errAlreadyRunning) {
|
|
reportError("MeBox", errors.New("MeBox 已在运行,请查看右下角托盘图标"))
|
|
return
|
|
}
|
|
if err != nil {
|
|
reportError("MeBox 启动失败", fmt.Errorf("创建单实例锁失败: %w", err))
|
|
return
|
|
}
|
|
|
|
app, err := newApplication()
|
|
if err != nil {
|
|
_ = instance.Close()
|
|
reportError("MeBox 启动失败", err)
|
|
return
|
|
}
|
|
|
|
controller := &trayController{app: app}
|
|
systray.Run(controller.onReady, controller.onExit)
|
|
|
|
// Release the mutex before starting the replacement process. The new
|
|
// process must be able to acquire it immediately after the old one exits.
|
|
_ = instance.Close()
|
|
|
|
if !controller.readyClosed.Load() {
|
|
_ = app.Shutdown()
|
|
reportError("MeBox 启动失败", errors.New("系统托盘初始化失败"))
|
|
return
|
|
}
|
|
if controller.restartRequested.Load() {
|
|
if err := launchSelf(); err != nil {
|
|
reportError("MeBox 重启失败", err)
|
|
}
|
|
}
|
|
}
|
|
|
|
type trayController struct {
|
|
app *application
|
|
|
|
readyClosed atomic.Bool
|
|
restartRequested atomic.Bool
|
|
shutdownStarted atomic.Bool
|
|
}
|
|
|
|
func (c *trayController) onReady() {
|
|
defer func() {
|
|
c.readyClosed.Store(true)
|
|
}()
|
|
|
|
systray.SetIcon(brand.Icon)
|
|
systray.SetTooltip("MeBox")
|
|
|
|
mOpen := systray.AddMenuItem("打开 MeBox", "在浏览器中打开 MeBox")
|
|
mAutoStart := systray.AddMenuItemCheckbox("开机自启", "登录 Windows 后自动启动 MeBox", autoStartEnabled())
|
|
mLogs := systray.AddMenuItem("查看日志", "打开 MeBox 应用日志")
|
|
systray.AddSeparator()
|
|
mRestart := systray.AddMenuItem("重启 MeBox", "重启 MeBox 服务")
|
|
mQuit := systray.AddMenuItem("退出 MeBox", "停止服务并退出")
|
|
|
|
initialAutoStart := mAutoStart.Checked()
|
|
go func() {
|
|
for {
|
|
select {
|
|
case <-mOpen.ClickedCh:
|
|
if err := openURL(c.app.localURL()); err != nil {
|
|
reportError("MeBox", fmt.Errorf("打开 MeBox 失败: %w", err))
|
|
}
|
|
case <-mAutoStart.ClickedCh:
|
|
enable := !mAutoStart.Checked()
|
|
if err := setAutoStart(enable); err != nil {
|
|
if initialAutoStart {
|
|
mAutoStart.Check()
|
|
} else {
|
|
mAutoStart.Uncheck()
|
|
}
|
|
reportError("MeBox", fmt.Errorf("更新开机自启设置失败: %w", err))
|
|
continue
|
|
}
|
|
if enable {
|
|
mAutoStart.Check()
|
|
} else {
|
|
mAutoStart.Uncheck()
|
|
}
|
|
initialAutoStart = enable
|
|
case <-mLogs.ClickedCh:
|
|
if err := c.app.openLog(); err != nil {
|
|
reportError("MeBox", fmt.Errorf("打开日志失败: %w", err))
|
|
}
|
|
case <-mRestart.ClickedCh:
|
|
c.restart()
|
|
case <-mQuit.ClickedCh:
|
|
c.quit()
|
|
}
|
|
}
|
|
}()
|
|
}
|
|
|
|
func (c *trayController) onExit() {
|
|
_ = c.app.Shutdown()
|
|
}
|
|
|
|
func (c *trayController) quit() {
|
|
if !c.shutdownStarted.CompareAndSwap(false, true) {
|
|
return
|
|
}
|
|
go func() {
|
|
_ = c.app.Shutdown()
|
|
systray.Quit()
|
|
}()
|
|
}
|
|
|
|
func (c *trayController) restart() {
|
|
if !c.shutdownStarted.CompareAndSwap(false, true) {
|
|
return
|
|
}
|
|
c.restartRequested.Store(true)
|
|
go func() {
|
|
_ = c.app.Shutdown()
|
|
systray.Quit()
|
|
}()
|
|
}
|
|
|
|
func (a *application) openLog() error {
|
|
appLog, _, _ := logFilePaths(a.cfg)
|
|
if appLog != "" {
|
|
if _, err := os.Stat(appLog); err == nil {
|
|
return openPath(appLog)
|
|
}
|
|
_ = os.MkdirAll(filepath.Dir(appLog), 0o750)
|
|
return openPath(filepath.Dir(appLog))
|
|
}
|
|
|
|
logDir := filepath.Join(a.cfg.App.DataDir, "logs")
|
|
if err := os.MkdirAll(logDir, 0o750); err != nil {
|
|
return err
|
|
}
|
|
return openPath(logDir)
|
|
}
|
|
|
|
func prepareWorkingDirectory() error {
|
|
exe, err := os.Executable()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
exeDir := filepath.Dir(exe)
|
|
cwd, err := os.Getwd()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if samePath(exeDir, cwd) || looksLikeProjectDirectory(cwd) {
|
|
return nil
|
|
}
|
|
return os.Chdir(exeDir)
|
|
}
|
|
|
|
func looksLikeProjectDirectory(dir string) bool {
|
|
for _, name := range []string{"go.mod", "config.yaml", "data", filepath.Join("web", "dist")} {
|
|
if _, err := os.Stat(filepath.Join(dir, name)); err == nil {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func samePath(left, right string) bool {
|
|
return strings.EqualFold(filepath.Clean(left), filepath.Clean(right))
|
|
}
|
|
|
|
type singleInstance struct {
|
|
handle windows.Handle
|
|
}
|
|
|
|
func acquireSingleInstance() (*singleInstance, error) {
|
|
name, err := windows.UTF16PtrFromString(singleInstanceName)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
handle, err := windows.CreateMutex(nil, false, name)
|
|
if errors.Is(err, windows.ERROR_ALREADY_EXISTS) {
|
|
if handle != 0 {
|
|
_ = windows.CloseHandle(handle)
|
|
}
|
|
return nil, errAlreadyRunning
|
|
}
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &singleInstance{handle: handle}, nil
|
|
}
|
|
|
|
func (s *singleInstance) Close() error {
|
|
if s == nil || s.handle == 0 {
|
|
return nil
|
|
}
|
|
err := windows.CloseHandle(s.handle)
|
|
s.handle = 0
|
|
return err
|
|
}
|
|
|
|
func launchSelf() error {
|
|
exe, err := os.Executable()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
cwd, err := os.Getwd()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
cmd := exec.Command(exe, os.Args[1:]...)
|
|
cmd.Dir = cwd
|
|
cmd.Env = os.Environ()
|
|
cmd.SysProcAttr = &syscall.SysProcAttr{
|
|
HideWindow: true,
|
|
CreationFlags: windows.CREATE_NEW_PROCESS_GROUP | windows.DETACHED_PROCESS,
|
|
}
|
|
if err := cmd.Start(); err != nil {
|
|
return err
|
|
}
|
|
return cmd.Process.Release()
|
|
}
|
|
|
|
func autoStartEnabled() bool {
|
|
key, err := registry.OpenKey(registry.CURRENT_USER, runRegistryKey, registry.QUERY_VALUE)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
defer key.Close()
|
|
|
|
value, _, err := key.GetStringValue(runRegistryValue)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
exe, err := os.Executable()
|
|
if err != nil {
|
|
return false
|
|
}
|
|
return strings.EqualFold(strings.TrimSpace(strings.Trim(value, `"`)), filepath.Clean(exe))
|
|
}
|
|
|
|
func setAutoStart(enabled bool) error {
|
|
key, _, err := registry.CreateKey(registry.CURRENT_USER, runRegistryKey, registry.SET_VALUE)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer key.Close()
|
|
|
|
if !enabled {
|
|
if err := key.DeleteValue(runRegistryValue); err != nil && !errors.Is(err, registry.ErrNotExist) {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
exe, err := os.Executable()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return key.SetStringValue(runRegistryValue, syscall.EscapeArg(filepath.Clean(exe)))
|
|
}
|
|
|
|
func openURL(url string) error {
|
|
return shellOpen(url)
|
|
}
|
|
|
|
func openPath(path string) error {
|
|
return shellOpen(path)
|
|
}
|
|
|
|
func shellOpen(target string) error {
|
|
targetPtr, err := windows.UTF16PtrFromString(target)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
verbPtr, err := windows.UTF16PtrFromString("open")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return windows.ShellExecute(0, verbPtr, targetPtr, nil, nil, 1)
|
|
}
|
|
|
|
func reportError(title string, err error) {
|
|
if err == nil {
|
|
return
|
|
}
|
|
text, textErr := windows.UTF16PtrFromString(title + "\r\n\r\n" + err.Error())
|
|
if textErr != nil {
|
|
return
|
|
}
|
|
caption, captionErr := windows.UTF16PtrFromString("MeBox")
|
|
if captionErr != nil {
|
|
return
|
|
}
|
|
_, _ = windows.MessageBox(0, text, caption, windows.MB_OK|windows.MB_ICONERROR|windows.MB_SETFOREGROUND)
|
|
}
|