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