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