Skip to content

Commit ff8fb1b

Browse files
feat: relax network deadlines from install, env, and doctor
Co-authored-by: Cursor
1 parent dc74d33 commit ff8fb1b

8 files changed

Lines changed: 217 additions & 32 deletions

File tree

‎src/cmd/main.go‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"common/fs"
77
"common/license"
88
"common/notify"
9+
"common/resolver"
910
"common/settings"
1011
"common/system"
1112
"common/verifycache"
@@ -32,6 +33,7 @@ var (
3233
)
3334

3435
func main() {
36+
resolver.OnDeadline = log.LogNetworkDeadline
3537
if len(os.Args) < 2 {
3638
os.Args = append(os.Args, "--help")
3739
}

‎src/commands/env.go‎

Lines changed: 52 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import (
1515
"nvm/bootstrap"
1616
"nvm/commands/cache"
1717
"nvm/constant"
18+
"nvm/log"
1819
"nvm/status"
1920
"os"
2021
"os/user"
@@ -33,22 +34,18 @@ var (
3334
helpURL string = "https://docs.nvm-windows.com"
3435
)
3536

36-
const remoteReachabilityTimeout = 1500 * time.Millisecond
37-
38-
// Use common/http so Author mirrors (mirror.author.io) get Bearer access token.
39-
var reachabilityClient = nvmhttp.NewClient(remoteReachabilityTimeout)
40-
4137
type Env struct {
4238
constant.FlagJSON
39+
RelaxDeadlines constant.RelaxDeadlines `optional:"" placeholder:"MS" help:"Relax network deadlines for this command. Omit a value to triple the configured budgets, or pass milliseconds."`
4340
}
4441

4542
type installData struct {
46-
Version string `json:"version"`
43+
Version string `json:"version"`
4744
BuildTime string `json:"build_time"`
4845
BuildArchitecture string `json:"build_architecture"`
4946
InstallDir string `json:"path"`
50-
Upgrade string `json:"upgrade"`
51-
Variables map[string]string `json:"variables"`
47+
Upgrade string `json:"upgrade"`
48+
Variables map[string]string `json:"variables"`
5249
}
5350

5451
type vmOps struct {
@@ -69,6 +66,10 @@ type vmOps struct {
6966
VersionsCacheRoot string `json:"cache_root"`
7067
CachedVersionsCount int `json:"cache_count"`
7168
CachedVersionsSizeMB int64 `json:"cache_size_mb"`
69+
TimeoutCatalog string `json:"timeout_catalog_ms"`
70+
TimeoutCatalogMirror string `json:"timeout_catalog_mirror_ms"`
71+
TimeoutReachability string `json:"timeout_reachability_ms"`
72+
TimeoutDownload string `json:"timeout_download_ms"`
7273
}
7374

7475
// nodeRuntimeFlags reports shim-enforced Node.js CLI security flags.
@@ -135,6 +136,7 @@ var (
135136
)
136137

137138
func (e *Env) Run(ctx *kong.Context, vars kong.Vars) error {
139+
settings.UseRelax(e.RelaxDeadlines.Setting())
138140
var spinner *status.Spinner
139141
if !e.JSON {
140142
spinner = status.NewSpinner("Analyzing environment")
@@ -249,6 +251,7 @@ func (e *Env) Run(ctx *kong.Context, vars kong.Vars) error {
249251
}
250252
}
251253

254+
budgets := settings.ActiveNetworkBudgets()
252255
node_ping_results := runReachabilityChecks(cfg.NodeMirror, isNodeMirrorReachable)
253256
npm_ping_results := runReachabilityChecks(cfg.NpmMirror, isNpmMirrorReachable)
254257

@@ -289,7 +292,7 @@ func (e *Env) Run(ctx *kong.Context, vars kong.Vars) error {
289292
BuildTime: vars["buildTime"],
290293
BuildArchitecture: buildArchitecture,
291294
InstallDir: path(programRoot),
292-
Upgrade: map[bool]string{true: "blocked", false: "allowed"}[cfg.DisableUpgrade],
295+
Upgrade: map[bool]string{true: "blocked", false: "allowed"}[cfg.DisableUpgrade],
293296
// Variables: map[string]string{
294297
// "NVM_HOME": getUserEnvVar("NVM_HOME"),
295298
// "NVM_NODE_PATH": getUserEnvVar("NVM_NODE_PATH"),
@@ -312,6 +315,10 @@ func (e *Env) Run(ctx *kong.Context, vars kong.Vars) error {
312315
NpmGlobalModuleTotal: moduleTotalCount,
313316
NpmGlobalModuleUnique: moduleUniqueCount,
314317
NpmModuleSizeMB: moduleSizeBytes / (1024 * 1024),
318+
TimeoutCatalog: budgets.Catalog.Note(),
319+
TimeoutCatalogMirror: budgets.CatalogMirror.Note(),
320+
TimeoutReachability: budgets.Reachability.Note(),
321+
TimeoutDownload: budgets.Download.Note(),
315322
},
316323
Node: nodeFlags,
317324
PackageManagers: packageManagersCfg{
@@ -453,10 +460,7 @@ func (e *Env) Run(ctx *kong.Context, vars kong.Vars) error {
453460
// Node Mirrors
454461
fmt.Fprintf(t, "%s%s Download Sources\t\n", indent(1), branch)
455462
for i, mirror := range out.VersionManagement.NodeMirror {
456-
reachable := ""
457-
if !out.VersionManagement.NodeMirrorPingResult[mirror] {
458-
reachable = " (unreachable)"
459-
}
463+
reachable := unreachableNote(out.VersionManagement.NodeMirrorPingResult[mirror], out.VersionManagement.TimeoutReachability)
460464

461465
if i == 0 {
462466
fmt.Fprintf(t, "%s%s%s %s Node.js\t: %s%s\n", indent(1), line, indent(1), branch, mirror, reachable)
@@ -467,10 +471,7 @@ func (e *Env) Run(ctx *kong.Context, vars kong.Vars) error {
467471

468472
// npm Mirrors
469473
for i, mirror := range out.VersionManagement.NpmMirror {
470-
reachable := ""
471-
if !out.VersionManagement.NpmMirrorPingResult[mirror] {
472-
reachable = " (unreachable)"
473-
}
474+
reachable := unreachableNote(out.VersionManagement.NpmMirrorPingResult[mirror], out.VersionManagement.TimeoutReachability)
474475

475476
if i == 0 {
476477
fmt.Fprintf(t, "%s%s%s %s npm\t: %s%s\n", indent(1), line, indent(1), end, mirror, reachable)
@@ -479,6 +480,12 @@ func (e *Env) Run(ctx *kong.Context, vars kong.Vars) error {
479480
}
480481
}
481482

483+
fmt.Fprintf(t, "%s%s Network deadlines\t\n", indent(1), branch)
484+
fmt.Fprintf(t, "%s%s%s %s TimeoutCatalogMs\t: %s\n", indent(1), line, indent(1), branch, out.VersionManagement.TimeoutCatalog)
485+
fmt.Fprintf(t, "%s%s%s %s TimeoutCatalogMirrorMs\t: %s\n", indent(1), line, indent(1), branch, out.VersionManagement.TimeoutCatalogMirror)
486+
fmt.Fprintf(t, "%s%s%s %s TimeoutReachabilityMs\t: %s\n", indent(1), line, indent(1), branch, out.VersionManagement.TimeoutReachability)
487+
fmt.Fprintf(t, "%s%s%s %s TimeoutDownloadMs\t: %s\n", indent(1), line, indent(1), end, out.VersionManagement.TimeoutDownload)
488+
482489
fmt.Fprintf(t, "%s%s Installed Versions\t\n", indent(1), branch)
483490

484491
// Active Version
@@ -960,9 +967,33 @@ func runReachabilityChecks(mirrors []string, checker func(string) bool) map[stri
960967
return results
961968
}
962969

970+
func unreachableNote(reachable bool, budget string) string {
971+
if reachable {
972+
return ""
973+
}
974+
if strings.Contains(budget, settings.SourceCommandFlag) {
975+
return " (unreachable; phase=reachability " + budget + ")"
976+
}
977+
return " (unreachable)"
978+
}
979+
980+
func reachabilityClient() *nvmhttp.Client {
981+
return nvmhttp.NewClient(settings.ActiveNetworkBudgets().Reachability.Duration())
982+
}
983+
984+
func noteReachabilityDeadline(url string, err error) {
985+
if !nvmhttp.IsDeadline(err) {
986+
return
987+
}
988+
b := settings.ActiveNetworkBudgets().Reachability
989+
log.LogNetworkDeadline("reachability", url, b.Milliseconds, b.Source)
990+
}
991+
963992
func isNodeMirrorReachable(url string) bool {
964-
res, err := reachabilityClient.Head(url + "/index.tab")
993+
target := strings.TrimRight(url, "/") + "/index.tab"
994+
res, err := reachabilityClient().Head(target)
965995
if err != nil {
996+
noteReachabilityDeadline(target, err)
966997
return false
967998
}
968999
defer res.Body.Close()
@@ -975,8 +1006,10 @@ func isNodeMirrorReachable(url string) bool {
9751006
}
9761007

9771008
func isNpmMirrorReachable(url string) bool {
978-
res, err := reachabilityClient.Get(url + "/-/ping")
1009+
target := strings.TrimRight(url, "/") + "/-/ping"
1010+
res, err := reachabilityClient().Get(target)
9791011
if err != nil {
1012+
noteReachabilityDeadline(target, err)
9801013
return false
9811014
}
9821015
defer res.Body.Close()

‎src/commands/install/install.go‎

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,18 +10,20 @@ import (
1010
)
1111

1212
type Install struct {
13-
CopyFrom string `flag:"copy-from" help:"Copy the global modules from an existing Node.js installation to the new installation. This does affect the original version and does not verify module compatibility with the new version." placeholder:"VERSION"`
14-
From string `flag:"from" help:"Install global modules from the list of modules installed in an another Node.js version." placeholder:"VERSION"`
15-
Notify bool `flag:"notify" hidden:"true" help:"Notify user when a version has been automatically installed."`
16-
Cache bool `flag:"cache" help:"Save the download (offline cache)."`
17-
NoCache bool `flag:"no-cache" help:"Install without caching. This is the default unless configured to always cache downloads."`
18-
Force bool `flag:"force" help:"Force (re)install if the version already exists."`
19-
Debug bool `flag:"debug" hidden:"true" help:"Write timing logs to the install directory for debugging."`
20-
Insecure bool `flag:"insecure" help:"Accept invalid TLS/SSL certs from download sources."`
13+
CopyFrom string `flag:"copy-from" help:"Copy the global modules from an existing Node.js installation to the new installation. This does affect the original version and does not verify module compatibility with the new version." placeholder:"VERSION"`
14+
From string `flag:"from" help:"Install global modules from the list of modules installed in an another Node.js version." placeholder:"VERSION"`
15+
Notify bool `flag:"notify" hidden:"true" help:"Notify user when a version has been automatically installed."`
16+
Cache bool `flag:"cache" help:"Save the download (offline cache)."`
17+
NoCache bool `flag:"no-cache" help:"Install without caching. This is the default unless configured to always cache downloads."`
18+
Force bool `flag:"force" help:"Force (re)install if the version already exists."`
19+
Debug bool `flag:"debug" hidden:"true" help:"Write timing logs to the install directory for debugging."`
20+
Insecure bool `flag:"insecure" help:"Accept invalid TLS/SSL certs from download sources."`
21+
RelaxDeadlines constant.RelaxDeadlines `optional:"" placeholder:"MS" help:"Relax network deadlines for this command. Omit a value to triple the configured budgets, or pass milliseconds."`
2122
constant.ArgVersion
2223
}
2324

2425
func (s *Install) Run(ctx *kong.Context) error {
26+
settings.UseRelax(s.RelaxDeadlines.Setting())
2527
cfg := settings.Global()
2628

2729
var cacheRoot string

‎src/commands/sync.go‎

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -49,11 +49,15 @@ func (s *Upgrade) Run() error {
4949
}
5050

5151
type Doctor struct {
52-
Checks []string `arg:"" optional:"" help:"Specific checks to run. If not specified, all checks will be run."`
53-
Autofix bool `flag:"autofix" help:"Automatically fix issues when possible."`
54-
List bool `flag:"list" help:"List all available checks without running them."`
55-
Update bool `flag:"update" help:"Force sync utility asset update before running checks."`
56-
Force bool `flag:"force" help:"When upgrading, do not warn about processes that will be closed."`
52+
Checks []string `arg:"" optional:"" help:"Specific checks to run. If not specified, all checks will be run."`
53+
Autofix bool `flag:"autofix" help:"Automatically fix issues when possible."`
54+
List bool `flag:"list" help:"List all available checks without running them."`
55+
Update bool `flag:"update" help:"Force sync utility asset update before running checks."`
56+
Force bool `flag:"force" help:"When upgrading, do not warn about processes that will be closed."`
57+
RelaxDeadlines constant.RelaxDeadlines `optional:"" placeholder:"MS" help:"Relax network deadlines for this command. Omit a value to triple the configured budgets, or pass milliseconds."`
58+
MeasureDeadlines bool `flag:"measure-deadlines" help:"Time each download source and print min, average, and max."`
59+
MeasureCount int `flag:"measure-count" default:"3" help:"How many times to repeat each deadline measurement."`
60+
AutoDeadlines bool `flag:"auto-deadlines" help:"Measure deadlines and save HKCU values from the results."`
5761
constant.FlagJSON
5862
}
5963

@@ -76,6 +80,22 @@ func (c *Doctor) Run() error {
7680
if c.Force {
7781
args = append(args, "--force")
7882
}
83+
if c.RelaxDeadlines.Active() {
84+
if c.RelaxDeadlines.Milliseconds() > 0 {
85+
args = append(args, fmt.Sprintf("--relax-deadlines=%d", c.RelaxDeadlines.Milliseconds()))
86+
} else {
87+
args = append(args, "--relax-deadlines")
88+
}
89+
}
90+
if c.MeasureDeadlines {
91+
args = append(args, "--measure-deadlines")
92+
}
93+
if c.AutoDeadlines {
94+
args = append(args, "--auto-deadlines")
95+
}
96+
if (c.MeasureDeadlines || c.AutoDeadlines) && c.MeasureCount > 0 {
97+
args = append(args, "--measure-count", fmt.Sprintf("%d", c.MeasureCount))
98+
}
7999
if c.JSON {
80100
args = append(args, "--json")
81101
}

‎src/constant/relax.go‎

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
package constant
2+
3+
import (
4+
"common/settings"
5+
"fmt"
6+
"strconv"
7+
"strings"
8+
9+
"github.com/alecthomas/kong"
10+
)
11+
12+
// RelaxDeadlines is --relax-deadlines.
13+
// A bare flag triples configured budgets. A positive integer is milliseconds.
14+
// Fields stay unexported so kong treats the value as one flag.
15+
type RelaxDeadlines struct {
16+
active bool
17+
milliseconds int
18+
}
19+
20+
func (r RelaxDeadlines) Active() bool { return r.active }
21+
22+
func (r RelaxDeadlines) Milliseconds() int { return r.milliseconds }
23+
24+
func (r RelaxDeadlines) Setting() settings.RelaxDeadlines {
25+
return settings.RelaxDeadlines{Active: r.active, Milliseconds: r.milliseconds}
26+
}
27+
28+
func (r *RelaxDeadlines) Decode(ctx *kong.DecodeContext) error {
29+
r.active = true
30+
raw, ok := tokenString(ctx.Scan.Peek().Value)
31+
if !ok || raw == "" || strings.HasPrefix(raw, "-") {
32+
return nil
33+
}
34+
n, err := strconv.Atoi(raw)
35+
if err != nil {
36+
return nil
37+
}
38+
if n <= 0 {
39+
return fmt.Errorf("--relax-deadlines must be a positive number of milliseconds")
40+
}
41+
ctx.Scan.Pop()
42+
r.milliseconds = n
43+
return nil
44+
}
45+
46+
func tokenString(v any) (string, bool) {
47+
s, ok := v.(string)
48+
return s, ok
49+
}

‎src/constant/relax_test.go‎

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
package constant
2+
3+
import (
4+
"testing"
5+
6+
"github.com/alecthomas/kong"
7+
)
8+
9+
func TestRelaxFlagParsing(t *testing.T) {
10+
parse := func(args ...string) (RelaxDeadlines, []string) {
11+
t.Helper()
12+
var cli struct {
13+
RelaxDeadlines RelaxDeadlines `optional:"" placeholder:"MS" help:"Relax network deadlines for this command."`
14+
Version []string `arg:"" optional:""`
15+
}
16+
parser := kong.Must(&cli)
17+
_, err := parser.Parse(args)
18+
if err != nil {
19+
t.Fatalf("parse %v: %v", args, err)
20+
}
21+
return cli.RelaxDeadlines, cli.Version
22+
}
23+
24+
bare, version := parse("--relax-deadlines")
25+
if !bare.Active() || bare.Milliseconds() != 0 {
26+
t.Fatalf("bare = %+v", bare)
27+
}
28+
if len(version) != 0 {
29+
t.Fatalf("bare consumed args: %v", version)
30+
}
31+
32+
custom, version := parse("--relax-deadlines", "9000", "20")
33+
if !custom.Active() || custom.Milliseconds() != 9000 {
34+
t.Fatalf("custom = %+v", custom)
35+
}
36+
if len(version) != 1 || version[0] != "20" {
37+
t.Fatalf("version = %v", version)
38+
}
39+
40+
absent, version := parse("22")
41+
if absent.Active() {
42+
t.Fatal("flag should be absent")
43+
}
44+
if len(version) != 1 || version[0] != "22" {
45+
t.Fatalf("version = %v", version)
46+
}
47+
}

‎src/installer/download_error.go‎

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,12 @@ package installer
22

33
import (
44
nvmhttp "common/http"
5+
"common/settings"
56
"fmt"
67
"net/http"
78
"strings"
9+
10+
"nvm/log"
811
)
912

1013
func describeDownloadResultFailure(label, url string, result nvmhttp.DownloadResult) error {
@@ -38,6 +41,22 @@ func formatNodeMirrorDownloadFailure(version, archiveName string, mirrors []stri
3841
mirrorNote,
3942
)
4043
}
44+
if nvmhttp.IsDeadline(lastErr) {
45+
b := settings.ActiveNetworkBudgets().Download
46+
log.LogNetworkDeadline("download", "", b.Milliseconds, b.Source)
47+
note := "TimeoutDownloadMs " + b.Note()
48+
if settings.ActiveNetworkBudgets().Verbose {
49+
note = "phase=download " + note
50+
}
51+
return fmt.Errorf(
52+
"failed to download Node.js v%s (%s) from %s (%s): %w",
53+
version,
54+
archiveName,
55+
mirrorNote,
56+
note,
57+
lastErr,
58+
)
59+
}
4160
return fmt.Errorf(
4261
"failed to download Node.js v%s (%s) from %s: %w",
4362
version,

0 commit comments

Comments
 (0)