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  }