|
| 1 | +package main |
| 2 | + |
| 3 | +import ( |
| 4 | + "context" |
| 5 | + "fmt" |
| 6 | + "net/url" |
| 7 | + "os" |
| 8 | + "path/filepath" |
| 9 | + "sync" |
| 10 | + "time" |
| 11 | + |
| 12 | + "github.com/Masterminds/semver" |
| 13 | + |
| 14 | + "github.com/sourcegraph/src-cli/internal/api" |
| 15 | + "github.com/sourcegraph/src-cli/internal/version" |
| 16 | +) |
| 17 | + |
| 18 | +const ( |
| 19 | + // versionCheckTimeout bounds the recommended-version lookup, which runs |
| 20 | + // before the command the user asked for. |
| 21 | + versionCheckTimeout = 3 * time.Second |
| 22 | + |
| 23 | + // versionCheckInterval is how long the result of a lookup is reused before |
| 24 | + // the instance is asked again. |
| 25 | + versionCheckInterval = 24 * time.Hour |
| 26 | +) |
| 27 | + |
| 28 | +// warnOutdatedVersionOnce limits the check to once per run of src-cli, however |
| 29 | +// many API clients the command creates. |
| 30 | +var warnOutdatedVersionOnce sync.Once |
| 31 | + |
| 32 | +// maybeWarnOutdatedVersion prints a warning to stderr when src-cli is older |
| 33 | +// than the version recommended by the Sourcegraph instance at endpointURL, |
| 34 | +// which it asks using client unless a recent answer is cached. |
| 35 | +// |
| 36 | +// It never fails the command: an unreachable instance, an unparseable version, |
| 37 | +// or an unwritable cache all result in no warning. |
| 38 | +func maybeWarnOutdatedVersion(client api.Client, endpointURL *url.URL, subcommand string) { |
| 39 | + // 'src version' asks the instance itself, and then calls |
| 40 | + // warnOutdatedVersionFromLookup with the answer. |
| 41 | + if subcommand == "version" || skipVersionCheck() { |
| 42 | + return |
| 43 | + } |
| 44 | + cachePath, err := recommendedVersionCachePath(endpointURL) |
| 45 | + if err != nil { |
| 46 | + return |
| 47 | + } |
| 48 | + printOutdatedVersionWarning(recommendedVersion(client, cachePath), endpointURL) |
| 49 | +} |
| 50 | + |
| 51 | +// warnOutdatedVersionFromLookup is the check for 'src version', which has just |
| 52 | +// been told the recommended version by the instance at endpointURL. The answer |
| 53 | +// is cached, so that the commands that follow warn about the same version. |
| 54 | +func warnOutdatedVersionFromLookup(recommended string, endpointURL *url.URL) { |
| 55 | + if skipVersionCheck() { |
| 56 | + return |
| 57 | + } |
| 58 | + if cachePath, err := recommendedVersionCachePath(endpointURL); err == nil { |
| 59 | + _ = cacheRecommendedVersion(cachePath, recommended) |
| 60 | + } |
| 61 | + printOutdatedVersionWarning(recommended, endpointURL) |
| 62 | +} |
| 63 | + |
| 64 | +// skipVersionCheck reports whether the version check is disabled, or has no |
| 65 | +// version to compare. |
| 66 | +func skipVersionCheck() bool { |
| 67 | + return os.Getenv("SRC_SKIP_VERSION_CHECK") != "" || version.BuildTag == version.DefaultBuildTag |
| 68 | +} |
| 69 | + |
| 70 | +func printOutdatedVersionWarning(recommended string, endpointURL *url.URL) { |
| 71 | + if warning := outdatedVersionWarning(recommended, endpointURL); warning != "" { |
| 72 | + fmt.Fprintln(os.Stderr, warning) |
| 73 | + } |
| 74 | +} |
| 75 | + |
| 76 | +// outdatedVersionWarning returns the warning to print, or "" when src-cli is |
| 77 | +// not known to be older than the recommended version. |
| 78 | +func outdatedVersionWarning(recommendedVersion string, endpointURL *url.URL) string { |
| 79 | + current, err := semver.NewVersion(version.BuildTag) |
| 80 | + if err != nil { |
| 81 | + return "" |
| 82 | + } |
| 83 | + recommended, err := semver.NewVersion(recommendedVersion) |
| 84 | + if err != nil { |
| 85 | + return "" |
| 86 | + } |
| 87 | + if !current.LessThan(recommended) { |
| 88 | + return "" |
| 89 | + } |
| 90 | + return fmt.Sprintf("⚠️ Warning: src-cli %s is older than %s, the version recommended by %s. "+ |
| 91 | + "To upgrade, see https://github.com/sourcegraph/src-cli#installation. "+ |
| 92 | + "To silence this warning, set SRC_SKIP_VERSION_CHECK=1.", |
| 93 | + current, recommended, endpointURL) |
| 94 | +} |
| 95 | + |
| 96 | +// recommendedVersionCachePath returns the file that holds the version |
| 97 | +// recommended by the instance at endpointURL. |
| 98 | +func recommendedVersionCachePath(endpointURL *url.URL) (string, error) { |
| 99 | + cacheDir, err := os.UserCacheDir() |
| 100 | + if err != nil { |
| 101 | + return "", err |
| 102 | + } |
| 103 | + return filepath.Join(cacheDir, "sourcegraph", "src-cli", "recommended-version-"+url.QueryEscape(endpointURL.String())), nil |
| 104 | +} |
| 105 | + |
| 106 | +// cacheRecommendedVersion records recommended, and the time it was learned, in |
| 107 | +// the file at cachePath. |
| 108 | +func cacheRecommendedVersion(cachePath, recommended string) error { |
| 109 | + if err := os.MkdirAll(filepath.Dir(cachePath), 0o700); err != nil { |
| 110 | + return err |
| 111 | + } |
| 112 | + return os.WriteFile(cachePath, []byte(recommended), 0o600) |
| 113 | +} |
| 114 | + |
| 115 | +// recommendedVersion returns the src-cli version recommended by the instance |
| 116 | +// that client talks to, or "" when it is unknown. The answer is cached in the |
| 117 | +// file at cachePath for versionCheckInterval, so most commands make no extra |
| 118 | +// request. |
| 119 | +func recommendedVersion(client api.Client, cachePath string) string { |
| 120 | + if info, err := os.Stat(cachePath); err == nil && time.Since(info.ModTime()) < versionCheckInterval { |
| 121 | + cached, _ := os.ReadFile(cachePath) |
| 122 | + return string(cached) |
| 123 | + } |
| 124 | + |
| 125 | + // Record the attempt before the lookup, so that an instance that does not |
| 126 | + // answer delays one command per versionCheckInterval instead of every |
| 127 | + // command. Without a writable cache there is no such bound, so do not look. |
| 128 | + if cacheRecommendedVersion(cachePath, "") != nil { |
| 129 | + return "" |
| 130 | + } |
| 131 | + |
| 132 | + ctx, cancel := context.WithTimeout(context.Background(), versionCheckTimeout) |
| 133 | + defer cancel() |
| 134 | + recommended, err := getRecommendedVersion(ctx, client) |
| 135 | + if err != nil { |
| 136 | + return "" |
| 137 | + } |
| 138 | + _ = cacheRecommendedVersion(cachePath, recommended) |
| 139 | + return recommended |
| 140 | +} |
0 commit comments