github.com/keybase/client/go@v0.0.0-20240309051027-028f7c731f8b/install/fuse_status_darwin.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  //go:build darwin && !ios
     5  // +build darwin,!ios
     6  
     7  package install
     8  
     9  import (
    10  	"fmt"
    11  	"io"
    12  	"os"
    13  	"os/exec"
    14  	"path/filepath"
    15  	"regexp"
    16  	"strings"
    17  
    18  	"github.com/keybase/client/go/libkb"
    19  	keybase1 "github.com/keybase/client/go/protocol/keybase1"
    20  	"github.com/keybase/go-kext"
    21  )
    22  
    23  const installPath = "/Library/Filesystems/kbfuse.fs"
    24  
    25  // KeybaseFuseStatus returns Fuse status
    26  func KeybaseFuseStatus(bundleVersion string, log Log) keybase1.FuseStatus {
    27  	st := keybase1.FuseStatus{
    28  		BundleVersion: bundleVersion,
    29  		InstallStatus: keybase1.InstallStatus_UNKNOWN,
    30  		InstallAction: keybase1.InstallAction_UNKNOWN,
    31  	}
    32  
    33  	var kextInfo *kext.Info
    34  
    35  	if _, err := os.Stat(installPath); err == nil {
    36  		st.Path = installPath
    37  		kextID := "com.github.kbfuse.filesystems.kbfuse"
    38  		var loadErr error
    39  		kextInfo, loadErr = kext.LoadInfo(kextID)
    40  		if loadErr != nil {
    41  			st.InstallStatus = keybase1.InstallStatus_ERROR
    42  			st.InstallAction = keybase1.InstallAction_REINSTALL
    43  			st.Status = keybase1.Status{Code: libkb.SCGeneric, Name: "INSTALL_ERROR", Desc: fmt.Sprintf("Error loading kext info: %s", loadErr)}
    44  			return st
    45  		}
    46  		if kextInfo == nil {
    47  			log.Debug("No kext info available (kext not loaded)")
    48  			// This means the kext isn't loaded, which is ok, kbfs will call
    49  			// load_kbfuse when it starts up.
    50  			// We have to get the version from the installed plist.
    51  			installedVersion, fivErr := fuseInstallVersion(log)
    52  			if fivErr != nil {
    53  				st.InstallStatus = keybase1.InstallStatus_ERROR
    54  				st.InstallAction = keybase1.InstallAction_REINSTALL
    55  				st.Status = keybase1.Status{Code: libkb.SCGeneric, Name: "INSTALL_ERROR", Desc: fmt.Sprintf("Error loading (plist) info: %s", fivErr)}
    56  				return st
    57  			}
    58  			if installedVersion != "" {
    59  				kextInfo = &kext.Info{
    60  					Version: installedVersion,
    61  					Started: false,
    62  				}
    63  			}
    64  		}
    65  
    66  		// Installed
    67  		st.KextID = kextID
    68  	}
    69  
    70  	// If neither is found, we have no install
    71  	if st.KextID == "" || kextInfo == nil {
    72  		st.InstallStatus = keybase1.InstallStatus_NOT_INSTALLED
    73  		st.InstallAction = keybase1.InstallAction_INSTALL
    74  		return st
    75  	}
    76  
    77  	// Try to get mount info, it's non-critical if we fail though.
    78  	mountInfos, err := mountInfo("kbfuse")
    79  	if err != nil {
    80  		log.Errorf("Error trying to read mount info: %s", err)
    81  	}
    82  	st.MountInfos = mountInfos
    83  
    84  	st.Version = kextInfo.Version
    85  	st.KextStarted = kextInfo.Started
    86  
    87  	installStatus, installAction, status := ResolveInstallStatus(st.Version, st.BundleVersion, "", log)
    88  	st.InstallStatus = installStatus
    89  	st.InstallAction = installAction
    90  	st.Status = status
    91  
    92  	return st
    93  }
    94  
    95  func mountInfo(fstype string) ([]keybase1.FuseMountInfo, error) {
    96  	out, err := exec.Command("/sbin/mount", "-t", fstype).Output()
    97  	if err != nil {
    98  		return nil, err
    99  	}
   100  	lines := strings.Split(string(out), "\n")
   101  	mountInfos := []keybase1.FuseMountInfo{}
   102  	for _, line := range lines {
   103  		if strings.TrimSpace(line) == "" {
   104  			continue
   105  		}
   106  		info := strings.SplitN(line, " ", 4)
   107  		path := ""
   108  		if len(info) >= 2 {
   109  			path = info[2]
   110  		}
   111  		mountInfos = append(mountInfos, keybase1.FuseMountInfo{
   112  			Fstype: fstype,
   113  			Path:   path,
   114  			Output: line,
   115  		})
   116  	}
   117  	return mountInfos, nil
   118  }
   119  
   120  func findStringInPlist(key string, plistData []byte, log Log) string {
   121  	// Hack to parse plist, instead of parsing we'll use a regex
   122  	res := fmt.Sprintf(`<key>%s<\/key>\s*<string>([\S ]+)<\/string>`, key)
   123  	re := regexp.MustCompile(res)
   124  	submatch := re.FindStringSubmatch(string(plistData))
   125  	if len(submatch) == 2 {
   126  		return submatch[1]
   127  	}
   128  	log.Debug("No key (%s) found", key)
   129  	return ""
   130  }
   131  
   132  func loadPlist(plistPath string, log Log) ([]byte, error) {
   133  	if _, err := os.Stat(plistPath); os.IsNotExist(err) {
   134  		log.Debug("No plist found: %s", plistPath)
   135  		return nil, err
   136  	}
   137  	log.Debug("Loading plist: %s", plistPath)
   138  	plistFile, err := os.Open(plistPath)
   139  	defer func() {
   140  		if err := plistFile.Close(); err != nil {
   141  			log.Debug("unable to close file: %s", err)
   142  		}
   143  	}()
   144  	if err != nil {
   145  		return nil, err
   146  	}
   147  	return io.ReadAll(plistFile)
   148  }
   149  
   150  func fuseInstallVersion(log Log) (string, error) {
   151  	plistPath := filepath.Join(installPath, "Contents/Info.plist")
   152  	plistData, err := loadPlist(plistPath, log)
   153  	if err != nil {
   154  		return "", err
   155  	}
   156  	return findStringInPlist("CFBundleVersion", plistData, log), nil
   157  }