Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ jobs:
runs-on: ubuntu-slim
permissions:
contents: read
pull-requests: read
outputs:
has_changes: ${{ steps.compute.outputs.has_changes }}
matrix: ${{ steps.compute.outputs.matrix }}
Expand Down Expand Up @@ -78,6 +79,7 @@ jobs:
runs-on: ubuntu-latest
permissions:
contents: read
members: read
steps:
- name: Validate Team Membership For Sensitive Changes
env:
Expand Down Expand Up @@ -123,6 +125,14 @@ jobs:
- name: Checkout Code
uses: actions/checkout@v6

- name: Checkout common modules
shell: pwsh
run: |
$dest = Join-Path (Split-Path (Split-Path $env:GITHUB_WORKSPACE -Parent) -Parent) "common"
if (-not (Test-Path (Join-Path $dest "settings\go.mod"))) {
git clone --depth 1 https://github.com/nvm-windows/common.git $dest
}

- name: Set up Go
uses: actions/setup-go@v6
with:
Expand Down
2 changes: 2 additions & 0 deletions src/cmd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"common/fs"
"common/license"
"common/notify"
"common/resolver"
"common/settings"
"common/system"
"common/verifycache"
Expand All @@ -32,6 +33,7 @@ var (
)

func main() {
resolver.OnDeadline = log.LogNetworkDeadline
if len(os.Args) < 2 {
os.Args = append(os.Args, "--help")
}
Expand Down
72 changes: 53 additions & 19 deletions src/commands/env.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import (
"nvm/bootstrap"
"nvm/commands/cache"
"nvm/constant"
"nvm/log"
"nvm/status"
"os"
"os/user"
Expand All @@ -33,22 +34,19 @@ var (
helpURL string = "https://docs.nvm-windows.com"
)

const remoteReachabilityTimeout = 1500 * time.Millisecond

// Use common/http so Author mirrors (mirror.author.io) get Bearer access token.
var reachabilityClient = nvmhttp.NewClient(remoteReachabilityTimeout)

type Env struct {
constant.FlagJSON
RelaxDeadlines constant.RelaxDeadlines `optional:"" placeholder:"MS" help:"Relax network deadlines for this command. Omit a value to triple the configured budgets, or pass milliseconds."`
Deadlines constant.DeadlineFlags `embed:""`
}

type installData struct {
Version string `json:"version"`
Version string `json:"version"`
BuildTime string `json:"build_time"`
BuildArchitecture string `json:"build_architecture"`
InstallDir string `json:"path"`
Upgrade string `json:"upgrade"`
Variables map[string]string `json:"variables"`
Upgrade string `json:"upgrade"`
Variables map[string]string `json:"variables"`
}

type vmOps struct {
Expand All @@ -69,6 +67,10 @@ type vmOps struct {
VersionsCacheRoot string `json:"cache_root"`
CachedVersionsCount int `json:"cache_count"`
CachedVersionsSizeMB int64 `json:"cache_size_mb"`
TimeoutCatalog string `json:"timeout_catalog_ms"`
TimeoutCatalogMirror string `json:"timeout_catalog_mirror_ms"`
TimeoutReachability string `json:"timeout_reachability_ms"`
TimeoutDownload string `json:"timeout_download_ms"`
}

// nodeRuntimeFlags reports shim-enforced Node.js CLI security flags.
Expand Down Expand Up @@ -135,6 +137,7 @@ var (
)

func (e *Env) Run(ctx *kong.Context, vars kong.Vars) error {
settings.UseCommandDeadlines(e.RelaxDeadlines.Setting(), e.Deadlines.Setting())
var spinner *status.Spinner
if !e.JSON {
spinner = status.NewSpinner("Analyzing environment")
Expand Down Expand Up @@ -249,6 +252,7 @@ func (e *Env) Run(ctx *kong.Context, vars kong.Vars) error {
}
}

budgets := settings.ActiveNetworkBudgets()
node_ping_results := runReachabilityChecks(cfg.NodeMirror, isNodeMirrorReachable)
npm_ping_results := runReachabilityChecks(cfg.NpmMirror, isNpmMirrorReachable)

Expand Down Expand Up @@ -289,7 +293,7 @@ func (e *Env) Run(ctx *kong.Context, vars kong.Vars) error {
BuildTime: vars["buildTime"],
BuildArchitecture: buildArchitecture,
InstallDir: path(programRoot),
Upgrade: map[bool]string{true: "blocked", false: "allowed"}[cfg.DisableUpgrade],
Upgrade: map[bool]string{true: "blocked", false: "allowed"}[cfg.DisableUpgrade],
// Variables: map[string]string{
// "NVM_HOME": getUserEnvVar("NVM_HOME"),
// "NVM_NODE_PATH": getUserEnvVar("NVM_NODE_PATH"),
Expand All @@ -312,6 +316,10 @@ func (e *Env) Run(ctx *kong.Context, vars kong.Vars) error {
NpmGlobalModuleTotal: moduleTotalCount,
NpmGlobalModuleUnique: moduleUniqueCount,
NpmModuleSizeMB: moduleSizeBytes / (1024 * 1024),
TimeoutCatalog: budgets.Catalog.Note(),
TimeoutCatalogMirror: budgets.CatalogMirror.Note(),
TimeoutReachability: budgets.Reachability.Note(),
TimeoutDownload: budgets.Download.Note(),
},
Node: nodeFlags,
PackageManagers: packageManagersCfg{
Expand Down Expand Up @@ -453,10 +461,7 @@ func (e *Env) Run(ctx *kong.Context, vars kong.Vars) error {
// Node Mirrors
fmt.Fprintf(t, "%s%s Download Sources\t\n", indent(1), branch)
for i, mirror := range out.VersionManagement.NodeMirror {
reachable := ""
if !out.VersionManagement.NodeMirrorPingResult[mirror] {
reachable = " (unreachable)"
}
reachable := unreachableNote(out.VersionManagement.NodeMirrorPingResult[mirror], out.VersionManagement.TimeoutReachability)

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

// npm Mirrors
for i, mirror := range out.VersionManagement.NpmMirror {
reachable := ""
if !out.VersionManagement.NpmMirrorPingResult[mirror] {
reachable = " (unreachable)"
}
reachable := unreachableNote(out.VersionManagement.NpmMirrorPingResult[mirror], out.VersionManagement.TimeoutReachability)

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

fmt.Fprintf(t, "%s%s Network deadlines\t\n", indent(1), branch)
fmt.Fprintf(t, "%s%s%s %s TimeoutCatalogMs\t: %s\n", indent(1), line, indent(1), branch, out.VersionManagement.TimeoutCatalog)
fmt.Fprintf(t, "%s%s%s %s TimeoutCatalogMirrorMs\t: %s\n", indent(1), line, indent(1), branch, out.VersionManagement.TimeoutCatalogMirror)
fmt.Fprintf(t, "%s%s%s %s TimeoutReachabilityMs\t: %s\n", indent(1), line, indent(1), branch, out.VersionManagement.TimeoutReachability)
fmt.Fprintf(t, "%s%s%s %s TimeoutDownloadMs\t: %s\n", indent(1), line, indent(1), end, out.VersionManagement.TimeoutDownload)

fmt.Fprintf(t, "%s%s Installed Versions\t\n", indent(1), branch)

// Active Version
Expand Down Expand Up @@ -960,9 +968,33 @@ func runReachabilityChecks(mirrors []string, checker func(string) bool) map[stri
return results
}

func unreachableNote(reachable bool, budget string) string {
if reachable {
return ""
}
if strings.Contains(budget, settings.SourceCommandFlag) {
return " (unreachable; phase=reachability " + budget + ")"
}
return " (unreachable)"
}

func reachabilityClient() *nvmhttp.Client {
return nvmhttp.NewClient(settings.ActiveNetworkBudgets().Reachability.Duration())
}

func noteReachabilityDeadline(url string, err error) {
if !nvmhttp.IsDeadline(err) {
return
}
b := settings.ActiveNetworkBudgets().Reachability
log.LogNetworkDeadline("reachability", url, b.Milliseconds, b.Source)
}

func isNodeMirrorReachable(url string) bool {
res, err := reachabilityClient.Head(url + "/index.tab")
target := strings.TrimRight(url, "/") + "/index.tab"
res, err := reachabilityClient().Head(target)
if err != nil {
noteReachabilityDeadline(target, err)
return false
}
defer res.Body.Close()
Expand All @@ -975,8 +1007,10 @@ func isNodeMirrorReachable(url string) bool {
}

func isNpmMirrorReachable(url string) bool {
res, err := reachabilityClient.Get(url + "/-/ping")
target := strings.TrimRight(url, "/") + "/-/ping"
res, err := reachabilityClient().Get(target)
if err != nil {
noteReachabilityDeadline(target, err)
return false
}
defer res.Body.Close()
Expand Down
19 changes: 11 additions & 8 deletions src/commands/install/install.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,18 +10,21 @@ import (
)

type Install struct {
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"`
From string `flag:"from" help:"Install global modules from the list of modules installed in an another Node.js version." placeholder:"VERSION"`
Notify bool `flag:"notify" hidden:"true" help:"Notify user when a version has been automatically installed."`
Cache bool `flag:"cache" help:"Save the download (offline cache)."`
NoCache bool `flag:"no-cache" help:"Install without caching. This is the default unless configured to always cache downloads."`
Force bool `flag:"force" help:"Force (re)install if the version already exists."`
Debug bool `flag:"debug" hidden:"true" help:"Write timing logs to the install directory for debugging."`
Insecure bool `flag:"insecure" help:"Accept invalid TLS/SSL certs from download sources."`
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"`
From string `flag:"from" help:"Install global modules from the list of modules installed in an another Node.js version." placeholder:"VERSION"`
Notify bool `flag:"notify" hidden:"true" help:"Notify user when a version has been automatically installed."`
Cache bool `flag:"cache" help:"Save the download (offline cache)."`
NoCache bool `flag:"no-cache" help:"Install without caching. This is the default unless configured to always cache downloads."`
Force bool `flag:"force" help:"Force (re)install if the version already exists."`
Debug bool `flag:"debug" hidden:"true" help:"Write timing logs to the install directory for debugging."`
Insecure bool `flag:"insecure" help:"Accept invalid TLS/SSL certs from download sources."`
RelaxDeadlines constant.RelaxDeadlines `optional:"" placeholder:"MS" help:"Relax network deadlines for this command. Omit a value to triple the configured budgets, or pass milliseconds."`
Deadlines constant.DeadlineFlags `embed:""`
constant.ArgVersion
}

func (s *Install) Run(ctx *kong.Context) error {
settings.UseCommandDeadlines(s.RelaxDeadlines.Setting(), s.Deadlines.Setting())
cfg := settings.Global()

var cacheRoot string
Expand Down
32 changes: 27 additions & 5 deletions src/commands/sync.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,11 +49,16 @@ func (s *Upgrade) Run() error {
}

type Doctor struct {
Checks []string `arg:"" optional:"" help:"Specific checks to run. If not specified, all checks will be run."`
Autofix bool `flag:"autofix" help:"Automatically fix issues when possible."`
List bool `flag:"list" help:"List all available checks without running them."`
Update bool `flag:"update" help:"Force sync utility asset update before running checks."`
Force bool `flag:"force" help:"When upgrading, do not warn about processes that will be closed."`
Checks []string `arg:"" optional:"" help:"Specific checks to run. If not specified, all checks will be run."`
Autofix bool `flag:"autofix" help:"Automatically fix issues when possible."`
List bool `flag:"list" help:"List all available checks without running them."`
Update bool `flag:"update" help:"Force sync utility asset update before running checks."`
Force bool `flag:"force" help:"When upgrading, do not warn about processes that will be closed."`
RelaxDeadlines constant.RelaxDeadlines `optional:"" placeholder:"MS" help:"Relax network deadlines for this command. Omit a value to triple the configured budgets, or pass milliseconds."`
Deadlines constant.DeadlineFlags `embed:""`
MeasureDeadlines bool `flag:"measure-deadlines" help:"Time each download source and print min, average, and max."`
MeasureCount int `flag:"measure-count" default:"3" help:"How many times to repeat each deadline measurement. Ignored with --auto-deadlines."`
AutoDeadlines bool `flag:"auto-deadlines" help:"Run 59 attempts per download source and save the slowest sample as your deadlines."`
constant.FlagJSON
}

Expand All @@ -76,6 +81,23 @@ func (c *Doctor) Run() error {
if c.Force {
args = append(args, "--force")
}
if c.RelaxDeadlines.Active() {
if c.RelaxDeadlines.Milliseconds() > 0 {
args = append(args, fmt.Sprintf("--relax-deadlines=%d", c.RelaxDeadlines.Milliseconds()))
} else {
args = append(args, "--relax-deadlines")
}
}
args = append(args, c.Deadlines.Args()...)
if c.MeasureDeadlines {
args = append(args, "--measure-deadlines")
}
if c.AutoDeadlines {
args = append(args, "--auto-deadlines")
}
if (c.MeasureDeadlines || c.AutoDeadlines) && c.MeasureCount > 0 {
args = append(args, "--measure-count", fmt.Sprintf("%d", c.MeasureCount))
}
if c.JSON {
args = append(args, "--json")
}
Expand Down
68 changes: 68 additions & 0 deletions src/constant/deadline.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package constant

import (
"common/settings"
"fmt"
"strconv"
"strings"

"github.com/alecthomas/kong"
)

// DeadlineMs is one optional millisecond flag.
// Fields stay unexported so kong treats the value as one flag.
type DeadlineMs struct {
set bool
ms int
}

func (d DeadlineMs) Set() bool { return d.set }

func (d DeadlineMs) Milliseconds() int { return d.ms }

func (d *DeadlineMs) Decode(ctx *kong.DecodeContext) error {
raw, ok := tokenString(ctx.Scan.Peek().Value)
if !ok || raw == "" || strings.HasPrefix(raw, "-") {
return fmt.Errorf("must be a positive number of milliseconds")
}
n, err := strconv.Atoi(raw)
if err != nil || n < 1 {
return fmt.Errorf("must be a positive number of milliseconds")
}
ctx.Scan.Pop()
d.set = true
d.ms = n
return nil
}

// DeadlineFlags sets one network deadline for this command.
// Kong embed flattens these fields onto the command.
type DeadlineFlags struct {
TimeoutCatalogMs DeadlineMs `optional:"" placeholder:"MS" help:"Set the catalog deadline for this command, in milliseconds."`
TimeoutCatalogMirrorMs DeadlineMs `optional:"" placeholder:"MS" help:"Set the per-mirror catalog deadline for this command, in milliseconds."`
TimeoutReachabilityMs DeadlineMs `optional:"" placeholder:"MS" help:"Set the reachability deadline for this command, in milliseconds."`
TimeoutDownloadMs DeadlineMs `optional:"" placeholder:"MS" help:"Set the download deadline for this command, in milliseconds."`
}

func (f DeadlineFlags) Setting() settings.StepDeadlines {
return settings.StepDeadlines{
CatalogMs: f.TimeoutCatalogMs.Milliseconds(),
CatalogMirrorMs: f.TimeoutCatalogMirrorMs.Milliseconds(),
ReachabilityMs: f.TimeoutReachabilityMs.Milliseconds(),
DownloadMs: f.TimeoutDownloadMs.Milliseconds(),
}
}

func (f DeadlineFlags) Args() []string {
args := make([]string, 0, 4)
add := func(flag string, d DeadlineMs) {
if d.Set() {
args = append(args, fmt.Sprintf("--%s=%d", flag, d.Milliseconds()))
}
}
add("timeout-catalog-ms", f.TimeoutCatalogMs)
add("timeout-catalog-mirror-ms", f.TimeoutCatalogMirrorMs)
add("timeout-reachability-ms", f.TimeoutReachabilityMs)
add("timeout-download-ms", f.TimeoutDownloadMs)
return args
}
Loading