github.com/telepresenceio/telepresence/v2@v2.20.0-pro.6.0.20240517030216-236ea954e789/pkg/client/cli/daemon/info.go (about) 1 package daemon 2 3 import ( 4 "context" 5 "errors" 6 "fmt" 7 "io/fs" 8 "os" 9 "path/filepath" 10 "regexp" 11 "strings" 12 "time" 13 14 "github.com/datawire/dlib/dlog" 15 "github.com/telepresenceio/telepresence/v2/pkg/client/cache" 16 "github.com/telepresenceio/telepresence/v2/pkg/errcat" 17 "github.com/telepresenceio/telepresence/v2/pkg/filelocation" 18 ) 19 20 type Info struct { 21 Options map[string]string `json:"options,omitempty"` 22 InDocker bool `json:"in_docker,omitempty"` 23 Name string `json:"name,omitempty"` 24 KubeContext string `json:"kube_context,omitempty"` 25 Namespace string `json:"namespace,omitempty"` 26 DaemonPort int `json:"daemon_port,omitempty"` 27 ExposedPorts []string `json:"exposed_ports,omitempty"` 28 Hostname string `json:"hostname,omitempty"` 29 } 30 31 func (info *Info) DaemonID() *Identifier { 32 id, _ := NewIdentifier(info.Name, info.KubeContext, info.Namespace, info.InDocker) 33 return id 34 } 35 36 const ( 37 daemonsDirName = "daemons" 38 keepAliveInterval = 5 * time.Second 39 ) 40 41 func LoadInfo(ctx context.Context, file string) (*Info, error) { 42 var di Info 43 if err := cache.LoadFromUserCache(ctx, &di, filepath.Join(daemonsDirName, file)); err != nil { 44 return nil, err 45 } 46 return &di, nil 47 } 48 49 func SaveInfo(ctx context.Context, object *Info, file string) error { 50 return cache.SaveToUserCache(ctx, object, filepath.Join(daemonsDirName, file), cache.Public) 51 } 52 53 func DeleteInfo(ctx context.Context, file string) error { 54 return cache.DeleteFromUserCache(ctx, filepath.Join(daemonsDirName, file)) 55 } 56 57 func InfoExists(ctx context.Context, file string) (bool, error) { 58 return cache.ExistsInCache(ctx, filepath.Join(daemonsDirName, file)) 59 } 60 61 func WatchInfos(ctx context.Context, onChange func(context.Context) error, files ...string) error { 62 return cache.WatchUserCache(ctx, daemonsDirName, onChange, files...) 63 } 64 65 func WaitUntilAllVanishes(ctx context.Context, ttw time.Duration) error { 66 giveUp := time.Now().Add(ttw) 67 for giveUp.After(time.Now()) { 68 files, err := infoFiles(ctx) 69 if err != nil || len(files) == 0 { 70 return err 71 } 72 time.Sleep(250 * time.Millisecond) 73 } 74 return errors.New("timeout while waiting for daemon files to vanish") 75 } 76 77 func DeleteAllInfos(ctx context.Context) error { 78 files, err := infoFiles(ctx) 79 if err != nil { 80 return err 81 } 82 for _, file := range files { 83 _ = cache.DeleteFromUserCache(ctx, filepath.Join(daemonsDirName, file.Name())) 84 } 85 return nil 86 } 87 88 func LoadInfos(ctx context.Context) ([]*Info, error) { 89 files, err := infoFiles(ctx) 90 if err != nil { 91 return nil, err 92 } 93 94 DaemonInfos := make([]*Info, len(files)) 95 for i, file := range files { 96 if err = cache.LoadFromUserCache(ctx, &DaemonInfos[i], filepath.Join(daemonsDirName, file.Name())); err != nil { 97 return nil, err 98 } 99 } 100 return DaemonInfos, nil 101 } 102 103 func infoFiles(ctx context.Context) ([]fs.DirEntry, error) { 104 files, err := os.ReadDir(filepath.Join(filelocation.AppUserCacheDir(ctx), daemonsDirName)) 105 if err != nil { 106 if os.IsNotExist(err) { 107 err = nil 108 } 109 return nil, err 110 } 111 active := make([]fs.DirEntry, 0, len(files)) 112 for _, file := range files { 113 fi, err := file.Info() 114 if err != nil { 115 return nil, err 116 } 117 age := time.Since(fi.ModTime()) 118 if age > keepAliveInterval+600*time.Millisecond { 119 // File has gone stale 120 dlog.Debugf(ctx, "Deleting stale info %s with age = %s", file.Name(), age) 121 if err = cache.DeleteFromUserCache(ctx, filepath.Join(daemonsDirName, file.Name())); err != nil { 122 return nil, err 123 } 124 } else { 125 active = append(active, file) 126 } 127 } 128 return active, err 129 } 130 131 type InfoMatchError string 132 133 func (i InfoMatchError) Error() string { 134 return string(i) 135 } 136 137 type MultipleDaemonsError []*Info //nolint:errname // Don't want a plural name just because the type is a slice 138 139 func (m MultipleDaemonsError) Error() string { 140 sb := strings.Builder{} 141 sb.WriteString("multiple daemons are running, please select ") 142 l := len(m) 143 i := 0 144 if l > 2 { 145 sb.WriteString("one of ") 146 for ; i+2 < l; i++ { 147 sb.WriteString(m[i].DaemonID().Name) 148 sb.WriteString(", ") 149 } 150 } else { 151 sb.WriteString(m[i].DaemonID().Name) 152 i++ 153 } 154 sb.WriteString(" or ") 155 sb.WriteString(m[i].DaemonID().Name) 156 sb.WriteString(" using the --use <match> flag") 157 return sb.String() 158 } 159 160 func LoadMatchingInfo(ctx context.Context, match *regexp.Regexp) (*Info, error) { 161 if match == nil { 162 infos, err := LoadInfos(ctx) 163 if err != nil { 164 return nil, err 165 } 166 switch len(infos) { 167 case 0: 168 return nil, os.ErrNotExist 169 case 1: 170 return infos[0], err 171 default: 172 return nil, MultipleDaemonsError(infos) 173 } 174 } 175 files, err := infoFiles(ctx) 176 if err != nil { 177 return nil, err 178 } 179 var found string 180 for _, file := range files { 181 name := file.Name() 182 if !strings.HasSuffix(name, ".json") { 183 continue 184 } 185 // If a match is given, then strip ".json" and apply it. 186 if match.MatchString(name[:len(name)-5]) { 187 if found != "" { 188 return nil, errcat.User.New( 189 InfoMatchError(fmt.Sprintf("the expression %q does not uniquely identify a running daemon", match.String()))) 190 } 191 found = name 192 } 193 } 194 if found == "" { 195 return nil, os.ErrNotExist 196 } 197 return LoadInfo(ctx, found) 198 } 199 200 // CancelWhenRmFromCache watches for the file to be removed from the cache, then calls cancel. 201 func CancelWhenRmFromCache(ctx context.Context, cancel context.CancelFunc, filename string) error { 202 return WatchInfos(ctx, func(ctx context.Context) error { 203 exists, err := InfoExists(ctx, filename) 204 if err != nil { 205 return err 206 } 207 if !exists { 208 // spec removed from cache, shut down gracefully 209 dlog.Infof(ctx, "daemon file %s removed from cache, shutting down gracefully", filename) 210 cancel() 211 } 212 return nil 213 }, filename) 214 } 215 216 // KeepInfoAlive updates the access and modification times of the given Info 217 // periodically so that it never gets older than keepAliveInterval. This means that 218 // any file with a modification time older than the current time minus two keepAliveIntervals 219 // can be considered stale and should be removed. 220 // 221 // The alive poll ends and the Info is deleted when the context is cancelled. 222 func KeepInfoAlive(ctx context.Context, file string) error { 223 daemonFile := filepath.Join(filelocation.AppUserCacheDir(ctx), daemonsDirName, file) 224 ticker := time.NewTicker(keepAliveInterval) 225 defer ticker.Stop() 226 now := time.Now() 227 for { 228 if err := os.Chtimes(daemonFile, now, now); err != nil { 229 if os.IsNotExist(err) { 230 // File is removed, so stop trying to update its timestamps 231 dlog.Debugf(ctx, "Daemon info %s does not exist", file) 232 return nil 233 } 234 return fmt.Errorf("failed to update timestamp on %s: %w", daemonFile, err) 235 } 236 select { 237 case <-ctx.Done(): 238 dlog.Debugf(ctx, "Deleting daemon info %s because context was cancelled", file) 239 _ = DeleteInfo(ctx, file) 240 return nil 241 case now = <-ticker.C: 242 } 243 } 244 }