clientupdate, util/osshare, util/winutil, version: improve Windows GUI filename resolution and WinUI build awareness

On Windows arm64 we are going to need to ship two different GUI builds;
one for Win10 (GOARCH=386) and one for Win11 (GOARCH=amd64, tags +=
winui). Due to quirks in MSI packaging, they cannot both share the
same filename. This requires some fixes in places where we have
hardcoded "tailscale-ipn" as the GUI filename.

We also do some cleanup in clientupdate to ensure that autoupdates
will continue to work correctly with the temporary "-winui" package
variant.

Fixes #17480
Updates https://github.com/tailscale/corp/issues/29940

Signed-off-by: Aaron Klotz <aaron@tailscale.com>
This commit is contained in:
Aaron Klotz
2025-09-29 11:44:23 -06:00
parent e45557afc0
commit 7c49cab1a6
7 changed files with 146 additions and 33 deletions

View File

@@ -30,11 +30,6 @@ const (
// tailscale.exe process from running before the msiexec process runs and
// tries to overwrite ourselves.
winMSIEnv = "TS_UPDATE_WIN_MSI"
// winExePathEnv is the environment variable that is set along with
// winMSIEnv and carries the full path of the calling tailscale.exe binary.
// It is used to re-launch the GUI process (tailscale-ipn.exe) after
// install is complete.
winExePathEnv = "TS_UPDATE_WIN_EXE_PATH"
// winVersionEnv is the environment variable that is set along with
// winMSIEnv and carries the version of tailscale that is being installed.
// It is used for logging purposes.
@@ -78,6 +73,17 @@ func verifyAuthenticode(path string) error {
return authenticode.Verify(path, certSubjectTailscale)
}
func isTSGUIPresent() bool {
us, err := os.Executable()
if err != nil {
return false
}
tsgui := filepath.Join(filepath.Dir(us), "tsgui.dll")
_, err = os.Stat(tsgui)
return err == nil
}
func (up *Updater) updateWindows() error {
if msi := os.Getenv(winMSIEnv); msi != "" {
// stdout/stderr from this part of the install could be lost since the
@@ -131,7 +137,15 @@ you can run the command prompt as Administrator one of these ways:
return err
}
up.cleanupOldDownloads(filepath.Join(msiDir, "*.msi"))
pkgsPath := fmt.Sprintf("%s/tailscale-setup-%s-%s.msi", up.Track, ver, arch)
qualifiers := []string{ver, arch}
// TODO(aaron): Temporary hack so autoupdate still works on winui builds;
// remove when we enable winui by default on the unstable track.
if isTSGUIPresent() {
qualifiers = append(qualifiers, "winui")
}
pkgsPath := fmt.Sprintf("%s/tailscale-setup-%s.msi", up.Track, strings.Join(qualifiers, "-"))
msiTarget := filepath.Join(msiDir, path.Base(pkgsPath))
if err := up.downloadURLToFile(pkgsPath, msiTarget); err != nil {
return err
@@ -145,7 +159,7 @@ you can run the command prompt as Administrator one of these ways:
up.Logf("making tailscale.exe copy to switch to...")
up.cleanupOldDownloads(filepath.Join(os.TempDir(), updaterPrefix+"-*.exe"))
selfOrig, selfCopy, err := makeSelfCopy()
_, selfCopy, err := makeSelfCopy()
if err != nil {
return err
}
@@ -153,7 +167,7 @@ you can run the command prompt as Administrator one of these ways:
up.Logf("running tailscale.exe copy for final install...")
cmd := exec.Command(selfCopy, "update")
cmd.Env = append(os.Environ(), winMSIEnv+"="+msiTarget, winExePathEnv+"="+selfOrig, winVersionEnv+"="+ver)
cmd.Env = append(os.Environ(), winMSIEnv+"="+msiTarget, winVersionEnv+"="+ver)
cmd.Stdout = up.Stderr
cmd.Stderr = up.Stderr
cmd.Stdin = os.Stdin
@@ -189,7 +203,7 @@ func (up *Updater) installMSI(msi string) error {
case windows.ERROR_SUCCESS_REBOOT_REQUIRED:
// In most cases, updating Tailscale should not require a reboot.
// If it does, it might be because we failed to close the GUI
// and the installer couldn't replace tailscale-ipn.exe.
// and the installer couldn't replace its executable.
// The old GUI will continue to run until the next reboot.
// Not ideal, but also not a retryable error.
up.Logf("[unexpected] reboot required")