github.com/keybase/client/go@v0.0.0-20240309051027-028f7c731f8b/install/install_windows.go (about)

     1  // Copyright 2015 Keybase, Inc. All rights reserved. Use of
     2  // this source code is governed by the included BSD license.
     3  
     4  package install
     5  
     6  import (
     7  	"bufio"
     8  	"bytes"
     9  	"context"
    10  	"fmt"
    11  	"io"
    12  	"os"
    13  	"os/exec"
    14  	"path/filepath"
    15  	"sort"
    16  	"strconv"
    17  	"strings"
    18  	"time"
    19  
    20  	"golang.org/x/sys/windows/registry"
    21  	"golang.org/x/text/encoding/unicode"
    22  	"golang.org/x/text/transform"
    23  
    24  	"github.com/keybase/client/go/libkb"
    25  	"github.com/keybase/client/go/logger"
    26  	keybase1 "github.com/keybase/client/go/protocol/keybase1"
    27  )
    28  
    29  // Install only handles the driver part on Windows
    30  func Install(context Context, binPath string, sourcePath string, components []string, force bool, timeout time.Duration, log Log) keybase1.InstallResult {
    31  	return keybase1.InstallResult{}
    32  }
    33  
    34  // AutoInstall is not supported on Windows
    35  func AutoInstall(context Context, binPath string, force bool, timeout time.Duration, log Log) (bool, error) {
    36  	return false, nil
    37  }
    38  
    39  // Uninstall empty implementation for unsupported platforms
    40  func Uninstall(context Context, components []string, log Log) keybase1.UninstallResult {
    41  	return keybase1.UninstallResult{}
    42  }
    43  
    44  // CheckIfValidLocation is not supported on Windows
    45  func CheckIfValidLocation() *keybase1.Error {
    46  	return nil
    47  }
    48  
    49  // KBFSBinPath returns the path to the KBFS executable
    50  func KBFSBinPath(runMode libkb.RunMode, binPath string) (string, error) {
    51  	return kbfsBinPathDefault(runMode, binPath)
    52  }
    53  
    54  func kbfsBinName() string {
    55  	return "kbfsdokan.exe"
    56  }
    57  
    58  func updaterBinName() (string, error) {
    59  	// Can't name it updater.exe because of Windows "Install Detection Heuristic",
    60  	// which is complete and total BULLSHIT LOL:
    61  	// https://technet.microsoft.com/en-us/library/cc709628%28v=ws.10%29.aspx?f=255&MSPPError=-2147217396
    62  	return "upd.exe", nil
    63  }
    64  
    65  func rqBinPath() (string, error) {
    66  	path, err := BinPath()
    67  	if err != nil {
    68  		return "", err
    69  	}
    70  	return filepath.Join(filepath.Dir(path), "keybaserq.exe"), nil
    71  }
    72  
    73  // RunApp starts the app
    74  func RunApp(context Context, log Log) error {
    75  	// TODO: Start the app
    76  	return nil
    77  }
    78  
    79  type utfScanner interface {
    80  	Read(p []byte) (n int, err error)
    81  }
    82  
    83  // newScannerUTF16or8 creates a scanner similar to os.Open() but decodes
    84  // the file as UTF-16 if the special byte order mark is present.
    85  func newScannerUTF16or8(filename string) (utfScanner, error) {
    86  	file, err := os.Open(filename)
    87  	if err != nil {
    88  		return nil, err
    89  	}
    90  
    91  	// Check for BOM
    92  	marker := make([]byte, 2)
    93  	numread, err := io.ReadAtLeast(file, marker, 2)
    94  	file.Seek(0, 0)
    95  	if numread == 2 && err == nil && ((marker[0] == 0xFE && marker[1] == 0xFF) || (marker[0] == 0xFF && marker[1] == 0xFE)) {
    96  		// Make an tranformer that converts MS-Win default to UTF8:
    97  		win16be := unicode.UTF16(unicode.BigEndian, unicode.UseBOM)
    98  		// Make a transformer that is like win16be, but abides by BOM:
    99  		utf16bom := unicode.BOMOverride(win16be.NewDecoder())
   100  
   101  		// Make a Reader that uses utf16bom:
   102  		unicodeReader := transform.NewReader(file, utf16bom)
   103  		return unicodeReader, nil
   104  	}
   105  	return file, nil
   106  }
   107  
   108  // InstallLogPath combines a handful of install logs in to one for
   109  // server upload.
   110  // Unfortunately, Dokan can generate UTF16 logs, so we test each file
   111  // and translate if necessary.
   112  func InstallLogPath() (string, error) {
   113  	// Get the 3 newest keybase logs - sorting by name works because timestamp
   114  	keybaseLogFiles, keybaseFetchLogErr := filepath.Glob(os.ExpandEnv(filepath.Join("${TEMP}", "Keybase*.log")))
   115  	sort.Sort(sort.Reverse(sort.StringSlice(keybaseLogFiles)))
   116  	if len(keybaseLogFiles) > 6 {
   117  		keybaseLogFiles = keybaseLogFiles[:6]
   118  	}
   119  
   120  	// Get the latest msi log (in the app data temp dir) for a keybase install
   121  	msiLogPattern := os.ExpandEnv(filepath.Join("${TEMP}", "MSI*.LOG"))
   122  	msiLogFile, msiFetchLogErr := LastModifiedMatchingFile(msiLogPattern, "Keybase")
   123  	if msiLogFile != nil {
   124  		keybaseLogFiles = append(keybaseLogFiles, *msiLogFile)
   125  	}
   126  
   127  	// Get the 2 newest dokan logs - sorting by name works because timestamp
   128  	dokanLogFiles, dokanFetchLogErr := filepath.Glob(os.ExpandEnv(filepath.Join("${TEMP}", "Dokan*.log")))
   129  	sort.Sort(sort.Reverse(sort.StringSlice(dokanLogFiles)))
   130  	if len(dokanLogFiles) > 2 {
   131  		dokanLogFiles = dokanLogFiles[:2]
   132  	}
   133  	keybaseLogFiles = append(keybaseLogFiles, dokanLogFiles...)
   134  
   135  	logName, logFile, err := libkb.OpenTempFile("KeybaseInstallUpload", ".log", 0)
   136  	defer logFile.Close()
   137  	if err != nil {
   138  		return "", err
   139  	}
   140  
   141  	if msiFetchLogErr != nil {
   142  		fmt.Fprintf(logFile, "  --- error fetching msi log %v---\n", msiFetchLogErr)
   143  	}
   144  	if keybaseFetchLogErr != nil {
   145  		fmt.Fprintf(logFile, "  --- error fetching keybase install log %v---\n", keybaseFetchLogErr)
   146  	}
   147  	if dokanFetchLogErr != nil {
   148  		fmt.Fprintf(logFile, "  --- error fetching dokan log %v---\n", dokanFetchLogErr)
   149  	}
   150  
   151  	getVersionAndDrivers(logFile)
   152  
   153  	if len(keybaseLogFiles) == 0 {
   154  		fmt.Fprintf(logFile, "   --- NO INSTALL LOGS FOUND!?! ---\n")
   155  	}
   156  	for _, path := range keybaseLogFiles {
   157  		fmt.Fprintf(logFile, "   --- %s ---\n", path)
   158  
   159  		// We have to parse the contents and write them because some files need to
   160  		// be decoded from utf16
   161  		s, err := newScannerUTF16or8(path)
   162  		if err != nil {
   163  			fmt.Fprintf(logFile, "  --- NewScannerUTF16(%s) returns %v---\n", path, err)
   164  		} else {
   165  			scanner := bufio.NewScanner(s)
   166  			for scanner.Scan() {
   167  				fmt.Fprintln(logFile, scanner.Text()) // Println will add back the final '\n'
   168  			}
   169  			if err := scanner.Err(); err != nil {
   170  				fmt.Fprintf(logFile, "  --- error reading (%s): %v---\n", path, err)
   171  			}
   172  		}
   173  		fmt.Fprint(logFile, "\n\n")
   174  	}
   175  
   176  	return logName, err
   177  }
   178  
   179  // WatchdogLogPath combines a handful of watchdog logs in to one for
   180  // server upload.
   181  func WatchdogLogPath(logGlobPath string) (string, error) {
   182  	// Get the 5 newest watchdog logs - sorting by name works because timestamp
   183  	watchdogLogFiles, err := filepath.Glob(logGlobPath)
   184  	sort.Sort(sort.Reverse(sort.StringSlice(watchdogLogFiles)))
   185  	if len(watchdogLogFiles) > 5 {
   186  		watchdogLogFiles = watchdogLogFiles[:5]
   187  	}
   188  	// resort the files so the combined file will be chronological
   189  	sort.Sort((sort.StringSlice(watchdogLogFiles)))
   190  
   191  	logName, logFile, err := libkb.OpenTempFile("KeybaseWatchdogUpload", ".log", 0)
   192  	defer logFile.Close()
   193  	if err != nil {
   194  		return "", err
   195  	}
   196  
   197  	if len(watchdogLogFiles) == 0 {
   198  		fmt.Fprintf(logFile, "   --- NO WATCHDOG LOGS FOUND!?! ---\n")
   199  	}
   200  	for _, path := range watchdogLogFiles {
   201  		fmt.Fprintf(logFile, "   --- %s ---\n", path)
   202  
   203  		// append the files
   204  		func() {
   205  			fd, err := os.Open(path)
   206  			defer fd.Close()
   207  			if err != nil {
   208  				fmt.Fprintf(logFile, "open error: %s\n", err.Error())
   209  				return
   210  			}
   211  			_, err = io.Copy(logFile, fd)
   212  			if err != nil {
   213  				fmt.Fprintf(logFile, "copy error: %s\n", err.Error())
   214  			}
   215  		}()
   216  	}
   217  
   218  	return logName, err
   219  }
   220  
   221  const autoRegPath = `Software\Microsoft\Windows\CurrentVersion\Run`
   222  const autoRegName = `Keybase.Keybase.GUI`
   223  
   224  // TODO Remove this in 2022.
   225  const autoRegDeprecatedName = `electron.app.keybase`
   226  
   227  func autostartStatus() (enabled bool, err error) {
   228  	k, err := registry.OpenKey(registry.CURRENT_USER, autoRegPath, registry.QUERY_VALUE|registry.READ)
   229  	if err != nil {
   230  		return false, fmt.Errorf("Error opening Run registry key: %v", err)
   231  	}
   232  	defer k.Close()
   233  
   234  	// Value not existing means that we are not starting up by default!
   235  	_, _, err = k.GetStringValue(autoRegName)
   236  	return err == nil, nil
   237  }
   238  
   239  func ToggleAutostart(context Context, on bool, forAutoinstallIgnored bool) error {
   240  	k, err := registry.OpenKey(registry.CURRENT_USER, autoRegPath, registry.QUERY_VALUE|registry.WRITE)
   241  	if err != nil {
   242  		return fmt.Errorf("Error opening StartupFolder registry key: %v", err)
   243  	}
   244  	defer k.Close()
   245  
   246  	// Delete old key if it exists.
   247  	// TODO Remove this in 2022.
   248  	k.DeleteValue(autoRegDeprecatedName)
   249  
   250  	if !on {
   251  		// it might not exists, don't propagate error.
   252  		k.DeleteValue(autoRegName)
   253  		return nil
   254  	}
   255  
   256  	appDataDir, err := libkb.LocalDataDir()
   257  	if err != nil {
   258  		return fmt.Errorf("Error getting AppDataDir: %v", err)
   259  	}
   260  
   261  	err = k.SetStringValue(autoRegName, appDataDir+`\Keybase\Gui\Keybase.exe`)
   262  	if err != nil {
   263  		return fmt.Errorf("Error setting registry Run value %v", err)
   264  	}
   265  	return nil
   266  }
   267  
   268  // This is the old startup info logging. Retain it for now, but it is soon useless.
   269  // TODO Remove in 2021.
   270  func deprecatedStartupInfo(logFile *os.File) {
   271  	if appDataDir, err := libkb.AppDataDir(); err != nil {
   272  		logFile.WriteString("Error getting AppDataDir\n")
   273  	} else {
   274  		if exists, err := libkb.FileExists(filepath.Join(appDataDir, "Microsoft\\Windows\\Start Menu\\Programs\\Startup\\KeybaseStartup.lnk")); err == nil && exists == false {
   275  			logFile.WriteString("  -- Service startup shortcut missing! --\n\n")
   276  		} else if err != nil {
   277  			k, err := registry.OpenKey(registry.CURRENT_USER, "Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\StartupApproved\\StartupFolder", registry.QUERY_VALUE|registry.READ)
   278  			if err != nil {
   279  				logFile.WriteString("Error opening Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\StartupApproved\\StartupFolder\n")
   280  			} else {
   281  				val, _, err := k.GetBinaryValue("KeybaseStartup.lnk")
   282  				if err == nil && len(val) > 0 && val[0] != 2 {
   283  					logFile.WriteString("  -- Service startup shortcut disabled in registry! --\n\n")
   284  				}
   285  			}
   286  		}
   287  	}
   288  }
   289  
   290  func getVersionAndDrivers(logFile *os.File) {
   291  	// Capture Windows Version
   292  	cmd := exec.Command("cmd", "ver")
   293  	cmd.Stdout = logFile
   294  	cmd.Stderr = logFile
   295  	err := cmd.Run()
   296  	if err != nil {
   297  		logFile.WriteString("Error getting version\n")
   298  	}
   299  	logFile.WriteString("\n")
   300  
   301  	// Check 64 or 32
   302  	cmd = exec.Command("reg", "query", "HKLM\\Hardware\\Description\\System\\CentralProcessor\\0")
   303  	cmd.Stdout = logFile
   304  	cmd.Stderr = logFile
   305  	err = cmd.Run()
   306  	if err != nil {
   307  		logFile.WriteString("Error getting CPU type\n")
   308  	}
   309  	logFile.WriteString("\n")
   310  
   311  	// Check whether the service shortcut is still present and not disabled
   312  	deprecatedStartupInfo(logFile)
   313  	status, err := autostartStatus()
   314  	logFile.WriteString(fmt.Sprintf("AutoStart: %v, %v\n", status, err))
   315  
   316  	// List filesystem drivers
   317  	outputBytes, err := exec.Command("driverquery").Output()
   318  	if err != nil {
   319  		fmt.Fprintf(logFile, "Error querying drivers: %v\n", err)
   320  	}
   321  	// For now, only list filesystem ones
   322  	scanner := bufio.NewScanner(bytes.NewReader(outputBytes))
   323  	for scanner.Scan() {
   324  		if strings.Contains(scanner.Text(), "File System") {
   325  			logFile.WriteString(scanner.Text() + "\n")
   326  		}
   327  	}
   328  	logFile.WriteString("\n\n")
   329  }
   330  
   331  func SystemLogPath() string {
   332  	return ""
   333  }
   334  
   335  // IsInUse returns true if the mount is in use. This may be used by the updater
   336  // to determine if it's safe to apply an update and restart.
   337  func IsInUse(mountDir string, log Log) bool {
   338  	if mountDir == "" {
   339  		return false
   340  	}
   341  	if _, serr := os.Stat(mountDir); os.IsNotExist(serr) {
   342  		log.Debug("%s doesn't exist", mountDir)
   343  		return false
   344  	}
   345  
   346  	dat, err := os.ReadFile(filepath.Join(mountDir, ".kbfs_number_of_handles"))
   347  	if err != nil {
   348  		log.Debug("Error reading kbfs handles: %s", err)
   349  		return false
   350  	}
   351  	i, err := strconv.Atoi(string(dat))
   352  	if err != nil {
   353  		log.Debug("Error converting count of kbfs handles: %s", err)
   354  		return false
   355  	}
   356  	if i > 0 {
   357  		log.Debug("Found kbfs handles in use: %d", i)
   358  		return true
   359  	}
   360  
   361  	return false
   362  }
   363  
   364  // StartUpdateIfNeeded starts to update the app if there's one available. It
   365  // calls `updater check` internally so it ignores the snooze.
   366  func StartUpdateIfNeeded(ctx context.Context, log logger.Logger) error {
   367  	rqPath, err := rqBinPath()
   368  	if err != nil {
   369  		return err
   370  	}
   371  	updaterPath, err := UpdaterBinPath()
   372  	if err != nil {
   373  		return err
   374  	}
   375  	log.Debug("Starting updater with keybaserq.exe")
   376  	if err = exec.Command(rqPath, updaterPath, "check").Run(); err != nil {
   377  		return err
   378  	}
   379  	return nil
   380  }
   381  
   382  func LsofMount(mountDir string, log Log) ([]CommonLsofResult, error) {
   383  	log.Warning("Cannot use lsof on Windows.")
   384  	return nil, fmt.Errorf("Cannot use lsof on Windows.")
   385  }
   386  
   387  // delete this function and calls to it if present after 2022
   388  func deleteDeprecatedFileIfPresent() {
   389  	// this file is no longer how we do things, and if it's present (which it shouldn't be) it could
   390  	// cause unexpected behavior
   391  	if appDataDir, err := libkb.AppDataDir(); err == nil {
   392  		autostartLinkPath := filepath.Join(appDataDir, "Microsoft\\Windows\\Start Menu\\Programs\\Startup\\KeybaseStartup.lnk")
   393  		_ = os.Remove(autostartLinkPath)
   394  	}
   395  }
   396  
   397  func GetAutostart(context Context) keybase1.OnLoginStartupStatus {
   398  	deleteDeprecatedFileIfPresent()
   399  	status, err := autostartStatus()
   400  	if err != nil {
   401  		return keybase1.OnLoginStartupStatus_UNKNOWN
   402  	}
   403  	if status {
   404  		return keybase1.OnLoginStartupStatus_ENABLED
   405  	}
   406  	return keybase1.OnLoginStartupStatus_DISABLED
   407  }