github.com/keybase/client/go@v0.0.0-20240309051027-028f7c731f8b/libkb/service_info.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  package libkb
     5  
     6  import (
     7  	"encoding/json"
     8  	"fmt"
     9  	"os"
    10  	"time"
    11  
    12  	"github.com/keybase/client/go/logger"
    13  )
    14  
    15  // ServiceInfo describes runtime info for a service.
    16  // This is primarily used to detect service updates.
    17  type ServiceInfo struct {
    18  	Version string `json:"version,omitempty"`
    19  	Label   string `json:"label,omitempty"`
    20  	Pid     int    `json:"pid,omitempty"`
    21  }
    22  
    23  // KeybaseServiceInfo is runtime info for the Keybase service.
    24  func KeybaseServiceInfo(g *GlobalContext) ServiceInfo {
    25  	return ServiceInfo{
    26  		Version: VersionString(),
    27  		Label:   g.Env.GetLabel(),
    28  		Pid:     os.Getpid(),
    29  	}
    30  }
    31  
    32  // NewServiceInfo generates service info for other services (like KBFS).
    33  func NewServiceInfo(version string, prerelease string, label string, pid int) ServiceInfo {
    34  	if prerelease != "" {
    35  		version = fmt.Sprintf("%s-%s", version, prerelease)
    36  	}
    37  	return ServiceInfo{
    38  		Version: version,
    39  		Label:   label,
    40  		Pid:     pid,
    41  	}
    42  }
    43  
    44  // WriteFile writes service info as JSON in runtimeDir.
    45  func (s ServiceInfo) WriteFile(path string, log logger.Logger) error {
    46  	out, err := json.MarshalIndent(s, "", "  ")
    47  	if err != nil {
    48  		return err
    49  	}
    50  
    51  	file := NewFile(path, out, 0644)
    52  	return file.Save(log)
    53  }
    54  
    55  // serviceLog is the log interface for ServiceInfo
    56  type serviceLog interface {
    57  	Debug(s string, args ...interface{})
    58  }
    59  
    60  func LoadServiceInfo(path string) (*ServiceInfo, error) {
    61  	if _, ferr := os.Stat(path); os.IsNotExist(ferr) {
    62  		return nil, nil
    63  	}
    64  	dat, err := os.ReadFile(path)
    65  	if err != nil {
    66  		return nil, err
    67  	}
    68  	var serviceInfo ServiceInfo
    69  	err = json.Unmarshal(dat, &serviceInfo)
    70  	if err != nil {
    71  		return nil, err
    72  	}
    73  
    74  	return &serviceInfo, nil
    75  }
    76  
    77  // WaitForServiceInfoFile tries to wait for a service info file, which should be
    78  // written on successful service startup.
    79  func WaitForServiceInfoFile(path string, label string, pid string, timeout time.Duration, log serviceLog) (*ServiceInfo, error) {
    80  	if pid == "" {
    81  		return nil, fmt.Errorf("No pid to wait for")
    82  	}
    83  
    84  	lookForServiceInfo := func() (*ServiceInfo, error) {
    85  		serviceInfo, err := LoadServiceInfo(path)
    86  		if err != nil {
    87  			return nil, err
    88  		}
    89  		if serviceInfo == nil {
    90  			return nil, nil
    91  		}
    92  
    93  		// Make sure the info file is the pid we are waiting for, otherwise it is
    94  		// still starting up.
    95  		if pid != fmt.Sprintf("%d", serviceInfo.Pid) {
    96  			return nil, nil
    97  		}
    98  
    99  		// PIDs match, the service has started up
   100  		return serviceInfo, nil
   101  	}
   102  
   103  	log.Debug("Looking for service info file (timeout=%s)", timeout)
   104  	serviceInfo, err := waitForServiceInfo(timeout, time.Millisecond*100, lookForServiceInfo)
   105  
   106  	// If no service info was found, let's return an error
   107  	if serviceInfo == nil {
   108  		if err == nil {
   109  			err = fmt.Errorf("%s isn't running (expecting pid=%s)", label, pid)
   110  		}
   111  		return nil, err
   112  	}
   113  
   114  	// We succeeded in finding service info
   115  	log.Debug("Found service info: %#v", *serviceInfo)
   116  	return serviceInfo, nil
   117  }
   118  
   119  type serviceInfoResult struct {
   120  	info *ServiceInfo
   121  	err  error
   122  }
   123  
   124  type loadServiceInfoFn func() (*ServiceInfo, error)
   125  
   126  func waitForServiceInfo(timeout time.Duration, delay time.Duration, fn loadServiceInfoFn) (*ServiceInfo, error) {
   127  	if timeout <= 0 {
   128  		return fn()
   129  	}
   130  
   131  	ticker := time.NewTicker(delay)
   132  	defer ticker.Stop()
   133  	resultChan := make(chan serviceInfoResult, 1)
   134  	go func() {
   135  		for range ticker.C {
   136  			info, err := fn()
   137  			if err != nil {
   138  				resultChan <- serviceInfoResult{info: nil, err: err}
   139  				return
   140  			}
   141  			if info != nil {
   142  				resultChan <- serviceInfoResult{info: info, err: nil}
   143  				return
   144  			}
   145  		}
   146  	}()
   147  
   148  	select {
   149  	case res := <-resultChan:
   150  		return res.info, res.err
   151  	case <-time.After(timeout):
   152  		return nil, nil
   153  	}
   154  }