github.com/keybase/client/go@v0.0.0-20240309051027-028f7c731f8b/kbfs/libfs/remote_status.go (about) 1 // Copyright 2016 Keybase Inc. All rights reserved. 2 // Use of this source code is governed by a BSD 3 // license that can be found in the LICENSE file. 4 5 package libfs 6 7 import ( 8 "runtime" 9 "strings" 10 "sync" 11 "time" 12 13 "github.com/keybase/client/go/kbfs/libkbfs" 14 kbname "github.com/keybase/client/go/kbun" 15 "github.com/keybase/client/go/libkb" 16 "github.com/keybase/client/go/logger" 17 "golang.org/x/net/context" 18 ) 19 20 // Special files in root directory. 21 const ( 22 HumanErrorFileName = "kbfs.error.txt" 23 HumanNoLoginFileName = "kbfs.nologin.txt" 24 failureDisplayThreshold = 5 * time.Second 25 ) 26 27 // RemoteStatusUpdater has callbacks that will be called from libfs 28 // when kbfs status changes in interesting ways. 29 type RemoteStatusUpdater interface { 30 // UserChanged is called when the kbfs user is changed. 31 // Either oldName or newName, or both may be empty. 32 UserChanged(ctx context.Context, oldName, newName kbname.NormalizedUsername) 33 } 34 35 // RemoteStatus is for maintaining status of various remote connections like keybase 36 // service and md-server. 37 type RemoteStatus struct { 38 sync.Mutex 39 currentUser kbname.NormalizedUsername 40 failingServices map[string]error 41 extraFileName string 42 extraFileContents []byte 43 failingSince time.Time 44 callbacks RemoteStatusUpdater 45 } 46 47 // Init a RemoteStatus and register it with libkbfs. 48 func (r *RemoteStatus) Init(ctx context.Context, log logger.Logger, config libkbfs.Config, rs RemoteStatusUpdater) { 49 r.failingServices = map[string]error{} 50 r.callbacks = rs 51 // A time in the far past that is not IsZero 52 r.failingSince.Add(time.Second) 53 go r.loop(ctx, log, config) 54 } 55 56 func (r *RemoteStatus) loop(ctx context.Context, log logger.Logger, config libkbfs.Config) { 57 for { 58 tctx, cancel := context.WithTimeout(ctx, 1*time.Second) 59 st, ch, err := config.KBFSOps().Status(tctx) 60 // No deferring inside loops, and no panics either here. 61 cancel() 62 if err != nil { 63 log.Warning("KBFS Status failed: %v,%v,%v", st, ch, err) 64 } 65 r.update(ctx, st) 66 // Block on the channel or shutdown. 67 select { 68 case <-ctx.Done(): 69 return 70 case <-ch: 71 } 72 } 73 } 74 75 func (r *RemoteStatus) update(ctx context.Context, st libkbfs.KBFSStatus) { 76 r.Lock() 77 defer r.Unlock() 78 79 if newUser := kbname.NormalizedUsername(st.CurrentUser); r.currentUser != newUser { 80 oldUser := r.currentUser 81 r.currentUser = newUser 82 if r.callbacks != nil { 83 go r.callbacks.UserChanged(ctx, oldUser, newUser) 84 } 85 } 86 87 r.failingServices = st.FailingServices 88 89 // Select the file name: 90 // + Default to an empty name 91 // + If all errors are LoginRequiredError then display the nologin filename 92 // + If there are any other errors use the generic error file name, 93 // implemented by breaking out of the loop if such an error is encountered. 94 fname := "" 95 for _, err := range r.failingServices { 96 if isNotLoggedInError(err) { 97 fname = HumanNoLoginFileName 98 } else { 99 fname = HumanErrorFileName 100 break 101 } 102 } 103 isZeroTime := r.failingSince.IsZero() 104 if len(r.failingServices) > 0 { 105 if isZeroTime { 106 r.failingSince = time.Now() 107 } 108 } else if !isZeroTime { 109 r.failingSince = time.Time{} 110 } 111 r.extraFileName = fname 112 r.extraFileContents = nil 113 } 114 115 func isNotLoggedInError(err error) bool { 116 _, ok := err.(libkb.LoginRequiredError) 117 return ok 118 } 119 120 var newline = func() string { 121 if runtime.GOOS == "windows" { 122 return "\r\n" 123 } 124 return "\n" 125 }() 126 127 // ExtraFileName returns the extra file name or an empty string for none. 128 func (r *RemoteStatus) ExtraFileName() string { 129 r.Lock() 130 defer r.Unlock() 131 132 if r.extraFileName == "" || time.Since(r.failingSince) < failureDisplayThreshold { 133 return "" 134 } 135 136 return r.extraFileName 137 } 138 139 // ExtraFileNameAndSize returns the extra file name or an empty string for none and the size of the extra file. 140 func (r *RemoteStatus) ExtraFileNameAndSize() (string, int64) { 141 r.Lock() 142 defer r.Unlock() 143 144 if r.extraFileName == "" || time.Since(r.failingSince) < failureDisplayThreshold { 145 return "", 0 146 } 147 148 return r.extraFileName, int64(len(r.humanReadableBytesLocked())) 149 } 150 151 // humanReadableBytesNeedsLock should be called with lock already held. 152 func (r *RemoteStatus) humanReadableBytesLocked() []byte { 153 if r.extraFileContents != nil { 154 return r.extraFileContents 155 } 156 157 var ss []string 158 needLogin := false 159 for service, err := range r.failingServices { 160 switch err.(type) { 161 case *libkb.LoginRequiredError: 162 needLogin = true 163 default: 164 ss = append(ss, service+": "+err.Error()) 165 } 166 } 167 168 if len(ss) == 0 { 169 if needLogin { 170 ss = append(ss, "Not logged in") 171 } else { 172 ss = append(ss, "Everything appears ok") 173 } 174 } 175 ss = append(ss, "") 176 177 res := []byte(strings.Join(ss, newline)) 178 r.extraFileContents = res 179 return res 180 } 181 182 // NewSpecialReadFunc implements a special read file that contains human readable 183 // current status. 184 func (r *RemoteStatus) NewSpecialReadFunc(ctx context.Context) ([]byte, time.Time, error) { 185 r.Lock() 186 defer r.Unlock() 187 188 return r.humanReadableBytesLocked(), time.Time{}, nil 189 }