mirror of
https://github.com/tailscale/tailscale.git
synced 2024-11-25 19:15:34 +00:00
clientupdate: manually restart Windows GUI after update (#9906)
When updating via c2n, `tailscale.exe update` runs from `tailscaled.exe`
which runs as SYSTEM. The MSI installer does not start the GUI when
running as SYSTEM. This results in Tailscale just existing on
auto-update, which is ungood.
Instead, always ask the MSI installer to not launch the GUI (via
`TS_NOLAUNCH` argument) and launch it manually with a token from the
current logged in user. The token code was borrowed from
d9081d6ba2/net/dns/wsl_windows.go (L207-L232)
Also, make some logging changes so that these issues are easier to debug
in the future.
Updates #755
Signed-off-by: Andrew Lytvynov <awly@tailscale.com>
This commit is contained in:
parent
e9956419f6
commit
e561f1ce61
@ -631,24 +631,53 @@ func (up *Updater) updateMacAppStore() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// winMSIEnv is the environment variable that, if set, is the MSI file for the
|
const (
|
||||||
// update command to install. It's passed like this so we can stop the
|
// winMSIEnv is the environment variable that, if set, is the MSI file for
|
||||||
// tailscale.exe process from running before the msiexec process runs and tries
|
// the update command to install. It's passed like this so we can stop the
|
||||||
// to overwrite ourselves.
|
// tailscale.exe process from running before the msiexec process runs and
|
||||||
const winMSIEnv = "TS_UPDATE_WIN_MSI"
|
// 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"
|
||||||
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
verifyAuthenticode func(string) error // or nil on non-Windows
|
verifyAuthenticode func(string) error // or nil on non-Windows
|
||||||
markTempFileFunc func(string) error // or nil on non-Windows
|
markTempFileFunc func(string) error // or nil on non-Windows
|
||||||
|
launchTailscaleAsWinGUIUser func(string) error // or nil on non-Windows
|
||||||
)
|
)
|
||||||
|
|
||||||
func (up *Updater) updateWindows() error {
|
func (up *Updater) updateWindows() error {
|
||||||
if msi := os.Getenv(winMSIEnv); msi != "" {
|
if msi := os.Getenv(winMSIEnv); msi != "" {
|
||||||
|
// stdout/stderr from this part of the install could be lost since the
|
||||||
|
// parent tailscaled is replaced. Create a temp log file to have some
|
||||||
|
// output to debug with in case update fails.
|
||||||
|
close, err := up.switchOutputToFile()
|
||||||
|
if err != nil {
|
||||||
|
up.Logf("failed to create log file for installation: %v; proceeding with existing outputs", err)
|
||||||
|
} else {
|
||||||
|
defer close.Close()
|
||||||
|
}
|
||||||
|
|
||||||
up.Logf("installing %v ...", msi)
|
up.Logf("installing %v ...", msi)
|
||||||
if err := up.installMSI(msi); err != nil {
|
if err := up.installMSI(msi); err != nil {
|
||||||
up.Logf("MSI install failed: %v", err)
|
up.Logf("MSI install failed: %v", err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
up.Logf("relaunching tailscale-ipn.exe...")
|
||||||
|
exePath := os.Getenv(winExePathEnv)
|
||||||
|
if exePath == "" {
|
||||||
|
up.Logf("env var %q not passed to installer binary copy", winExePathEnv)
|
||||||
|
return fmt.Errorf("env var %q not passed to installer binary copy", winExePathEnv)
|
||||||
|
}
|
||||||
|
if err := launchTailscaleAsWinGUIUser(exePath); err != nil {
|
||||||
|
up.Logf("Failed to re-launch tailscale after update: %v", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
up.Logf("success.")
|
up.Logf("success.")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@ -691,7 +720,7 @@ func (up *Updater) updateWindows() error {
|
|||||||
up.Logf("authenticode verification succeeded")
|
up.Logf("authenticode verification succeeded")
|
||||||
|
|
||||||
up.Logf("making tailscale.exe copy to switch to...")
|
up.Logf("making tailscale.exe copy to switch to...")
|
||||||
selfCopy, err := makeSelfCopy()
|
selfOrig, selfCopy, err := makeSelfCopy()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -699,7 +728,7 @@ func (up *Updater) updateWindows() error {
|
|||||||
up.Logf("running tailscale.exe copy for final install...")
|
up.Logf("running tailscale.exe copy for final install...")
|
||||||
|
|
||||||
cmd := exec.Command(selfCopy, "update")
|
cmd := exec.Command(selfCopy, "update")
|
||||||
cmd.Env = append(os.Environ(), winMSIEnv+"="+msiTarget)
|
cmd.Env = append(os.Environ(), winMSIEnv+"="+msiTarget, winExePathEnv+"="+selfOrig)
|
||||||
cmd.Stdout = up.Stderr
|
cmd.Stdout = up.Stderr
|
||||||
cmd.Stderr = up.Stderr
|
cmd.Stderr = up.Stderr
|
||||||
cmd.Stdin = os.Stdin
|
cmd.Stdin = os.Stdin
|
||||||
@ -712,10 +741,35 @@ func (up *Updater) updateWindows() error {
|
|||||||
panic("unreachable")
|
panic("unreachable")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (up *Updater) switchOutputToFile() (io.Closer, error) {
|
||||||
|
var logFilePath string
|
||||||
|
exePath, err := os.Executable()
|
||||||
|
if err != nil {
|
||||||
|
logFilePath = filepath.Join(os.TempDir(), "tailscale-updater.log")
|
||||||
|
} else {
|
||||||
|
logFilePath = strings.TrimSuffix(exePath, ".exe") + ".log"
|
||||||
|
}
|
||||||
|
|
||||||
|
up.Logf("writing update output to %q", logFilePath)
|
||||||
|
logFile, err := os.Create(logFilePath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
up.Logf = func(m string, args ...any) {
|
||||||
|
fmt.Fprintf(logFile, m+"\n", args...)
|
||||||
|
}
|
||||||
|
up.Stdout = logFile
|
||||||
|
up.Stderr = logFile
|
||||||
|
return logFile, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (up *Updater) installMSI(msi string) error {
|
func (up *Updater) installMSI(msi string) error {
|
||||||
var err error
|
var err error
|
||||||
for tries := 0; tries < 2; tries++ {
|
for tries := 0; tries < 2; tries++ {
|
||||||
cmd := exec.Command("msiexec.exe", "/i", filepath.Base(msi), "/quiet", "/promptrestart", "/qn")
|
// TS_NOLAUNCH: don't automatically launch the app after install.
|
||||||
|
// We will launch it explicitly as the current GUI user afterwards.
|
||||||
|
cmd := exec.Command("msiexec.exe", "/i", filepath.Base(msi), "/quiet", "/promptrestart", "/qn", "TS_NOLAUNCH=true")
|
||||||
cmd.Dir = filepath.Dir(msi)
|
cmd.Dir = filepath.Dir(msi)
|
||||||
cmd.Stdout = up.Stdout
|
cmd.Stdout = up.Stdout
|
||||||
cmd.Stderr = up.Stderr
|
cmd.Stderr = up.Stderr
|
||||||
@ -724,6 +778,7 @@ func (up *Updater) installMSI(msi string) error {
|
|||||||
if err == nil {
|
if err == nil {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
up.Logf("Install attempt failed: %v", err)
|
||||||
uninstallVersion := version.Short()
|
uninstallVersion := version.Short()
|
||||||
if v := os.Getenv("TS_DEBUG_UNINSTALL_VERSION"); v != "" {
|
if v := os.Getenv("TS_DEBUG_UNINSTALL_VERSION"); v != "" {
|
||||||
uninstallVersion = v
|
uninstallVersion = v
|
||||||
@ -753,30 +808,30 @@ func msiUUIDForVersion(ver string) string {
|
|||||||
return "{" + strings.ToUpper(uuid.NewSHA1(uuid.NameSpaceURL, []byte(msiURL)).String()) + "}"
|
return "{" + strings.ToUpper(uuid.NewSHA1(uuid.NameSpaceURL, []byte(msiURL)).String()) + "}"
|
||||||
}
|
}
|
||||||
|
|
||||||
func makeSelfCopy() (tmpPathExe string, err error) {
|
func makeSelfCopy() (origPathExe, tmpPathExe string, err error) {
|
||||||
selfExe, err := os.Executable()
|
selfExe, err := os.Executable()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", "", err
|
||||||
}
|
}
|
||||||
f, err := os.Open(selfExe)
|
f, err := os.Open(selfExe)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", "", err
|
||||||
}
|
}
|
||||||
defer f.Close()
|
defer f.Close()
|
||||||
f2, err := os.CreateTemp("", "tailscale-updater-*.exe")
|
f2, err := os.CreateTemp("", "tailscale-updater-*.exe")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", "", err
|
||||||
}
|
}
|
||||||
if f := markTempFileFunc; f != nil {
|
if f := markTempFileFunc; f != nil {
|
||||||
if err := f(f2.Name()); err != nil {
|
if err := f(f2.Name()); err != nil {
|
||||||
return "", err
|
return "", "", err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if _, err := io.Copy(f2, f); err != nil {
|
if _, err := io.Copy(f2, f); err != nil {
|
||||||
f2.Close()
|
f2.Close()
|
||||||
return "", err
|
return "", "", err
|
||||||
}
|
}
|
||||||
return f2.Name(), f2.Close()
|
return selfExe, f2.Name(), f2.Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (up *Updater) downloadURLToFile(pathSrc, fileDst string) (ret error) {
|
func (up *Updater) downloadURLToFile(pathSrc, fileDst string) (ret error) {
|
||||||
|
@ -7,13 +7,20 @@
|
|||||||
package clientupdate
|
package clientupdate
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"os/exec"
|
||||||
|
"os/user"
|
||||||
|
"path/filepath"
|
||||||
|
"syscall"
|
||||||
|
|
||||||
"golang.org/x/sys/windows"
|
"golang.org/x/sys/windows"
|
||||||
|
"tailscale.com/util/winutil"
|
||||||
"tailscale.com/util/winutil/authenticode"
|
"tailscale.com/util/winutil/authenticode"
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
markTempFileFunc = markTempFileWindows
|
markTempFileFunc = markTempFileWindows
|
||||||
verifyAuthenticode = verifyTailscale
|
verifyAuthenticode = verifyTailscale
|
||||||
|
launchTailscaleAsWinGUIUser = launchTailscaleAsGUIUser
|
||||||
}
|
}
|
||||||
|
|
||||||
func markTempFileWindows(name string) error {
|
func markTempFileWindows(name string) error {
|
||||||
@ -26,3 +33,25 @@ func markTempFileWindows(name string) error {
|
|||||||
func verifyTailscale(path string) error {
|
func verifyTailscale(path string) error {
|
||||||
return authenticode.Verify(path, certSubjectTailscale)
|
return authenticode.Verify(path, certSubjectTailscale)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func launchTailscaleAsGUIUser(exePath string) error {
|
||||||
|
exePath = filepath.Join(filepath.Dir(exePath), "tailscale-ipn.exe")
|
||||||
|
|
||||||
|
var token windows.Token
|
||||||
|
if u, err := user.Current(); err == nil && u.Name == "SYSTEM" {
|
||||||
|
sessionID := winutil.WTSGetActiveConsoleSessionId()
|
||||||
|
if sessionID != 0xFFFFFFFF {
|
||||||
|
if err := windows.WTSQueryUserToken(sessionID, &token); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer token.Close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd := exec.Command(exePath)
|
||||||
|
cmd.SysProcAttr = &syscall.SysProcAttr{
|
||||||
|
Token: syscall.Token(token),
|
||||||
|
HideWindow: true,
|
||||||
|
}
|
||||||
|
return cmd.Start()
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user