gitlab.com/SkynetLabs/skyd@v1.6.9/cmd/skyc/rentercmd_helpers.go (about) 1 package main 2 3 import ( 4 "encoding/json" 5 "fmt" 6 "math/big" 7 "os" 8 "path/filepath" 9 "sort" 10 "strings" 11 "sync" 12 "sync/atomic" 13 "text/tabwriter" 14 "time" 15 16 "gitlab.com/NebulousLabs/errors" 17 "gitlab.com/SkynetLabs/skyd/build" 18 "gitlab.com/SkynetLabs/skyd/node/api" 19 "gitlab.com/SkynetLabs/skyd/skymodules" 20 "gitlab.com/SkynetLabs/skyd/skymodules/renter/filesystem" 21 "go.sia.tech/siad/modules" 22 "go.sia.tech/siad/types" 23 ) 24 25 var ( 26 // errIncorrectNumArgs is the error returned if there is an incorrect number 27 // of arguments 28 errIncorrectNumArgs = errors.New("incorrect number of arguments") 29 ) 30 31 // byDirectoryInfo implements sort.Interface for []directoryInfo based on the 32 // SiaPath field. 33 type byDirectoryInfo []directoryInfo 34 35 func (s byDirectoryInfo) Len() int { return len(s) } 36 func (s byDirectoryInfo) Swap(i, j int) { s[i], s[j] = s[j], s[i] } 37 func (s byDirectoryInfo) Less(i, j int) bool { 38 return s[i].dir.SiaPath.String() < s[j].dir.SiaPath.String() 39 } 40 41 // bySiaPathFile implements sort.Interface for [] skymodules.FileInfo based on the 42 // SiaPath field. 43 type bySiaPathFile []skymodules.FileInfo 44 45 func (s bySiaPathFile) Len() int { return len(s) } 46 func (s bySiaPathFile) Swap(i, j int) { s[i], s[j] = s[j], s[i] } 47 func (s bySiaPathFile) Less(i, j int) bool { return s[i].SiaPath.String() < s[j].SiaPath.String() } 48 49 // bySiaPathDir implements sort.Interface for [] skymodules.DirectoryInfo based on the 50 // SiaPath field. 51 type bySiaPathDir []skymodules.DirectoryInfo 52 53 func (s bySiaPathDir) Len() int { return len(s) } 54 func (s bySiaPathDir) Swap(i, j int) { s[i], s[j] = s[j], s[i] } 55 func (s bySiaPathDir) Less(i, j int) bool { return s[i].SiaPath.String() < s[j].SiaPath.String() } 56 57 // byValue sorts contracts by their value in siacoins, high to low. If two 58 // contracts have the same value, they are sorted by their host's address. 59 type byValue []api.RenterContract 60 61 func (s byValue) Len() int { return len(s) } 62 func (s byValue) Swap(i, j int) { s[i], s[j] = s[j], s[i] } 63 func (s byValue) Less(i, j int) bool { 64 cmp := s[i].RenterFunds.Cmp(s[j].RenterFunds) 65 if cmp == 0 { 66 return s[i].NetAddress < s[j].NetAddress 67 } 68 return cmp > 0 69 } 70 71 // directoryInfo is a helper struct that contains the skymodules.DirectoryInfo for 72 // a directory, the skymodules.FileInfo for all the directory's files, and the 73 // skymodules.DirectoryInfo for all the subdirs. 74 type directoryInfo struct { 75 dir skymodules.DirectoryInfo 76 files []skymodules.FileInfo 77 subDirs []skymodules.DirectoryInfo 78 } 79 80 // progressMeasurement is a helper type used for measuring the progress of 81 // a download. 82 type progressMeasurement struct { 83 progress uint64 84 time time.Time 85 } 86 87 // trackedFile is a helper struct for tracking files related to downloads 88 type trackedFile struct { 89 siaPath skymodules.SiaPath 90 dst string 91 } 92 93 // contractStats is a helper function to pull information out of the renter 94 // contracts to be displayed 95 func contractStats(contracts []api.RenterContract) (size uint64, spent, remaining, fees types.Currency) { 96 for _, c := range contracts { 97 size += c.Size 98 remaining = remaining.Add(c.RenterFunds) 99 fees = fees.Add(c.Fees) 100 // Negative Currency Check 101 var contractTotalSpent types.Currency 102 if c.TotalCost.Cmp(c.RenterFunds.Add(c.Fees)) < 0 { 103 contractTotalSpent = c.RenterFunds.Add(c.Fees) 104 } else { 105 contractTotalSpent = c.TotalCost.Sub(c.RenterFunds).Sub(c.Fees) 106 } 107 spent = spent.Add(contractTotalSpent) 108 } 109 return 110 } 111 112 // downloadDir downloads the dir at the specified siaPath to the specified 113 // location. It returns all the files for which a download was initialized as 114 // tracked files and the ones which were ignored as skipped. Errors are composed 115 // into a single error. 116 func downloadDir(siaPath skymodules.SiaPath, destination string) (tfs []trackedFile, skipped []string, totalSize uint64, err error) { 117 // Get dir info. 118 rd, err := httpClient.RenterDirRootGet(siaPath) 119 if err != nil { 120 err = errors.AddContext(err, "failed to get dir info") 121 return 122 } 123 // Create destination on disk. 124 if err = os.MkdirAll(destination, 0750); err != nil { 125 err = errors.AddContext(err, "failed to create destination dir") 126 return 127 } 128 // Download files. 129 for _, file := range rd.Files { 130 // Skip files that already exist. 131 dst := filepath.Join(destination, file.SiaPath.Name()) 132 if _, err = os.Stat(dst); err == nil { 133 skipped = append(skipped, dst) 134 continue 135 } else if !os.IsNotExist(err) { 136 err = errors.AddContext(err, "failed to get file stats") 137 return 138 } 139 // Download file. 140 totalSize += file.Filesize 141 _, err = httpClient.RenterDownloadFullGet(file.SiaPath, dst, true, true) 142 if err != nil { 143 err = errors.AddContext(err, "Failed to start download") 144 return 145 } 146 // Append file to tracked files. 147 tfs = append(tfs, trackedFile{ 148 siaPath: file.SiaPath, 149 dst: dst, 150 }) 151 } 152 // If the download isn't recursive we are done. 153 if !renterDownloadRecursive { 154 return 155 } 156 // Call downloadDir on all subdirs. 157 for i := 1; i < len(rd.Directories); i++ { 158 subDir := rd.Directories[i] 159 rtfs, rskipped, totalSubSize, rerr := downloadDir(subDir.SiaPath, filepath.Join(destination, subDir.SiaPath.Name())) 160 tfs = append(tfs, rtfs...) 161 skipped = append(skipped, rskipped...) 162 totalSize += totalSubSize 163 err = errors.Compose(err, rerr) 164 } 165 return 166 } 167 168 // downloadProgress will display the progress of the provided files and return a 169 // slice of DownloadInfos for failed downloads. 170 func downloadProgress(tfs []trackedFile) []api.DownloadInfo { 171 // Nothing to do if no files are tracked. 172 if len(tfs) == 0 { 173 return nil 174 } 175 start := time.Now() 176 177 // Create a map of all tracked files for faster lookups and also a measurement 178 // map which is initialized with 0 progress for all tracked files. 179 tfsMap := make(map[skymodules.SiaPath]trackedFile) 180 measurements := make(map[skymodules.SiaPath][]progressMeasurement) 181 for _, tf := range tfs { 182 tfsMap[tf.siaPath] = tf 183 measurements[tf.siaPath] = []progressMeasurement{{ 184 progress: 0, 185 time: time.Now(), 186 }} 187 } 188 // Periodically print measurements until download is done. 189 completed := make(map[string]struct{}) 190 errMap := make(map[string]api.DownloadInfo) 191 failedDownloads := func() (fd []api.DownloadInfo) { 192 for _, di := range errMap { 193 fd = append(fd, di) 194 } 195 return 196 } 197 for range time.Tick(OutputRefreshRate) { 198 // Get the list of downloads. 199 rdg, err := httpClient.RenterDownloadsRootGet() 200 if err != nil { 201 continue // benign 202 } 203 // Create a map of downloads for faster lookups. To get unique keys we use 204 // siaPath + destination as the key. 205 queue := make(map[string]api.DownloadInfo) 206 for _, d := range rdg.Downloads { 207 key := d.SiaPath.String() + d.Destination 208 if _, exists := queue[key]; !exists { 209 queue[key] = d 210 } 211 } 212 // Clear terminal. 213 clearStr := fmt.Sprint("\033[H\033[2J") 214 // Take new measurements for each tracked file. 215 progressStr := clearStr 216 for tfIdx, tf := range tfs { 217 // Search for the download in the list of downloads. 218 mapKey := tf.siaPath.String() + tf.dst 219 d, found := queue[mapKey] 220 m, exists := measurements[tf.siaPath] 221 if !exists { 222 die("Measurement missing for tracked file. This should never happen.") 223 } 224 // If the download has not appeared in the queue yet, either continue or 225 // give up. 226 if !found { 227 if time.Since(start) > RenterDownloadTimeout { 228 die("Unable to find download in queue. This should never happen.") 229 } 230 continue 231 } 232 // Check whether the file has completed or otherwise errored out. 233 if d.Error != "" { 234 errMap[mapKey] = d 235 } 236 if d.Completed { 237 completed[mapKey] = struct{}{} 238 // Check if all downloads are done. 239 if len(completed) == len(tfs) { 240 return failedDownloads() 241 } 242 continue 243 } 244 // Add the current progress to the measurements. 245 m = append(m, progressMeasurement{ 246 progress: d.Received, 247 time: time.Now(), 248 }) 249 // Shrink the measurements to only contain measurements from within the 250 // SpeedEstimationWindow. 251 for len(m) > 2 && m[len(m)-1].time.Sub(m[0].time) > SpeedEstimationWindow { 252 m = m[1:] 253 } 254 // Update measurements in the map. 255 measurements[tf.siaPath] = m 256 // Compute the progress and timespan between the first and last 257 // measurement to get the speed. 258 received := float64(m[len(m)-1].progress - m[0].progress) 259 timespan := m[len(m)-1].time.Sub(m[0].time) 260 speed := bandwidthUnit(uint64((received * 8) / timespan.Seconds())) 261 262 // Compuate the percentage of completion and time elapsed since the 263 // start of the download. 264 pct := 100 * float64(d.Received) / float64(d.Filesize) 265 elapsed := time.Since(d.StartTime) 266 elapsed -= elapsed % time.Second // round to nearest second 267 268 progressLine := fmt.Sprintf("Downloading %v... %5.1f%% of %v, %v elapsed, %s ", tf.siaPath.String(), pct, modules.FilesizeUnits(d.Filesize), elapsed, speed) 269 if tfIdx < len(tfs)-1 { 270 progressStr += fmt.Sprintln(progressLine) 271 } else { 272 progressStr += fmt.Sprint(progressLine) 273 } 274 } 275 fmt.Print(progressStr) 276 progressStr = clearStr 277 } 278 // This code is unreachable, but the compiler requires this to be here. 279 return nil 280 } 281 282 // fileHealthBreakdown returns a percentage breakdown of the renter's files' 283 // healths and the number of stuck files 284 func fileHealthBreakdown(dirs []directoryInfo, printLostFiles bool) ([]float64, int, error) { 285 // Check for nil input 286 if len(dirs) == 0 { 287 return nil, 0, errors.New("No Directories Found") 288 } 289 290 // Note: we are manually counting the number of files here since the 291 // aggregate fields in the directory could be incorrect due to delays in the 292 // health loop. This is OK since we have to iterate over all the files 293 // anyways. 294 var total, fullHealth, greater75, greater50, greater25, greater0, lost float64 295 var numStuck int 296 for _, dir := range dirs { 297 for _, file := range dir.files { 298 total++ 299 if file.Stuck { 300 numStuck++ 301 } 302 switch { 303 case file.MaxHealthPercent == 100: 304 fullHealth++ 305 case file.MaxHealthPercent > 75: 306 greater75++ 307 case file.MaxHealthPercent > 50: 308 greater50++ 309 case file.MaxHealthPercent > 25: 310 greater25++ 311 case file.MaxHealthPercent > 0 || file.OnDisk: 312 greater0++ 313 case file.Lost: 314 lost++ 315 if printLostFiles { 316 fmt.Println(file.SiaPath) 317 } 318 case !file.Finished: 319 // Nothing to report for unfinished files. 320 default: 321 return nil, 0, fmt.Errorf("unexpected file condition; Health %v, OnDisk %v, Lost %v, Finished %v", file.MaxHealthPercent, file.OnDisk, file.Lost, file.Finished) 322 } 323 } 324 } 325 326 // Print out total lost files 327 if printLostFiles { 328 fmt.Println() 329 fmt.Println(lost, "lost files found.") 330 } 331 332 // Check for no files uploaded 333 if total == 0 { 334 return nil, 0, errors.New("No Files Uploaded") 335 } 336 337 fullHealth = 100 * fullHealth / total 338 greater75 = 100 * greater75 / total 339 greater50 = 100 * greater50 / total 340 greater25 = 100 * greater25 / total 341 greater0 = 100 * greater0 / total 342 lost = 100 * lost / total 343 344 return []float64{fullHealth, greater75, greater50, greater25, greater0, lost}, numStuck, nil 345 } 346 347 // atomicTotalGetDirs is a helper for printing out the status of the getDir 348 // function call. 349 var atomicTotalGetDirs uint64 350 351 // getDir returns the directory info for the directory at siaPath and its 352 // subdirs, querying the root directory. 353 func getDir(siaPath skymodules.SiaPath, root, recursive, verbose bool) (dirs []directoryInfo) { 354 // Query the directory 355 var rd api.RenterDirectory 356 var err error 357 if root { 358 rd, err = httpClient.RenterDirRootGet(siaPath) 359 } else { 360 rd, err = httpClient.RenterDirGet(siaPath) 361 } 362 if err != nil { 363 die("failed to get dir info:", err) 364 } 365 366 // Defer print status update 367 if verbose { 368 defer func() { 369 fmt.Printf("\r%v directories queried", atomic.AddUint64(&atomicTotalGetDirs, 1)) 370 }() 371 } 372 373 // Split the directory and sub directory information 374 dir := rd.Directories[0] 375 subDirs := rd.Directories[1:] 376 377 // Append directory to dirs. 378 dirs = append(dirs, directoryInfo{ 379 dir: dir, 380 files: rd.Files, 381 subDirs: subDirs, 382 }) 383 384 // If -R isn't set, or there are no subDirs we are done. 385 if !recursive || len(subDirs) == 0 { 386 return 387 } 388 389 // Define number of workers for this call to use. 390 // 391 // NOTE: This is a recursive call so all subsequent calls will also have this 392 // many workers. While go routines themselves are cheap, we want to limit the 393 // chance of a panic due to too many open files. 394 // 395 // There will be numGetDirWorkers^maxDirectoryDepth go routines launched. 396 numGetDirWorkers := 5 397 var dirsMu sync.Mutex 398 399 // Create a siapath chan 400 siaPathChan := make(chan skymodules.SiaPath, numGetDirWorkers) 401 402 // Define getDirWorker function 403 getDirWorkerFunc := func(root, recursive bool) { 404 for siaPath := range siaPathChan { 405 subdirs := getDir(siaPath, root, recursive, verbose) 406 dirsMu.Lock() 407 dirs = append(dirs, subdirs...) 408 dirsMu.Unlock() 409 } 410 } 411 412 // Launch workers. 413 var wg sync.WaitGroup 414 for i := 0; i < numGetDirWorkers; i++ { 415 wg.Add(1) 416 go func(root, recursive bool) { 417 getDirWorkerFunc(root, recursive) 418 wg.Done() 419 }(root, recursive) 420 } 421 422 // Call getDir on subdirs. 423 for _, subDir := range subDirs { 424 siaPathChan <- subDir.SiaPath 425 } 426 427 close(siaPathChan) 428 wg.Wait() 429 return 430 } 431 432 // getDirSorted calls getDir and then sorts the response by siapath 433 func getDirSorted(siaPath skymodules.SiaPath, root, recursive, verbose bool) []directoryInfo { 434 // Get Dirs 435 dirs := getDir(siaPath, root, recursive, verbose) 436 437 // Sort the directories and the files. 438 sort.Sort(byDirectoryInfo(dirs)) 439 for i := 0; i < len(dirs); i++ { 440 sort.Sort(bySiaPathDir(dirs[i].subDirs)) 441 sort.Sort(bySiaPathFile(dirs[i].files)) 442 } 443 return dirs 444 } 445 446 // parseLSArgs is a helper that parses the arguments for renter ls and skynet ls 447 // and returns the siapath. 448 func parseLSArgs(args []string) (skymodules.SiaPath, error) { 449 var path string 450 switch len(args) { 451 case 0: 452 path = "." 453 case 1: 454 path = args[0] 455 default: 456 return skymodules.SiaPath{}, errIncorrectNumArgs 457 } 458 // Parse the input siapath. 459 var sp skymodules.SiaPath 460 var err error 461 if path == "." || path == "" || path == "/" { 462 sp = skymodules.RootSiaPath() 463 } else { 464 sp, err = skymodules.NewSiaPath(path) 465 if err != nil { 466 return skymodules.SiaPath{}, errors.AddContext(err, "could not parse siaPath") 467 } 468 } 469 return sp, nil 470 } 471 472 // printContractInfo is a helper function for printing the information about a 473 // specific contract 474 func printContractInfo(cid string, contracts []api.RenterContract) error { 475 for _, rc := range contracts { 476 if rc.ID.String() == cid { 477 var fundsAllocated types.Currency 478 if rc.TotalCost.Cmp(rc.Fees) > 0 { 479 fundsAllocated = rc.TotalCost.Sub(rc.Fees) 480 } 481 hostInfo, err := httpClient.HostDbHostsGet(rc.HostPublicKey) 482 if err != nil { 483 return fmt.Errorf("Could not fetch details of host: %v", err) 484 } 485 fmt.Printf(` 486 Contract %v 487 Host: %v (Public Key: %v) 488 Host Version: %v 489 490 Start Height: %v 491 End Height: %v 492 493 Total cost: %v (Fees: %v) 494 Funds Allocated: %v 495 Upload Spending: %v 496 Storage Spending: %v 497 Download Spending: %v 498 FundAccount Spending: %v 499 Maintenance Spending: %v 500 Remaining Funds: %v 501 502 File Size: %v 503 `, rc.ID, rc.NetAddress, rc.HostPublicKey.String(), rc.HostVersion, rc.StartHeight, rc.EndHeight, 504 currencyUnits(rc.TotalCost), currencyUnits(rc.Fees), 505 currencyUnits(fundsAllocated), 506 currencyUnits(rc.UploadSpending), 507 currencyUnits(rc.StorageSpending), 508 currencyUnits(rc.DownloadSpending), 509 currencyUnits(rc.FundAccountSpending), 510 currencyUnits(rc.MaintenanceSpending.Sum()), 511 currencyUnits(rc.RenterFunds), 512 modules.FilesizeUnits(rc.Size)) 513 514 printScoreBreakdown(&hostInfo) 515 return nil 516 } 517 } 518 519 fmt.Println("Contract not found") 520 return nil 521 } 522 523 // printDirs is a helper for printing directoryInfos 524 func printDirs(dirs []directoryInfo) error { 525 for _, dir := range dirs { 526 // Initialize a tab writer for the diretory 527 w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0) 528 529 // Print the Directory SiaPath 530 fmt.Fprintf(w, "%v/\n", dir.dir.SiaPath) 531 532 // Print SubDirs 533 for _, subDir := range dir.subDirs { 534 name := subDir.SiaPath.Name() + "/" 535 size := modules.FilesizeUnits(subDir.AggregateSize) 536 fmt.Fprintf(w, " %v\t%9v\n", name, size) 537 } 538 539 // Print files 540 for _, file := range dir.files { 541 name := file.SiaPath.Name() 542 size := modules.FilesizeUnits(file.Filesize) 543 fmt.Fprintf(w, " %v\t%9v\n", name, size) 544 } 545 fmt.Fprintln(w) 546 547 // Flush the writer 548 if err := w.Flush(); err != nil { 549 return errors.AddContext(err, "failed to flush writer") 550 } 551 } 552 return nil 553 } 554 555 // printDirsVerbose is a helper for verbose printing of directoryInfos 556 func printDirsVerbose(dirs []directoryInfo) error { 557 for _, dir := range dirs { 558 // Create a tab writer for the directory 559 w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0) 560 561 // Print the Directory SiaPath 562 fmt.Fprintf(w, "%v/\n", dir.dir.SiaPath) 563 564 // Print SubDirs 565 fmt.Fprintf(w, " Name\tFilesize\tAvailable\t Uploaded\tProgress\tRedundancy\tHealth\tStuck Health\tStuck\tRenewing\tOn Disk\tRecoverable\n") 566 for _, subDir := range dir.subDirs { 567 name := subDir.SiaPath.Name() + "/" 568 size := modules.FilesizeUnits(subDir.AggregateSize) 569 redundancyStr := fmt.Sprintf("%.2f", subDir.AggregateMinRedundancy) 570 if subDir.AggregateMinRedundancy == -1 { 571 redundancyStr = "-" 572 } 573 healthStr := fmt.Sprintf("%.2f%%", skymodules.HealthPercentage(subDir.AggregateHealth)) 574 stuckHealthStr := fmt.Sprintf("%.2f%%", skymodules.HealthPercentage(subDir.AggregateStuckHealth)) 575 stuckStr := yesNo(subDir.AggregateNumStuckChunks > 0) 576 fmt.Fprintf(w, " %v\t%9v\t%9s\t%9s\t%8s\t%10s\t%7s\t%7s\t%5s\t%8s\t%7s\t%11s\n", name, size, "-", "-", "-", redundancyStr, healthStr, stuckHealthStr, stuckStr, "-", "-", "-") 577 } 578 579 // Print files 580 for _, file := range dir.files { 581 name := file.SiaPath.Name() 582 size := modules.FilesizeUnits(file.Filesize) 583 availStr := yesNo(file.Available) 584 bytesUploaded := modules.FilesizeUnits(file.UploadedBytes) 585 uploadStr := fmt.Sprintf("%.2f%%", file.UploadProgress) 586 if file.UploadProgress == -1 { 587 uploadStr = "-" 588 } 589 redundancyStr := fmt.Sprintf("%.2f", file.Redundancy) 590 if file.Redundancy == -1 { 591 redundancyStr = "-" 592 } 593 594 healthStr := fmt.Sprintf("%.2f%%", skymodules.HealthPercentage(file.Health)) 595 stuckHealthStr := fmt.Sprintf("%.2f%%", skymodules.HealthPercentage(file.StuckHealth)) 596 stuckStr := yesNo(file.Stuck) 597 renewStr := yesNo(file.Renewing) 598 onDiskStr := yesNo(file.OnDisk) 599 recoverStr := yesNo(file.Recoverable) 600 fmt.Fprintf(w, " %v\t%9v\t%9s\t%9s\t%8s\t%10s\t%7s\t%7s\t%5s\t%8s\t%7s\t%11s\n", name, size, availStr, bytesUploaded, uploadStr, redundancyStr, healthStr, stuckHealthStr, stuckStr, renewStr, onDiskStr, recoverStr) 601 } 602 fmt.Fprintln(w) 603 604 // Flush the writer 605 if err := w.Flush(); err != nil { 606 return errors.AddContext(err, "failed to flush writer") 607 } 608 } 609 return nil 610 } 611 612 // printSingleFile is a helper for printing information about a single file 613 func printSingleFile(sp skymodules.SiaPath, root, skylinkCheck bool) (tryDir bool, err error) { 614 var rf api.RenterFile 615 if root { 616 rf, err = httpClient.RenterFileRootGet(sp) 617 } else { 618 rf, err = httpClient.RenterFileGet(sp) 619 } 620 if err == nil { 621 if skylinkCheck && len(rf.File.Skylinks) == 0 { 622 err = errors.New("File is not pinning any skylinks") 623 return 624 } 625 var data []byte 626 data, err = json.MarshalIndent(rf.File, "", " ") 627 if err != nil { 628 return 629 } 630 631 fmt.Println() 632 fmt.Println(string(data)) 633 fmt.Println() 634 return 635 } else if !strings.Contains(err.Error(), filesystem.ErrNotExist.Error()) { 636 err = fmt.Errorf("Error getting file %v: %v", sp.Name(), err) 637 return 638 } 639 tryDir = true 640 err = nil 641 return 642 } 643 644 // fundaccountdrift is a small helper that returns a big.Int representing the 645 // drift that might have occurred between the money spent on funding the account 646 // and the money that's actually accounted for 647 func fundaccountdrift(fm skymodules.FinancialMetrics, ea skymodules.AccountSpending) *big.Int { 648 var drift *big.Int 649 650 funded := fm.FundAccountSpending 651 accountedFor := ea.Sum().Add(ea.Balance).Add(ea.Residue) 652 if funded.Cmp(accountedFor) >= 0 { 653 drift = funded.Sub(accountedFor).Big() 654 } else { 655 drift = accountedFor.Sub(funded).Big() 656 drift = drift.Neg(drift) 657 } 658 659 return drift 660 } 661 662 // currentperiodspending returns the spending breakdown for the given financial 663 // metrics and exchange rate as a string 664 func currentperiodspending(fm skymodules.FinancialMetrics, rate *types.ExchangeRate) string { 665 // Calculate breakdown 666 totalSpent, unspentAllocated, unspentUnallocated := fm.SpendingBreakdown() 667 668 // Calculate the aggregated account spending 669 var ea skymodules.AccountSpending 670 for _, as := range fm.EphemeralAccountSpending { 671 ea = ea.Add(as.AccountSpending) 672 } 673 674 // Calculate drift 675 balanceDrift := &ea.BalanceDrift 676 spendingDrift := fundaccountdrift(fm, ea) 677 678 // Calculate misc & repair 679 misc := ea.SnapshotDownloadsCost.Add(ea.SnapshotUploadsCost) 680 repair := ea.RepairDownloadsCost.Add(ea.RepairUploadsCost) 681 682 return fmt.Sprintf(` 683 Spent Funds: %v 684 Storage: %v 685 Upload: %v 686 Download: %v 687 FundAccount: %v (+%v residue) 688 AccountBalanceCost: %v 689 Balance: %v (%v drift) 690 DownloadsCost: %v 691 MiscCost: %v 692 RegistryReadsCost: %v 693 RegistryWritesCost: %v 694 RepairsCost: %v 695 SubscriptionsCost: %v 696 UpdatePriceTableCost: %v 697 UploadsCost: %v 698 Drift: %v 699 Maintenance: %v 700 AccountBalanceCost: %v 701 FundAccountCost: %v 702 UpdatePriceTableCost: %v 703 Fees: %v 704 ContractFees: %v 705 SiafundFees: %v 706 TransactionFees: %v 707 Unspent Funds: %v 708 Allocated: %v 709 Unallocated: %v 710 Skynet Fee: %v 711 `, currencyUnitsWithExchangeRate(totalSpent, rate), 712 currencyUnitsWithExchangeRate(fm.StorageSpending, rate), 713 currencyUnitsWithExchangeRate(fm.UploadSpending, rate), 714 currencyUnitsWithExchangeRate(fm.DownloadSpending, rate), 715 currencyUnitsWithExchangeRate(fm.FundAccountSpending, rate), currencyUnitsWithExchangeRate(ea.Residue, rate), 716 currencyUnitsWithExchangeRate(ea.AccountBalanceCost, rate), 717 currencyUnitsWithExchangeRate(ea.Balance, rate), 718 bigIntToCurrencyUnitsWithExchangeRate(balanceDrift, rate), 719 currencyUnitsWithExchangeRate(ea.DownloadsCost, rate), 720 currencyUnitsWithExchangeRate(misc, rate), 721 currencyUnitsWithExchangeRate(ea.RegistryReadsCost, rate), 722 currencyUnitsWithExchangeRate(ea.RegistryWritesCost, rate), 723 currencyUnitsWithExchangeRate(repair, rate), 724 currencyUnitsWithExchangeRate(ea.SubscriptionsCost, rate), 725 currencyUnitsWithExchangeRate(ea.UpdatePriceTableCost, rate), 726 currencyUnitsWithExchangeRate(ea.UploadsCost, rate), 727 bigIntToCurrencyUnitsWithExchangeRate(spendingDrift, rate), 728 currencyUnitsWithExchangeRate(fm.MaintenanceSpending.Sum(), rate), 729 currencyUnitsWithExchangeRate(fm.MaintenanceSpending.AccountBalanceCost, rate), 730 currencyUnitsWithExchangeRate(fm.MaintenanceSpending.FundAccountCost, rate), 731 currencyUnitsWithExchangeRate(fm.MaintenanceSpending.UpdatePriceTableCost, rate), 732 currencyUnitsWithExchangeRate(fm.Fees.Sum(), rate), 733 currencyUnitsWithExchangeRate(fm.Fees.ContractFees, rate), 734 currencyUnitsWithExchangeRate(fm.Fees.SiafundFees, rate), 735 currencyUnitsWithExchangeRate(fm.Fees.TransactionFees, rate), 736 currencyUnitsWithExchangeRate(fm.Unspent, rate), 737 currencyUnitsWithExchangeRate(unspentAllocated, rate), 738 currencyUnitsWithExchangeRate(unspentUnallocated, rate), 739 currencyUnitsWithExchangeRate(fm.SkynetFee, rate)) 740 } 741 742 // renewedContracts is a helper function to determine the number of renewed and to renew contracts 743 func renewedContracts(contracts []api.RenterContract, endHeight types.BlockHeight) (renewed, toRenew uint64) { 744 for _, c := range contracts { 745 if c.EndHeight <= endHeight && c.GoodForRenew { 746 toRenew++ 747 } else if c.EndHeight > endHeight { 748 renewed++ 749 } 750 } 751 return 752 } 753 754 // renterallowancespending prints info about the current period spending 755 // this also get called by 'skyc renter -v' which is why it's in its own 756 // function 757 func renterallowancespending(rg api.RenterGET) { 758 // Parse exchange rate 759 rate, err := types.ParseExchangeRate(build.ExchangeRate()) 760 if err != nil { 761 fmt.Printf("Warning: ignoring exchange rate - %s\n", err) 762 } 763 764 // Print current period spending 765 fmt.Printf(` 766 Spending: 767 Current Period Spending:`) 768 769 if rg.Settings.Allowance.Funds.IsZero() { 770 fmt.Printf("\n No current period spending.\n") 771 } else { 772 fmt.Print(currentperiodspending(rg.FinancialMetrics, rate)) 773 } 774 } 775 776 // renterFilesAndContractSummary prints out a summary of what the renter is 777 // storing 778 func renterFilesAndContractSummary(verbose bool) error { 779 rf, err := httpClient.RenterDirRootGet(skymodules.RootSiaPath()) 780 if errors.Contains(err, api.ErrAPICallNotRecognized) { 781 // Assume module is not loaded if status command is not recognized. 782 fmt.Printf("\n Status: %s\n\n", moduleNotReadyStatus) 783 return nil 784 } else if err != nil { 785 return errors.AddContext(err, "unable to get root dir with RenterDirRootGet") 786 } 787 timeSinceHealthCheck := time.Since(rf.Directories[0].AggregateLastHealthCheckTime) 788 789 rc, err := httpClient.RenterDisabledContractsGet() 790 if err != nil { 791 return err 792 } 793 redundancyStr := fmt.Sprintf("%.2f", rf.Directories[0].AggregateMinRedundancy) 794 if rf.Directories[0].AggregateMinRedundancy == -1 { 795 redundancyStr = "-" 796 } 797 // Active Contracts are all good data 798 activeSize, _, _, _ := contractStats(rc.ActiveContracts) 799 // Passive Contracts are all good data 800 passiveSize, _, _, _ := contractStats(rc.PassiveContracts) 801 802 // Grab the RenewWindow Calculation from Skynet Stats 803 ss, err := httpClient.SkynetStatsGet() 804 if err != nil { 805 return err 806 } 807 808 // Renew Window Calculations 809 rg, err := httpClient.RenterGet() 810 if err != nil { 811 return err 812 } 813 cg, err := httpClient.ConsensusGet() 814 if err != nil { 815 return err 816 } 817 renewWindowStart := rg.NextPeriod - rg.Settings.Allowance.RenewWindow 818 var renewBlocksStr string 819 outsideOfRenewWindow := renewWindowStart > cg.Height 820 if outsideOfRenewWindow { 821 // renew window in the future 822 renewBlocksStr = fmt.Sprintf("%v Blocks until Renew", renewWindowStart-cg.Height) 823 } else { 824 // we are in the renew window 825 renewBlocksStr = fmt.Sprintf("%v Blocks Remaining in Renew Window", rg.NextPeriod-cg.Height) 826 } 827 828 // Check on renewal status 829 activeRenewed, activeToRenew := renewedContracts(rc.ActiveContracts, rg.NextPeriod) 830 passiveRenewed, passiveToRenew := renewedContracts(rc.PassiveContracts, rg.NextPeriod) 831 disabledRenewed, disabledToRenew := renewedContracts(rc.DisabledContracts, rg.NextPeriod) 832 833 // Sum totals, disabledRenewed and disabledToRenew are expected to be zero but including just 834 // in case there are some state discrepencies. 835 totalRenewed := activeRenewed + passiveRenewed + disabledRenewed 836 totalToRenew := activeToRenew + passiveToRenew + disabledToRenew 837 838 w := tabwriter.NewWriter(os.Stdout, 2, 0, 2, ' ', 0) 839 fmt.Fprint(w, "File Summary:\n") 840 fmt.Fprintf(w, " Files:\t%v\n", rf.Directories[0].AggregateNumFiles) 841 fmt.Fprintf(w, " Total Stored:\t%v\n", modules.FilesizeUnits(rf.Directories[0].AggregateSize)) 842 fmt.Fprintf(w, " Total Renewing Data:\t%v\n", modules.FilesizeUnits(activeSize+passiveSize)) 843 fmt.Fprint(w, "Repair Status:\n") 844 fmt.Fprintf(w, " Last Health Check:\t%.0fm\n", timeSinceHealthCheck.Minutes()) 845 fmt.Fprintf(w, " Repair Data Remaining:\t%v\n", modules.FilesizeUnits(rf.Directories[0].AggregateRepairSize)) 846 fmt.Fprintf(w, " Stuck Repair Remaining:\t%v\n", modules.FilesizeUnits(rf.Directories[0].AggregateStuckSize)) 847 fmt.Fprintf(w, " Stuck Chunks:\t%v\n", rf.Directories[0].AggregateNumStuckChunks) 848 fmt.Fprintf(w, " Max Health:\t%v%%\n", rf.Directories[0].AggregateMaxHealthPercentage) 849 fmt.Fprintf(w, " Min Redundancy:\t%v\n", redundancyStr) 850 fmt.Fprintf(w, " Lost Files:\t%v\n", rf.Directories[0].AggregateNumLostFiles) 851 fmt.Fprint(w, "Contract Summary:\n") 852 fmt.Fprintf(w, " Renew Window (days):\t%v\n", ss.RenewWindow) 853 // If the allowance isn't set, don't bother printing this renew window 854 // fields. The Above print out will tell the user that no renew window 855 // is set. 856 if !rg.Settings.Allowance.Funds.IsZero() { 857 fmt.Fprintf(w, " Renew Window (blocks):\t%v\n", renewBlocksStr) 858 fmt.Fprintf(w, " Current Period Start:\t%v\n", rg.CurrentPeriod) 859 fmt.Fprintf(w, " Next Period Start:\t%v\n", rg.NextPeriod) 860 fmt.Fprintf(w, " Renew Window Start:\t%v\n", renewWindowStart) 861 // Only print the contract renewal status if we are in the renew 862 // window. Otherwise the current contracts are what was renewed. 863 if !outsideOfRenewWindow { 864 fmt.Fprintf(w, " Renewed Contracts:\t%v\n", totalRenewed) 865 if verbose { 866 fmt.Fprintf(w, " Active:\t%v\n", activeRenewed) 867 fmt.Fprintf(w, " Passive:\t%v\n", passiveRenewed) 868 fmt.Fprintf(w, " Disabled:\t%v\n", disabledRenewed) 869 } 870 fmt.Fprintf(w, " Contracts to Renew:\t%v\n", totalToRenew) 871 if verbose { 872 fmt.Fprintf(w, " Active:\t%v\n", activeToRenew) 873 fmt.Fprintf(w, " Passive:\t%v\n", passiveToRenew) 874 fmt.Fprintf(w, " Disabled:\t%v\n", disabledToRenew) 875 } 876 } 877 } 878 fmt.Fprintf(w, " Active Contracts:\t%v\n", len(rc.ActiveContracts)) 879 fmt.Fprintf(w, " Passive Contracts:\t%v\n", len(rc.PassiveContracts)) 880 fmt.Fprintf(w, " Disabled Contracts:\t%v\n", len(rc.DisabledContracts)) 881 return w.Flush() 882 } 883 884 // renterFilesDownload downloads the file at the specified path from the Sia 885 // network to the local specified destination. 886 func renterFilesDownload(path, destination string) { 887 destination = abs(destination) 888 // Parse SiaPath. 889 siaPath, err := skymodules.NewSiaPath(path) 890 if err != nil { 891 die("Couldn't parse SiaPath:", err) 892 } 893 // If root is not set we need to rebase. 894 if !renterDownloadRoot { 895 siaPath, err = siaPath.Rebase(skymodules.RootSiaPath(), skymodules.UserFolder) 896 if err != nil { 897 die("Couldn't rebase SiaPath:", err) 898 } 899 } 900 // If the destination is a folder, download the file to that folder. 901 fi, err := os.Stat(destination) 902 if err == nil && fi.IsDir() { 903 destination = filepath.Join(destination, siaPath.Name()) 904 } 905 906 // Queue the download. An error will be returned if the queueing failed, but 907 // the call will return before the download has completed. The call is made 908 // as an async call. 909 start := time.Now() 910 cancelID, err := httpClient.RenterDownloadFullGet(siaPath, destination, true, true) 911 if err != nil { 912 die("Download could not be started:", err) 913 } 914 915 // If the download is async, report success. 916 if renterDownloadAsync { 917 fmt.Printf("Queued Download '%s' to %s.\n", siaPath.String(), abs(destination)) 918 fmt.Printf("ID to cancel download: '%v'\n", cancelID) 919 return 920 } 921 922 // If the download is blocking, display progress as the file downloads. 923 var file api.RenterFile 924 file, err = httpClient.RenterFileRootGet(siaPath) 925 if err != nil { 926 die("Error getting file after download has started:", err) 927 } 928 929 failedDownloads := downloadProgress([]trackedFile{{siaPath: siaPath, dst: destination}}) 930 if len(failedDownloads) > 0 { 931 die("\nDownload could not be completed:", failedDownloads[0].Error) 932 } 933 fmt.Printf("\nDownloaded '%s' to '%s - %v in %v'.\n", path, abs(destination), modules.FilesizeUnits(file.File.Filesize), time.Since(start).Round(time.Millisecond)) 934 } 935 936 // renterFileHealthSummary prints out a summary of the status of all the files 937 // in the renter to track the progress of the files 938 func renterFileHealthSummary(dirs []directoryInfo) { 939 percentages, numStuck, err := fileHealthBreakdown(dirs, false) 940 if err != nil { 941 die(err) 942 } 943 944 percentages = parsePercentages(percentages) 945 946 fmt.Println("File Health Summary") 947 w := tabwriter.NewWriter(os.Stdout, 2, 0, 2, ' ', 0) 948 fmt.Fprintf(w, " %% At 100%%\t%v%%\n", percentages[0]) 949 fmt.Fprintf(w, " %% Between 75%% - 100%%\t%v%%\n", percentages[1]) 950 fmt.Fprintf(w, " %% Between 50%% - 75%%\t%v%%\n", percentages[2]) 951 fmt.Fprintf(w, " %% Between 25%% - 50%%\t%v%%\n", percentages[3]) 952 fmt.Fprintf(w, " %% Between 0%% - 25%%\t%v%%\n", percentages[4]) 953 fmt.Fprintf(w, " %% Lost\t%v%%\n", percentages[5]) 954 fmt.Fprintf(w, " Number of Stuck Files\t%v\n", numStuck) 955 if err := w.Flush(); err != nil { 956 die("failed to flush writer:", err) 957 } 958 } 959 960 // writeContracts is a helper function to display contracts 961 func writeContracts(contracts []api.RenterContract) { 962 fmt.Println(" Number of Contracts:", len(contracts)) 963 sort.Sort(byValue(contracts)) 964 w := tabwriter.NewWriter(os.Stdout, 2, 0, 2, ' ', 0) 965 fmt.Fprintln(w, " \nHost\tHost PubKey\tHost Version\tRemaining Funds\tSpent Funds\tSpent Fees\tData\tEnd Height\tContract ID\tGoodForUpload\tGoodForRenew\tGoodForRefresh\tBadContract") 966 for _, c := range contracts { 967 address := c.NetAddress 968 hostVersion := c.HostVersion 969 if address == "" { 970 address = "Host Removed" 971 hostVersion = "" 972 } 973 // Negative Currency Check 974 var contractTotalSpent types.Currency 975 if c.TotalCost.Cmp(c.RenterFunds.Add(c.Fees)) < 0 { 976 contractTotalSpent = c.RenterFunds.Add(c.Fees) 977 } else { 978 contractTotalSpent = c.TotalCost.Sub(c.RenterFunds).Sub(c.Fees) 979 } 980 fmt.Fprintf(w, " %v\t%v\t%v\t%8s\t%8s\t%8s\t%v\t%v\t%v\t%v\t%v\t%v\t%v\n", 981 address, 982 c.HostPublicKey.String(), 983 hostVersion, 984 currencyUnits(c.RenterFunds), 985 currencyUnits(contractTotalSpent), 986 currencyUnits(c.Fees), 987 modules.FilesizeUnits(c.Size), 988 c.EndHeight, 989 c.ID, 990 c.GoodForUpload, 991 c.GoodForRenew, 992 c.GoodForRefresh, 993 c.BadContract) 994 } 995 if err := w.Flush(); err != nil { 996 die("failed to flush writer:", err) 997 } 998 } 999 1000 // writeWorkerDownloadInfo is a helper function for writing the download 1001 // information to the tabwriter. 1002 func writeWorkerDownloadInfo(w *tabwriter.Writer, rw skymodules.WorkerPoolStatus) { 1003 // print summary 1004 fmt.Fprintf(w, "Worker Pool Summary \n") 1005 fmt.Fprintf(w, " Total Workers: \t%v\n", rw.NumWorkers) 1006 fmt.Fprintf(w, " Workers On HasSector Cooldown:\t%v\n", rw.TotalHasSectorCoolDown) 1007 fmt.Fprintf(w, " Workers On Download Cooldown:\t%v\n", rw.TotalDownloadCoolDown) 1008 1009 // print header 1010 hostInfo := "Host PubKey" 1011 info := "\tOn Cooldown\tCooldown Time\tLast Error\tQueue\tTerminated" 1012 header := hostInfo + info 1013 fmt.Fprintln(w, "\nWorker Downloads Detail \n\n"+header) 1014 1015 // print rows 1016 for _, worker := range rw.Workers { 1017 // Host Info 1018 fmt.Fprintf(w, "%v", worker.HostPubKey.String()) 1019 1020 // Download Info 1021 fmt.Fprintf(w, "\t%v\t%v\t%v\t%v\t%v\n", 1022 worker.DownloadOnCoolDown, 1023 absDuration(worker.DownloadCoolDownTime), 1024 sanitizeErr(worker.DownloadCoolDownError), 1025 worker.DownloadQueueSize, 1026 worker.DownloadTerminated) 1027 } 1028 } 1029 1030 // writeWorkerRepairInfo is a helper function for writing the low prio download 1031 // information (used for repairs) to the tabwriter. 1032 func writeWorkerRepairInfo(w *tabwriter.Writer, rw skymodules.WorkerPoolStatus) { 1033 // print summary 1034 fmt.Fprintf(w, "Worker Pool Summary \n") 1035 fmt.Fprintf(w, " Total Workers: \t%v\n", rw.NumWorkers) 1036 fmt.Fprintf(w, " Workers On Repair Cooldown:\t%v\n", rw.TotalLowPrioDownloadCoolDown) 1037 1038 // print header 1039 hostInfo := "Host PubKey" 1040 info := "\tOn Cooldown\tCooldown Time\tLast Error\tQueue\tTerminated" 1041 header := hostInfo + info 1042 fmt.Fprintln(w, "\nWorker Repair Detail \n\n"+header) 1043 1044 // print rows 1045 for _, worker := range rw.Workers { 1046 // Host Info 1047 fmt.Fprintf(w, "%v", worker.HostPubKey.String()) 1048 1049 // Download Info 1050 fmt.Fprintf(w, "\t%v\t%v\t%v\t%v\t%v\n", 1051 worker.LowPrioDownloadOnCoolDown, 1052 absDuration(worker.LowPrioDownloadCoolDownTime), 1053 sanitizeErr(worker.LowPrioDownloadCoolDownError), 1054 worker.LowPrioDownloadQueueSize, 1055 worker.LowPrioDownloadTerminated) 1056 } 1057 } 1058 1059 // writeWorkerMaintenanceInfo is a helper function for writing the maintenance 1060 // state for every worker. 1061 func writeWorkerMaintenanceInfo(w *tabwriter.Writer, rw skymodules.WorkerPoolStatus) { 1062 // print summary 1063 fmt.Fprintf(w, "Worker Pool Summary \n") 1064 fmt.Fprintf(w, " Total Workers: \t%v\n", rw.NumWorkers) 1065 fmt.Fprintf(w, " Workers On Maintenance Cooldown:\t%v\n", rw.TotalMaintenanceCoolDown) 1066 1067 // print header 1068 hostInfo := "Host PubKey" 1069 info := "\tOn Cooldown\tCooldown Time\tLast Error" 1070 header := hostInfo + info 1071 fmt.Fprintln(w, "\nWorker Maintenance Detail \n\n"+header) 1072 1073 // print rows 1074 for _, worker := range rw.Workers { 1075 // Host Info 1076 fmt.Fprintf(w, "%v", worker.HostPubKey.String()) 1077 1078 // Download Info 1079 fmt.Fprintf(w, "\t%v\t%v\t%v\n", 1080 worker.MaintenanceOnCooldown, 1081 absDuration(worker.MaintenanceCoolDownTime), 1082 sanitizeErr(worker.MaintenanceCoolDownError)) 1083 } 1084 } 1085 1086 // writeWorkerUploadInfo is a helper function for writing the upload information 1087 // to the tabwriter. 1088 func writeWorkerUploadInfo(w *tabwriter.Writer, rw skymodules.WorkerPoolStatus) { 1089 // print summary 1090 fmt.Fprintf(w, "Worker Pool Summary \n") 1091 fmt.Fprintf(w, " Total Workers: \t%v\n", rw.NumWorkers) 1092 fmt.Fprintf(w, " Workers On Upload Cooldown:\t%v\n", rw.TotalUploadCoolDown) 1093 1094 // print header 1095 hostInfo := "Host PubKey" 1096 info := "\tOn Cooldown\tCooldown Time\tLast Error\tQueue\tTerminated" 1097 header := hostInfo + info 1098 fmt.Fprintln(w, "\nWorker Uploads Detail \n\n"+header) 1099 1100 // print rows 1101 for _, worker := range rw.Workers { 1102 // Host Info 1103 fmt.Fprintf(w, "%v", worker.HostPubKey.String()) 1104 1105 // Upload Info 1106 fmt.Fprintf(w, "\t%v\t%v\t%v\t%v\t%v\n", 1107 worker.UploadOnCoolDown, 1108 absDuration(worker.UploadCoolDownTime), 1109 sanitizeErr(worker.UploadCoolDownError), 1110 worker.UploadQueueSize, 1111 worker.UploadTerminated) 1112 } 1113 } 1114 1115 // writeWorkerReadUpdateRegistryInfo is a helper function for writing the read registry 1116 // or update registry information to the tabwriter. 1117 func writeWorkerReadUpdateRegistryInfo(read bool, w *tabwriter.Writer, rw skymodules.WorkerPoolStatus) { 1118 // print summary 1119 fmt.Fprintf(w, "Worker Pool Summary \n") 1120 fmt.Fprintf(w, " Total Workers: \t%v\n", rw.NumWorkers) 1121 if read { 1122 fmt.Fprintf(w, " Workers On ReadRegistry Cooldown:\t%v\n", rw.TotalDownloadCoolDown) 1123 } else { 1124 fmt.Fprintf(w, " Workers On UpdateRegistry Cooldown:\t%v\n", rw.TotalUploadCoolDown) 1125 } 1126 1127 // print header 1128 hostInfo := "Host PubKey" 1129 info := "\tOn Cooldown\tCooldown Time\tLast Error\tLast Error Time\tQueue" 1130 header := hostInfo + info 1131 if read { 1132 fmt.Fprintln(w, "\nWorker ReadRegistry Detail \n\n"+header) 1133 } else { 1134 fmt.Fprintln(w, "\nWorker UpdateRegistry Detail \n\n"+header) 1135 } 1136 1137 // print rows 1138 for _, worker := range rw.Workers { 1139 // Host Info 1140 fmt.Fprintf(w, "%v", worker.HostPubKey.String()) 1141 1142 // Qeue Info 1143 if read { 1144 status := worker.ReadRegistryJobsStatus 1145 fmt.Fprintf(w, "\t%v\t%v\t%v\t%v\t%v\n", 1146 status.OnCooldown, 1147 absDuration(time.Until(status.OnCooldownUntil)), 1148 sanitizeErr(status.RecentErr), 1149 status.RecentErrTime, 1150 status.JobQueueSize) 1151 } else { 1152 status := worker.UpdateRegistryJobsStatus 1153 fmt.Fprintf(w, "\t%v\t%v\t%v\t%v\t%v\n", 1154 status.OnCooldown, 1155 absDuration(time.Until(status.OnCooldownUntil)), 1156 sanitizeErr(status.RecentErr), 1157 status.RecentErrTime, 1158 status.JobQueueSize) 1159 } 1160 } 1161 }