github.com/keybase/client/go@v0.0.0-20240309051027-028f7c731f8b/kbfs/libkbfs/reporter_kbpki.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 libkbfs
     6  
     7  import (
     8  	"fmt"
     9  	"strconv"
    10  	"sync"
    11  	"time"
    12  
    13  	"github.com/keybase/client/go/kbfs/data"
    14  	"github.com/keybase/client/go/kbfs/idutil"
    15  	"github.com/keybase/client/go/kbfs/kbfsmd"
    16  	"github.com/keybase/client/go/kbfs/tlf"
    17  	"github.com/keybase/client/go/kbfs/tlfhandle"
    18  	"github.com/keybase/client/go/libkb"
    19  	"github.com/keybase/client/go/logger"
    20  	"github.com/keybase/client/go/protocol/keybase1"
    21  	"github.com/pkg/errors"
    22  	"golang.org/x/net/context"
    23  )
    24  
    25  const (
    26  	// error param keys
    27  	errorParamTlf                 = "tlf"
    28  	errorParamMode                = "mode"
    29  	errorParamUsername            = "username"
    30  	errorParamRekeySelf           = "rekeyself"
    31  	errorParamUsageBytes          = "usageBytes"
    32  	errorParamLimitBytes          = "limitBytes"
    33  	errorParamUsageFiles          = "usageFiles"
    34  	errorParamLimitFiles          = "limitFiles"
    35  	errorParamFoldersCreated      = "foldersCreated"
    36  	errorParamFolderLimit         = "folderLimit"
    37  	errorParamApplicationExecPath = "applicationExecPath"
    38  
    39  	// error operation modes
    40  	errorModeRead  = "read"
    41  	errorModeWrite = "write"
    42  )
    43  
    44  const connectionStatusConnected keybase1.FSStatusCode = keybase1.FSStatusCode_START
    45  const connectionStatusDisconnected keybase1.FSStatusCode = keybase1.FSStatusCode_ERROR
    46  
    47  // ReporterKBPKI implements the Notify function of the Reporter
    48  // interface in addition to embedding ReporterSimple for error
    49  // tracking.  Notify will make RPCs to the keybase daemon.
    50  type ReporterKBPKI struct {
    51  	*ReporterSimple
    52  	config                  Config
    53  	log                     logger.Logger
    54  	vlog                    *libkb.VDebugLog
    55  	notifyBuffer            chan *keybase1.FSNotification
    56  	onlineStatusBuffer      chan bool
    57  	notifyPathBuffer        chan string
    58  	notifySyncBuffer        chan *keybase1.FSPathSyncStatus
    59  	notifyOverallSyncBuffer chan keybase1.FolderSyncStatus
    60  	notifyFavsBuffer        chan struct{}
    61  	shutdownCh              chan struct{}
    62  	canceler                func()
    63  
    64  	lastNotifyPathLock sync.Mutex
    65  	lastNotifyPath     string
    66  
    67  	shutdownLock sync.RWMutex
    68  	isShutdown   bool
    69  }
    70  
    71  // NewReporterKBPKI creates a new ReporterKBPKI.
    72  func NewReporterKBPKI(config Config, maxErrors, bufSize int) *ReporterKBPKI {
    73  	log := config.MakeLogger("")
    74  	r := &ReporterKBPKI{
    75  		ReporterSimple:          NewReporterSimple(config.Clock(), maxErrors),
    76  		config:                  config,
    77  		log:                     log,
    78  		vlog:                    config.MakeVLogger(log),
    79  		notifyBuffer:            make(chan *keybase1.FSNotification, bufSize),
    80  		onlineStatusBuffer:      make(chan bool, bufSize),
    81  		notifyPathBuffer:        make(chan string, 1),
    82  		notifySyncBuffer:        make(chan *keybase1.FSPathSyncStatus, 1),
    83  		notifyOverallSyncBuffer: make(chan keybase1.FolderSyncStatus, 1),
    84  		notifyFavsBuffer:        make(chan struct{}, 1),
    85  		shutdownCh:              make(chan struct{}),
    86  	}
    87  	var ctx context.Context
    88  	ctx, r.canceler = context.WithCancel(context.Background())
    89  	go r.send(ctx)
    90  	return r
    91  }
    92  
    93  // ReportErr implements the Reporter interface for ReporterKBPKI.
    94  func (r *ReporterKBPKI) ReportErr(ctx context.Context,
    95  	tlfName tlf.CanonicalName, t tlf.Type, mode ErrorModeType, err error) {
    96  	r.ReporterSimple.ReportErr(ctx, tlfName, t, mode, err)
    97  
    98  	// Fire off error popups
    99  	params := make(map[string]string)
   100  	filename := ""
   101  	var code keybase1.FSErrorType = -1
   102  	switch e := errors.Cause(err).(type) {
   103  	case tlfhandle.ReadAccessError:
   104  		code = keybase1.FSErrorType_ACCESS_DENIED
   105  		params[errorParamMode] = errorModeRead
   106  		filename = e.Filename
   107  	case tlfhandle.WriteAccessError:
   108  		code = keybase1.FSErrorType_ACCESS_DENIED
   109  		params[errorParamUsername] = e.User.String()
   110  		params[errorParamMode] = errorModeWrite
   111  		filename = e.Filename
   112  	case WriteUnsupportedError:
   113  		code = keybase1.FSErrorType_ACCESS_DENIED
   114  		params[errorParamMode] = errorModeWrite
   115  		filename = e.Filename
   116  	case UnverifiableTlfUpdateError:
   117  		code = keybase1.FSErrorType_REVOKED_DATA_DETECTED
   118  	case idutil.NoCurrentSessionError:
   119  		code = keybase1.FSErrorType_NOT_LOGGED_IN
   120  	case NeedSelfRekeyError:
   121  		code = keybase1.FSErrorType_REKEY_NEEDED
   122  		params[errorParamRekeySelf] = "true"
   123  	case NeedOtherRekeyError:
   124  		code = keybase1.FSErrorType_REKEY_NEEDED
   125  		params[errorParamRekeySelf] = "false"
   126  	case kbfsmd.NewMetadataVersionError:
   127  		code = keybase1.FSErrorType_OLD_VERSION
   128  		err = OutdatedVersionError{}
   129  	case kbfsmd.NewMerkleVersionError:
   130  		code = keybase1.FSErrorType_OLD_VERSION
   131  		err = OutdatedVersionError{}
   132  	case NewDataVersionError:
   133  		code = keybase1.FSErrorType_OLD_VERSION
   134  		err = OutdatedVersionError{}
   135  	case OverQuotaWarning:
   136  		code = keybase1.FSErrorType_OVER_QUOTA
   137  		params[errorParamUsageBytes] = strconv.FormatInt(e.UsageBytes, 10)
   138  		params[errorParamLimitBytes] = strconv.FormatInt(e.LimitBytes, 10)
   139  	case *ErrDiskLimitTimeout:
   140  		if !e.reportable {
   141  			return
   142  		}
   143  		code = keybase1.FSErrorType_DISK_LIMIT_REACHED
   144  		params[errorParamUsageBytes] = strconv.FormatInt(e.usageBytes, 10)
   145  		params[errorParamLimitBytes] =
   146  			strconv.FormatFloat(e.limitBytes, 'f', 0, 64)
   147  		params[errorParamUsageFiles] = strconv.FormatInt(e.usageFiles, 10)
   148  		params[errorParamLimitFiles] =
   149  			strconv.FormatFloat(e.limitFiles, 'f', 0, 64)
   150  	case idutil.NoSigChainError:
   151  		code = keybase1.FSErrorType_NO_SIG_CHAIN
   152  		params[errorParamUsername] = e.User.String()
   153  	case kbfsmd.ServerErrorTooManyFoldersCreated:
   154  		code = keybase1.FSErrorType_TOO_MANY_FOLDERS
   155  		params[errorParamFolderLimit] = strconv.FormatUint(e.Limit, 10)
   156  		params[errorParamFoldersCreated] = strconv.FormatUint(e.Created, 10)
   157  	case RenameAcrossDirsError:
   158  		if len(e.ApplicationExecPath) > 0 {
   159  			code = keybase1.FSErrorType_EXDEV_NOT_SUPPORTED
   160  			params[errorParamApplicationExecPath] = e.ApplicationExecPath
   161  		}
   162  	case OfflineArchivedError:
   163  		code = keybase1.FSErrorType_OFFLINE_ARCHIVED
   164  	case OfflineUnsyncedError:
   165  		code = keybase1.FSErrorType_OFFLINE_UNSYNCED
   166  	}
   167  
   168  	if code < 0 && err == context.DeadlineExceeded {
   169  		code = keybase1.FSErrorType_TIMEOUT
   170  		// Workaround for DESKTOP-2442
   171  		filename = string(tlfName)
   172  	}
   173  
   174  	if code >= 0 {
   175  		n := errorNotification(err, code, tlfName, t, mode, filename, params)
   176  		r.Notify(ctx, n)
   177  		r.config.GetPerfLog().CDebugf(ctx, "KBFS error: %v", err)
   178  	}
   179  }
   180  
   181  // Notify implements the Reporter interface for ReporterKBPKI.
   182  //
   183  // TODO: might be useful to get the debug tags out of ctx and store
   184  //
   185  //	them in the notifyBuffer as well so that send() can put
   186  //	them back in its context.
   187  func (r *ReporterKBPKI) Notify(ctx context.Context, notification *keybase1.FSNotification) {
   188  	r.shutdownLock.RLock()
   189  	defer r.shutdownLock.RUnlock()
   190  	if r.isShutdown {
   191  		return
   192  	}
   193  
   194  	select {
   195  	case r.notifyBuffer <- notification:
   196  	default:
   197  		r.vlog.CLogf(
   198  			ctx, libkb.VLog1, "ReporterKBPKI: notify buffer full, dropping %+v",
   199  			notification)
   200  	}
   201  }
   202  
   203  // OnlineStatusChanged notifies the service (and eventually GUI) when we
   204  // detected we are connected to or disconnected from mdserver.
   205  func (r *ReporterKBPKI) OnlineStatusChanged(ctx context.Context, online bool) {
   206  	r.shutdownLock.RLock()
   207  	defer r.shutdownLock.RUnlock()
   208  	if r.isShutdown {
   209  		return
   210  	}
   211  
   212  	r.onlineStatusBuffer <- online
   213  }
   214  
   215  func (r *ReporterKBPKI) setLastNotifyPath(p string) (same bool) {
   216  	r.lastNotifyPathLock.Lock()
   217  	defer r.lastNotifyPathLock.Unlock()
   218  	same = p == r.lastNotifyPath
   219  	r.lastNotifyPath = p
   220  	return same
   221  }
   222  
   223  // NotifyPathUpdated implements the Reporter interface for ReporterKBPKI.
   224  //
   225  // TODO: might be useful to get the debug tags out of ctx and store
   226  //
   227  //	them in the notifyPathBuffer as well so that send() can put
   228  //	them back in its context.
   229  func (r *ReporterKBPKI) NotifyPathUpdated(ctx context.Context, path string) {
   230  	r.shutdownLock.RLock()
   231  	defer r.shutdownLock.RUnlock()
   232  	if r.isShutdown {
   233  		return
   234  	}
   235  
   236  	sameAsLast := r.setLastNotifyPath(path)
   237  	select {
   238  	case r.notifyPathBuffer <- path:
   239  	default:
   240  		if sameAsLast {
   241  			r.vlog.CLogf(
   242  				ctx, libkb.VLog1,
   243  				"ReporterKBPKI: notify path buffer full, dropping %s", path)
   244  		} else {
   245  			// This should be rare; it only happens when user switches from one
   246  			// TLF to another, and we happen to have an update from the old TLF
   247  			// in the buffer before switching subscribed TLF.
   248  			r.vlog.CLogf(
   249  				ctx, libkb.VLog1,
   250  				"ReporterKBPKI: notify path buffer full, but path is "+
   251  					"different from last one, so send in a goroutine %s", path)
   252  			go func() {
   253  				r.shutdownLock.RLock()
   254  				defer r.shutdownLock.RUnlock()
   255  				if r.isShutdown {
   256  					return
   257  				}
   258  
   259  				select {
   260  				case r.notifyPathBuffer <- path:
   261  				case <-r.shutdownCh:
   262  				}
   263  			}()
   264  		}
   265  	}
   266  }
   267  
   268  // NotifySyncStatus implements the Reporter interface for ReporterKBPKI.
   269  //
   270  // TODO: might be useful to get the debug tags out of ctx and store
   271  //
   272  //	them in the notifyBuffer as well so that send() can put
   273  //	them back in its context.
   274  func (r *ReporterKBPKI) NotifySyncStatus(ctx context.Context,
   275  	status *keybase1.FSPathSyncStatus) {
   276  	r.shutdownLock.RLock()
   277  	defer r.shutdownLock.RUnlock()
   278  	if r.isShutdown {
   279  		return
   280  	}
   281  
   282  	select {
   283  	case r.notifySyncBuffer <- status:
   284  	default:
   285  		r.vlog.CLogf(
   286  			ctx, libkb.VLog1, "ReporterKBPKI: notify sync buffer full, "+
   287  				"dropping %+v", status)
   288  	}
   289  }
   290  
   291  // NotifyFavoritesChanged implements the Reporter interface for
   292  // ReporterSimple.
   293  func (r *ReporterKBPKI) NotifyFavoritesChanged(ctx context.Context) {
   294  	r.shutdownLock.RLock()
   295  	defer r.shutdownLock.RUnlock()
   296  	if r.isShutdown {
   297  		return
   298  	}
   299  
   300  	select {
   301  	case r.notifyFavsBuffer <- struct{}{}:
   302  	default:
   303  		r.vlog.CLogf(
   304  			ctx, libkb.VLog1, "ReporterKBPKI: notify favs buffer full, "+
   305  				"dropping")
   306  	}
   307  }
   308  
   309  // NotifyOverallSyncStatus implements the Reporter interface for ReporterKBPKI.
   310  func (r *ReporterKBPKI) NotifyOverallSyncStatus(
   311  	ctx context.Context, status keybase1.FolderSyncStatus) {
   312  	r.shutdownLock.RLock()
   313  	defer r.shutdownLock.RUnlock()
   314  	if r.isShutdown {
   315  		return
   316  	}
   317  
   318  	select {
   319  	case r.notifyOverallSyncBuffer <- status:
   320  	default:
   321  		// If this represents a "complete" status, we can't drop it.
   322  		// Instead launch a goroutine to make sure it gets sent
   323  		// eventually.
   324  		if status.PrefetchStatus == keybase1.PrefetchStatus_COMPLETE {
   325  			go func() {
   326  				r.shutdownLock.RLock()
   327  				defer r.shutdownLock.RUnlock()
   328  				if r.isShutdown {
   329  					return
   330  				}
   331  
   332  				select {
   333  				case r.notifyOverallSyncBuffer <- status:
   334  				case <-r.shutdownCh:
   335  				}
   336  			}()
   337  		} else {
   338  			r.vlog.CLogf(
   339  				ctx, libkb.VLog1,
   340  				"ReporterKBPKI: notify overall sync buffer dropping %+v",
   341  				status)
   342  		}
   343  	}
   344  }
   345  
   346  // Shutdown implements the Reporter interface for ReporterKBPKI.
   347  func (r *ReporterKBPKI) Shutdown() {
   348  	r.shutdownLock.Lock()
   349  	defer r.shutdownLock.Unlock()
   350  	if r.isShutdown {
   351  		return
   352  	}
   353  	r.isShutdown = true
   354  
   355  	r.canceler()
   356  	close(r.shutdownCh)
   357  	close(r.notifyBuffer)
   358  	close(r.onlineStatusBuffer)
   359  	close(r.notifySyncBuffer)
   360  	close(r.notifyOverallSyncBuffer)
   361  	close(r.notifyFavsBuffer)
   362  }
   363  
   364  const (
   365  	reporterSendInterval    = time.Second
   366  	reporterFavSendInterval = 5 * time.Second
   367  )
   368  
   369  // send takes notifications out of notifyBuffer, notifyPathBuffer, and
   370  // notifySyncBuffer and sends them to the keybase daemon.
   371  func (r *ReporterKBPKI) send(ctx context.Context) {
   372  	sendTicker := time.NewTicker(reporterSendInterval)
   373  	defer sendTicker.Stop()
   374  	favSendTicker := time.NewTicker(reporterFavSendInterval)
   375  	defer favSendTicker.Stop()
   376  
   377  	for {
   378  		select {
   379  		case notification, ok := <-r.notifyBuffer:
   380  			if !ok {
   381  				return
   382  			}
   383  			nt := notification.NotificationType
   384  			st := notification.StatusCode
   385  			// Only these notifications are used in frontend:
   386  			// https://github.com/keybase/client/blob/0d63795105f64289ba4ef20fbefe56aad91bc7e9/shared/util/kbfs-notifications.js#L142-L154
   387  			if nt != keybase1.FSNotificationType_REKEYING &&
   388  				nt != keybase1.FSNotificationType_INITIALIZED &&
   389  				nt != keybase1.FSNotificationType_CONNECTION &&
   390  				nt != keybase1.FSNotificationType_SYNC_CONFIG_CHANGED &&
   391  				st != keybase1.FSStatusCode_ERROR {
   392  				continue
   393  			}
   394  			// Send them right away rather than staging it and waiting for the
   395  			// ticker, since each of them can be distinct from each other.
   396  			if err := r.config.KeybaseService().Notify(ctx,
   397  				notification); err != nil {
   398  				r.log.CDebugf(ctx, "ReporterDaemon: error sending "+
   399  					"notification: %s", err)
   400  			}
   401  		case online, ok := <-r.onlineStatusBuffer:
   402  			if !ok {
   403  				return
   404  			}
   405  			if err := r.config.KeybaseService().NotifyOnlineStatusChanged(ctx, online); err != nil {
   406  				r.log.CDebugf(ctx, "ReporterDaemon: error sending "+
   407  					"NotifyOnlineStatusChanged: %s", err)
   408  			}
   409  		case <-sendTicker.C:
   410  			select {
   411  			case path, ok := <-r.notifyPathBuffer:
   412  				if !ok {
   413  					return
   414  				}
   415  				if err := r.config.KeybaseService().NotifyPathUpdated(
   416  					ctx, path); err != nil {
   417  					r.log.CDebugf(ctx, "ReporterDaemon: error sending "+
   418  						"notification for path: %s", err)
   419  				}
   420  			default:
   421  			}
   422  
   423  			select {
   424  			case status, ok := <-r.notifySyncBuffer:
   425  				if !ok {
   426  					return
   427  				}
   428  				if err := r.config.KeybaseService().NotifySyncStatus(ctx,
   429  					status); err != nil {
   430  					r.log.CDebugf(ctx, "ReporterDaemon: error sending "+
   431  						"sync status: %s", err)
   432  				}
   433  			default:
   434  			}
   435  
   436  			select {
   437  			case status, ok := <-r.notifyOverallSyncBuffer:
   438  				if !ok {
   439  					return
   440  				}
   441  				if err := r.config.KeybaseService().NotifyOverallSyncStatus(
   442  					ctx, status); err != nil {
   443  					r.log.CDebugf(ctx, "ReporterDaemon: error sending "+
   444  						"overall sync status: %s", err)
   445  				}
   446  			default:
   447  			}
   448  		case <-favSendTicker.C:
   449  			select {
   450  			case _, ok := <-r.notifyFavsBuffer:
   451  				if !ok {
   452  					return
   453  				}
   454  				if err := r.config.KeybaseService().NotifyFavoritesChanged(
   455  					ctx); err != nil {
   456  					r.log.CDebugf(ctx, "ReporterDaemon: error sending "+
   457  						"favorites changed notification: %s", err)
   458  				}
   459  			default:
   460  			}
   461  		case <-ctx.Done():
   462  			return
   463  		}
   464  	}
   465  }
   466  
   467  // writeNotification creates FSNotifications from paths for file
   468  // write events.
   469  func writeNotification(file data.Path, finish bool) *keybase1.FSNotification {
   470  	n := baseNotification(file, finish)
   471  	if file.Tlf.Type() == tlf.Public {
   472  		n.NotificationType = keybase1.FSNotificationType_SIGNING
   473  	} else {
   474  		n.NotificationType = keybase1.FSNotificationType_ENCRYPTING
   475  	}
   476  	return n
   477  }
   478  
   479  // readNotification creates FSNotifications from paths for file
   480  // read events.
   481  func readNotification(file data.Path, finish bool) *keybase1.FSNotification {
   482  	n := baseNotification(file, finish)
   483  	if file.Tlf.Type() == tlf.Public {
   484  		n.NotificationType = keybase1.FSNotificationType_VERIFYING
   485  	} else {
   486  		n.NotificationType = keybase1.FSNotificationType_DECRYPTING
   487  	}
   488  	return n
   489  }
   490  
   491  // rekeyNotification creates FSNotifications from TlfHandles for rekey
   492  // events.
   493  func rekeyNotification(ctx context.Context, config Config, handle *tlfhandle.Handle, finish bool) *keybase1.FSNotification {
   494  	code := keybase1.FSStatusCode_START
   495  	if finish {
   496  		code = keybase1.FSStatusCode_FINISH
   497  	}
   498  
   499  	return &keybase1.FSNotification{
   500  		FolderType:       handle.Type().FolderType(),
   501  		Filename:         handle.GetCanonicalPath(),
   502  		StatusCode:       code,
   503  		NotificationType: keybase1.FSNotificationType_REKEYING,
   504  	}
   505  }
   506  
   507  // connectionNotification creates FSNotifications based on whether
   508  // or not KBFS is online.
   509  func connectionNotification(status keybase1.FSStatusCode) *keybase1.FSNotification {
   510  	// TODO finish placeholder
   511  	return &keybase1.FSNotification{
   512  		NotificationType: keybase1.FSNotificationType_CONNECTION,
   513  		StatusCode:       status,
   514  	}
   515  }
   516  
   517  // baseNotification creates a basic FSNotification without a
   518  // NotificationType from a path.
   519  func baseNotification(file data.Path, finish bool) *keybase1.FSNotification {
   520  	code := keybase1.FSStatusCode_START
   521  	if finish {
   522  		code = keybase1.FSStatusCode_FINISH
   523  	}
   524  
   525  	return &keybase1.FSNotification{
   526  		Filename:   file.CanonicalPathPlaintext(),
   527  		StatusCode: code,
   528  	}
   529  }
   530  
   531  // errorNotification creates FSNotifications for errors.
   532  func errorNotification(err error, errType keybase1.FSErrorType,
   533  	tlfName tlf.CanonicalName, t tlf.Type, mode ErrorModeType,
   534  	filename string, params map[string]string) *keybase1.FSNotification {
   535  	if tlfName != "" {
   536  		params[errorParamTlf] = string(tlfName)
   537  	}
   538  	var nType keybase1.FSNotificationType
   539  	switch mode {
   540  	case ReadMode:
   541  		params[errorParamMode] = errorModeRead
   542  		if t == tlf.Public {
   543  			nType = keybase1.FSNotificationType_VERIFYING
   544  		} else {
   545  			nType = keybase1.FSNotificationType_DECRYPTING
   546  		}
   547  	case WriteMode:
   548  		params[errorParamMode] = errorModeWrite
   549  		if t == tlf.Public {
   550  			nType = keybase1.FSNotificationType_SIGNING
   551  		} else {
   552  			nType = keybase1.FSNotificationType_ENCRYPTING
   553  		}
   554  	default:
   555  		panic(fmt.Sprintf("Unknown mode: %v", mode))
   556  	}
   557  	return &keybase1.FSNotification{
   558  		FolderType:       t.FolderType(),
   559  		Filename:         filename,
   560  		StatusCode:       keybase1.FSStatusCode_ERROR,
   561  		Status:           err.Error(),
   562  		ErrorType:        errType,
   563  		Params:           params,
   564  		NotificationType: nType,
   565  	}
   566  }
   567  
   568  func mdReadSuccessNotification(handle *tlfhandle.Handle,
   569  	public bool) *keybase1.FSNotification {
   570  	params := make(map[string]string)
   571  	if handle != nil {
   572  		params[errorParamTlf] = string(handle.GetCanonicalName())
   573  	}
   574  	return &keybase1.FSNotification{
   575  		FolderType:       handle.Type().FolderType(),
   576  		Filename:         handle.GetCanonicalPath(),
   577  		StatusCode:       keybase1.FSStatusCode_START,
   578  		NotificationType: keybase1.FSNotificationType_MD_READ_SUCCESS,
   579  		Params:           params,
   580  	}
   581  }
   582  
   583  func syncConfigChangeNotification(handle *tlfhandle.Handle,
   584  	fsc keybase1.FolderSyncConfig) *keybase1.FSNotification {
   585  	params := map[string]string{
   586  		"syncMode": fsc.Mode.String(),
   587  	}
   588  	return &keybase1.FSNotification{
   589  		FolderType:       handle.Type().FolderType(),
   590  		Filename:         handle.GetCanonicalPath(),
   591  		StatusCode:       keybase1.FSStatusCode_START,
   592  		NotificationType: keybase1.FSNotificationType_SYNC_CONFIG_CHANGED,
   593  		Params:           params,
   594  	}
   595  }