| 1 | //go:build windows |
| 2 | |
| 3 | package desktoplauncher |
| 4 | |
| 5 | import ( |
| 6 | "fmt" |
| 7 | "io" |
| 8 | "os" |
| 9 | "os/exec" |
| 10 | "path/filepath" |
| 11 | |
| 12 | "reasonix/internal/config" |
| 13 | "reasonix/internal/desktopinstance" |
| 14 | "reasonix/internal/installlayout" |
| 15 | "reasonix/internal/proc" |
| 16 | ) |
| 17 | |
| 18 | func coordinatedLaunch(root string, args []string) (bool, int) { |
| 19 | interactive := os.Getenv("REASONIX_NONINTERACTIVE") != "1" |
| 20 | return coordinatedLaunchWith(root, args, interactive, coordinatedLaunchDependencies{ |
| 21 | classifyLocation: classifyLaunchLocation, |
| 22 | launchAndVerify: desktopinstance.LaunchAndVerify, |
| 23 | notify: desktopinstance.Notify, |
| 24 | logRejected: desktopinstance.LogPortableLocationRejected, |
| 25 | stderr: os.Stderr, |
| 26 | }) |
| 27 | } |
| 28 | |
| 29 | type coordinatedLaunchDependencies struct { |
| 30 | classifyLocation func(string) (launchLocation, error) |
| 31 | launchAndVerify func(string, string, bool, func() error, ...string) error |
| 32 | notify func(error) |
| 33 | logRejected func(string, string, bool) |
| 34 | stderr io.Writer |
| 35 | } |
| 36 | |
| 37 | func coordinatedLaunchWith(root string, args []string, interactive bool, deps coordinatedLaunchDependencies) (bool, int) { |
| 38 | // Shell-less versioned layouts (pre-shell releases, partial rollbacks) |
| 39 | // carry no shell lifecycle to coordinate or verify; launch directly. |
| 40 | if !installlayout.HasCurrent(root) || !installlayout.HasActiveShell(root) { |
| 41 | return false, 0 |
| 42 | } |
| 43 | home := config.ReasonixHomeDir() |
| 44 | if isPortableRoot(root) { |
| 45 | location, err := deps.classifyLocation(root) |
| 46 | if err != nil { |
| 47 | return coordinatedLaunchError(err, interactive, deps) |
| 48 | } |
| 49 | if location == launchLocationUNC || location == launchLocationRemoteDrive { |
| 50 | err := desktopinstance.NewUnsupportedPortableLocationError(string(location)) |
| 51 | deps.logRejected(home, string(location), interactive) |
| 52 | return coordinatedLaunchError(err, interactive, deps) |
| 53 | } |
| 54 | } |
| 55 | err := deps.launchAndVerify(root, home, interactive, func() error { |
| 56 | path, err := ResolveDesktopPath(root) |
| 57 | if err != nil { |
| 58 | return err |
| 59 | } |
| 60 | cmd := exec.Command(path, StripLegacyLaunchArgs(args)...) |
| 61 | proc.HideConsole(cmd) |
| 62 | cmd.Dir = root |
| 63 | cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, os.Stdout, os.Stderr |
| 64 | if err := cmd.Start(); err != nil { |
| 65 | return err |
| 66 | } |
| 67 | go func() { _ = cmd.Wait() }() |
| 68 | return nil |
| 69 | }, StripLegacyLaunchArgs(args)...) |
| 70 | if err != nil { |
| 71 | return coordinatedLaunchError(err, interactive, deps) |
| 72 | } |
| 73 | return true, 0 |
| 74 | } |
| 75 | |
| 76 | func coordinatedLaunchError(err error, interactive bool, deps coordinatedLaunchDependencies) (bool, int) { |
| 77 | fmt.Fprintln(deps.stderr, err) |
| 78 | if interactive { |
| 79 | deps.notify(err) |
| 80 | } |
| 81 | return true, desktopinstance.ExitCode(err) |
| 82 | } |
| 83 | |
| 84 | func isPortableRoot(root string) bool { |
| 85 | info, err := os.Lstat(filepath.Join(root, installlayout.PortableAliasName())) |
| 86 | if err != nil || !info.Mode().IsRegular() || info.Mode()&os.ModeSymlink != 0 { |
| 87 | return false |
| 88 | } |
| 89 | _, err = os.Lstat(filepath.Join(root, "uninstall.exe")) |
| 90 | return os.IsNotExist(err) |
| 91 | } |
| 92 |