Initialize module and dependencies

This commit is contained in:
dwrz
2026-02-13 14:59:42 +00:00
commit 0740968bca
390 changed files with 131652 additions and 0 deletions

17
vendor/golang.org/x/telemetry/.dockerignore generated vendored Normal file
View File

@@ -0,0 +1,17 @@
.git
.localstorage
node_modules
devtools
.eslint*
.gitignore
.prettier*
.stylelint*
CONTRIBUTING.md
LICENSE
npm
npx
package-lock.json
package.json
PATENTS
README.md
tsconfig.json

11
vendor/golang.org/x/telemetry/.eslintrc.json generated vendored Normal file
View File

@@ -0,0 +1,11 @@
{
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/recommended",
"prettier"
],
"parser": "@typescript-eslint/parser",
"plugins": ["@typescript-eslint"],
"root": true,
"ignorePatterns": ["*.min.js"]
}

14
vendor/golang.org/x/telemetry/.gitattributes generated vendored Normal file
View File

@@ -0,0 +1,14 @@
# Treat all files in the repo as binary, with no git magic updating
# line endings. This produces predictable results in different environments.
#
# Windows users contributing to Go will need to use a modern version
# of git and editors capable of LF line endings.
#
# Windows .bat files are known to have multiple bugs when run with LF
# endings. So if they are checked in with CRLF endings, there should
# be a test like the one in test/winbatch.go in the go repository.
# (See golang.org/issue/37791.)
#
# See golang.org/issue/9281.
* -text

2
vendor/golang.org/x/telemetry/.gitignore generated vendored Normal file
View File

@@ -0,0 +1,2 @@
node_modules
.localstorage

1
vendor/golang.org/x/telemetry/.prettierrc.json generated vendored Normal file
View File

@@ -0,0 +1 @@
{"proseWrap": "always"}

11
vendor/golang.org/x/telemetry/.stylelintrc.json generated vendored Normal file
View File

@@ -0,0 +1,11 @@
{
"extends": ["stylelint-config-standard"],
"rules": {
"declaration-property-value-allowed-list": {
"/color/": ["/^var\\(--/", "transparent"]
},
"unit-disallowed-list": ["px"],
"selector-class-pattern": "^[a-zA-Z\\-]+$"
},
"ignoreFiles": ["**/*.min.css"]
}

30
vendor/golang.org/x/telemetry/CONTRIBUTING.md generated vendored Normal file
View File

@@ -0,0 +1,30 @@
# Contributing to Go
Go is an open source project.
It is the work of hundreds of contributors. We appreciate your help!
## Filing issues
When [filing an issue](https://golang.org/issue/new), make sure to answer these
five questions:
1. What version of Go are you using (`go version`)?
2. What operating system and processor architecture are you using?
3. What did you do?
4. What did you expect to see?
5. What did you see instead?
General questions should go to the
[golang-nuts mailing list](https://groups.google.com/group/golang-nuts) instead
of the issue tracker. The gophers there will answer or ask you to file an issue
if you've tripped over a bug.
## Contributing code
Please read the
[Contribution Guidelines](https://golang.org/doc/contribute.html) before sending
patches.
Unless otherwise noted, the Go source files are distributed under the BSD-style
license found in the LICENSE file.

27
vendor/golang.org/x/telemetry/LICENSE generated vendored Normal file
View File

@@ -0,0 +1,27 @@
Copyright 2009 The Go Authors.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following disclaimer
in the documentation and/or other materials provided with the
distribution.
* Neither the name of Google LLC nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

22
vendor/golang.org/x/telemetry/PATENTS generated vendored Normal file
View File

@@ -0,0 +1,22 @@
Additional IP Rights Grant (Patents)
"This implementation" means the copyrightable works distributed by
Google as part of the Go project.
Google hereby grants to You a perpetual, worldwide, non-exclusive,
no-charge, royalty-free, irrevocable (except as stated in this section)
patent license to make, have made, use, offer to sell, sell, import,
transfer and otherwise run, modify and propagate the contents of this
implementation of Go, where such license applies only to those patent
claims, both currently owned or controlled by Google and acquired in
the future, licensable by Google that are necessarily infringed by this
implementation of Go. This grant does not include claims that would be
infringed only as a consequence of further modification of this
implementation. If you or your agent or exclusive licensee institute or
order or agree to the institution of patent litigation against any
entity (including a cross-claim or counterclaim in a lawsuit) alleging
that this implementation of Go or any code incorporated within this
implementation of Go constitutes direct or contributory patent
infringement, or inducement of patent infringement, then any patent
rights granted to you under this License for this implementation of Go
shall terminate as of the date such litigation is filed.

62
vendor/golang.org/x/telemetry/README.md generated vendored Normal file
View File

@@ -0,0 +1,62 @@
# Go Telemetry
This repository holds the Go Telemetry server code and libraries, used for
hosting [telemetry.go.dev](https://telemetry.go.dev) and instrumenting Go
toolchain programs with opt-in telemetry.
**Warning**: this repository is intended for use only in tools maintained by
the Go team, including tools in the Go distribution and auxiliary tools like
[gopls](https://pkg.go.dev/golang.org/x/tools/gopls) or
[govulncheck](https://pkg.go.dev/golang.org/x/vuln/cmd/govulncheck). There are
no compatibility guarantees for any of the packages here: public APIs will
change in breaking ways as the telemetry integration is refined.
## Notable Packages
- The [x/telemetry/counter](https://pkg.go.dev/golang.org/x/telemetry/counter)
package provides a library for instrumenting programs with counters and stack
reports.
- The [x/telemetry/upload](https://pkg.go.dev/golang.org/x/telemetry/upload)
package provides a hook for Go toolchain programs to upload telemetry data,
if the user has opted in to telemetry uploading.
- The [x/telemetry/cmd/gotelemetry](https://pkg.go.dev/pkg/golang.org/x/telemetry/cmd/gotelemetry)
command is used for managing telemetry data and configuration.
- The [x/telemetry/config](https://pkg.go.dev/pkg/golang.org/x/telemetry/config)
package defines the subset of telemetry data that has been approved for
uploading by the telemetry proposal process.
- The [x/telemetry/godev](https://pkg.go.dev/pkg/golang.org/x/telemetry/godev) directory defines
the services running at [telemetry.go.dev](https://telemetry.go.dev).
## Contributing
This repository uses Gerrit for code changes. To learn how to submit changes to
this repository, see https://go.dev/doc/contribute.
The git repository is https://go.googlesource.com/telemetry.
The main issue tracker for the telemetry repository is located at
https://go.dev/issues. Prefix your issue with "x/telemetry:" in
the subject line, so it is easy to find.
### Linting & Formatting
This repository uses [eslint](https://eslint.org/) to format TS files,
[stylelint](https://stylelint.io/) to format CSS files, and
[prettier](https://prettier.io/) to format TS, CSS, Markdown, and YAML files.
See the style guides:
- [TypeScript](https://google.github.io/styleguide/tsguide.html)
- [CSS](https://go.dev/wiki/CSSStyleGuide)
It is encouraged that all TS and CSS code be run through formatters before
submitting a change. However, it is not a strict requirement enforced by CI.
### Installing npm Dependencies:
1. Install [docker](https://docs.docker.com/get-docker/)
2. Run `./npm install`
### Run ESLint, Stylelint, & Prettier
./npm run all

1
vendor/golang.org/x/telemetry/codereview.cfg generated vendored Normal file
View File

@@ -0,0 +1 @@
issuerepo: golang/go

146
vendor/golang.org/x/telemetry/counter/counter.go generated vendored Normal file
View File

@@ -0,0 +1,146 @@
// Copyright 2023 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package counter
// The implementation of this package and tests are located in
// internal/counter, which can be shared with the upload package.
// TODO(hyangah): use of type aliases prevents nice documentation
// rendering in go doc or pkgsite. Fix this either by avoiding
// type aliasing or restructuring the internal/counter package.
import (
"flag"
"path"
"runtime/debug"
"golang.org/x/telemetry/internal/counter"
"golang.org/x/telemetry/internal/telemetry"
)
// Inc increments the counter with the given name.
func Inc(name string) {
New(name).Inc()
}
// Add adds n to the counter with the given name.
func Add(name string, n int64) {
New(name).Add(n)
}
// New returns a counter with the given name.
// New can be called in global initializers and will be compiled down to
// linker-initialized data. That is, calling New to initialize a global
// has no cost at program startup.
//
// See "Counter Naming" in the package doc for a description of counter naming
// conventions.
func New(name string) *Counter {
// Note: not calling DefaultFile.New in order to keep this
// function something the compiler can inline and convert
// into static data initializations, with no init-time footprint.
// TODO(hyangah): is it trivial enough for the compiler to inline?
return counter.New(name)
}
// A Counter is a single named event counter.
// A Counter is safe for use by multiple goroutines simultaneously.
//
// Counters should typically be created using New
// and stored as global variables, like:
//
// package mypackage
// var errorCount = counter.New("mypackage/errors")
//
// (The initialization of errorCount in this example is handled
// entirely by the compiler and linker; this line executes no code
// at program startup.)
//
// Then code can call Add to increment the counter
// each time the corresponding event is observed.
//
// Although it is possible to use New to create
// a Counter each time a particular event needs to be recorded,
// that usage fails to amortize the construction cost over
// multiple calls to Add, so it is more expensive and not recommended.
type Counter = counter.Counter
// A StackCounter is the in-memory knowledge about a stack counter.
// StackCounters are more expensive to use than regular Counters,
// requiring, at a minimum, a call to runtime.Callers.
type StackCounter = counter.StackCounter
// NewStack returns a new stack counter with the given name and depth.
//
// See "Counter Naming" in the package doc for a description of counter naming
// conventions.
func NewStack(name string, depth int) *StackCounter {
return counter.NewStack(name, depth)
}
// Open prepares telemetry counters for recording to the file system.
//
// If the telemetry mode is "off", Open is a no-op. Otherwise, it opens the
// counter file on disk and starts to mmap telemetry counters to the file.
// Open also persists any counters already created in the current process.
//
// Open should only be called from short-lived processes such as command line
// tools. If your process is long-running, use [OpenAndRotate].
func Open() {
counter.Open(false)
}
// OpenAndRotate is like [Open], but also schedules a rotation of the counter
// file when it expires.
//
// See golang/go#68497 for background on why [OpenAndRotate] is a separate API.
//
// TODO(rfindley): refactor Open and OpenAndRotate for Go 1.24.
func OpenAndRotate() {
counter.Open(true)
}
// OpenDir prepares telemetry counters for recording to the file system, using
// the specified telemetry directory, if it is not the empty string.
//
// If the telemetry mode is "off", Open is a no-op. Otherwise, it opens the
// counter file on disk and starts to mmap telemetry counters to the file.
// Open also persists any counters already created in the current process.
func OpenDir(telemetryDir string) {
if telemetryDir != "" {
telemetry.Default = telemetry.NewDir(telemetryDir)
}
counter.Open(false)
}
// CountFlags creates a counter for every flag that is set
// and increments the counter. The name of the counter is
// the concatenation of prefix and the flag name.
//
// For instance, CountFlags("gopls/flag:", *flag.CommandLine)
func CountFlags(prefix string, fs flag.FlagSet) {
fs.Visit(func(f *flag.Flag) {
New(prefix + f.Name).Inc()
})
}
// CountCommandLineFlags creates a counter for every flag
// that is set in the default flag.CommandLine FlagSet using
// the counter name binaryName+"/flag:"+flagName where
// binaryName is the base name of the Path embedded in the
// binary's build info. If the binary does not have embedded build
// info, the "flag:"+flagName counter will be incremented.
//
// CountCommandLineFlags must be called after flags are parsed
// with flag.Parse.
//
// For instance, if the -S flag is passed to cmd/compile and
// CountCommandLineFlags is called after flags are parsed,
// the "compile/flag:S" counter will be incremented.
func CountCommandLineFlags() {
prefix := "flag:"
if buildInfo, ok := debug.ReadBuildInfo(); ok && buildInfo.Path != "" {
prefix = path.Base(buildInfo.Path) + "/" + prefix
}
CountFlags(prefix, *flag.CommandLine)
}

58
vendor/golang.org/x/telemetry/counter/doc.go generated vendored Normal file
View File

@@ -0,0 +1,58 @@
// Copyright 2023 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Package counter implements a simple counter system for collecting
// totally public telemetry data.
//
// There are two kinds of counters, basic counters and stack counters.
// Basic counters are created by [New].
// Stack counters are created by [NewStack].
// Both are incremented by calling Inc().
//
// Basic counters are very cheap. Stack counters are more expensive, as they
// require parsing the stack. (Stack counters are implemented as basic counters
// whose names are the concatenation of the name and the stack trace. There is
// an upper limit on the size of this name, about 4K bytes. If the name is too
// long the stack will be truncated and "truncated" appended.)
//
// When counter files expire they are turned into reports by the upload
// package. The first time any counter file is created for a user, a random day
// of the week is selected on which counter files will expire. For the first
// week, that day is more than 7 days (but not more than two weeks) in the
// future. After that the counter files expire weekly on the same day of the
// week.
//
// # Counter Naming
//
// Counter names passed to [New] and [NewStack] should follow these
// conventions:
//
// - Names cannot contain whitespace or newlines.
//
// - Names must be valid unicode, with no unprintable characters.
//
// - Names may contain at most one ':'. In the counter "foo:bar", we refer to
// "foo" as the "chart name" and "bar" as the "bucket name".
//
// - The '/' character should partition counter names into a hierarchy. The
// root of this hierarchy should identify the logical entity that "owns"
// the counter. This could be an application, such as "gopls" in the case
// of "gopls/client:vscode", or a shared library, such as "crash" in the
// case of the "crash/crash" counter owned by the crashmonitor library. If
// the entity name itself contains a '/', that's ok: "cmd/go/flag" is fine.
//
// - Words should be '-' separated, as in "gopls/completion/errors-latency"
//
// - Histograms should use bucket names identifying upper bounds with '<'.
// For example given two counters "gopls/completion/latency:<50ms" and
// "gopls/completion/latency:<100ms", the "<100ms" bucket counts events
// with latency in the half-open interval [50ms, 100ms).
//
// # Debugging
//
// The GODEBUG environment variable can enable printing of additional debug
// information for counters. Adding GODEBUG=countertrace=1 to the environment
// of a process using counters causes the x/telemetry/counter package to log
// counter information to stderr.
package counter

12
vendor/golang.org/x/telemetry/dir.go generated vendored Normal file
View File

@@ -0,0 +1,12 @@
// Copyright 2024 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package telemetry
import "golang.org/x/telemetry/internal/telemetry"
// Dir returns the telemetry directory.
func Dir() string {
return telemetry.Default.Dir()
}

1
vendor/golang.org/x/telemetry/doc.go generated vendored Normal file
View File

@@ -0,0 +1 @@
package telemetry

140
vendor/golang.org/x/telemetry/internal/config/config.go generated vendored Normal file
View File

@@ -0,0 +1,140 @@
// Copyright 2023 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// package config provides methods for loading and querying a
// telemetry upload config file.
package config
import (
"encoding/json"
"os"
"strings"
"golang.org/x/telemetry/internal/telemetry"
)
// Config is a wrapper around telemetry.UploadConfig that provides some
// convenience methods for checking the contents of a report.
type Config struct {
*telemetry.UploadConfig
program map[string]bool
goos map[string]bool
goarch map[string]bool
goversion map[string]bool
pgversion map[pgkey]bool
pgcounter map[pgkey]bool
pgcounterprefix map[pgkey]bool
pgstack map[pgkey]bool
rate map[pgkey]float64
}
type pgkey struct {
program, key string
}
func ReadConfig(file string) (*Config, error) {
data, err := os.ReadFile(file)
if err != nil {
return nil, err
}
var cfg telemetry.UploadConfig
if err := json.Unmarshal(data, &cfg); err != nil {
return nil, err
}
return NewConfig(&cfg), nil
}
func NewConfig(cfg *telemetry.UploadConfig) *Config {
ucfg := Config{UploadConfig: cfg}
ucfg.goos = set(ucfg.GOOS)
ucfg.goarch = set(ucfg.GOARCH)
ucfg.goversion = set(ucfg.GoVersion)
ucfg.program = make(map[string]bool, len(ucfg.Programs))
ucfg.pgversion = make(map[pgkey]bool, len(ucfg.Programs))
ucfg.pgcounter = make(map[pgkey]bool, len(ucfg.Programs))
ucfg.pgcounterprefix = make(map[pgkey]bool, len(ucfg.Programs))
ucfg.pgstack = make(map[pgkey]bool, len(ucfg.Programs))
ucfg.rate = make(map[pgkey]float64)
for _, p := range ucfg.Programs {
ucfg.program[p.Name] = true
for _, v := range p.Versions {
ucfg.pgversion[pgkey{p.Name, v}] = true
}
for _, c := range p.Counters {
for _, e := range Expand(c.Name) {
ucfg.pgcounter[pgkey{p.Name, e}] = true
ucfg.rate[pgkey{p.Name, e}] = c.Rate
}
prefix, _, found := strings.Cut(c.Name, ":")
if found {
ucfg.pgcounterprefix[pgkey{p.Name, prefix}] = true
}
}
for _, s := range p.Stacks {
ucfg.pgstack[pgkey{p.Name, s.Name}] = true
ucfg.rate[pgkey{p.Name, s.Name}] = s.Rate
}
}
return &ucfg
}
func (r *Config) HasProgram(s string) bool {
return r.program[s]
}
func (r *Config) HasGOOS(s string) bool {
return r.goos[s]
}
func (r *Config) HasGOARCH(s string) bool {
return r.goarch[s]
}
func (r *Config) HasGoVersion(s string) bool {
return r.goversion[s]
}
func (r *Config) HasVersion(program, version string) bool {
return r.pgversion[pgkey{program, version}]
}
func (r *Config) HasCounter(program, counter string) bool {
return r.pgcounter[pgkey{program, counter}]
}
func (r *Config) HasCounterPrefix(program, prefix string) bool {
return r.pgcounterprefix[pgkey{program, prefix}]
}
func (r *Config) HasStack(program, stack string) bool {
return r.pgstack[pgkey{program, stack}]
}
func (r *Config) Rate(program, name string) float64 {
return r.rate[pgkey{program, name}]
}
func set(slice []string) map[string]bool {
s := make(map[string]bool, len(slice))
for _, v := range slice {
s[v] = true
}
return s
}
// Expand takes a counter defined with buckets and expands it into distinct
// strings for each bucket.
func Expand(counter string) []string {
prefix, rest, hasBuckets := strings.Cut(counter, "{")
var counters []string
if hasBuckets {
buckets := strings.Split(strings.TrimSuffix(rest, "}"), ",")
for _, b := range buckets {
counters = append(counters, prefix+b)
}
} else {
counters = append(counters, prefix)
}
return counters
}

View File

@@ -0,0 +1,86 @@
// Copyright 2023 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Package configstore abstracts interaction with the telemetry config server.
// Telemetry config (golang.org/x/telemetry/config) is distributed as a go
// module containing go.mod and config.json. Programs that upload collected
// counters download the latest config using `go mod download`. This provides
// verification of downloaded configuration and cacheability.
package configstore
import (
"bytes"
"encoding/json"
"fmt"
"os"
"os/exec"
"path/filepath"
"sync/atomic"
"golang.org/x/telemetry/internal/telemetry"
)
const (
ModulePath = "golang.org/x/telemetry/config"
configFileName = "config.json"
)
// needNoConsole is used on windows to set the windows.CREATE_NO_WINDOW
// creation flag.
var needNoConsole = func(cmd *exec.Cmd) {}
var downloads int64
// Downloads reports, for testing purposes, the number of times [Download] has
// been called.
func Downloads() int64 {
return atomic.LoadInt64(&downloads)
}
// Download fetches the requested telemetry UploadConfig using "go mod
// download". If envOverlay is provided, it is appended to the environment used
// for invoking the go command.
//
// The second result is the canonical version of the requested configuration.
func Download(version string, envOverlay []string) (*telemetry.UploadConfig, string, error) {
atomic.AddInt64(&downloads, 1)
if version == "" {
version = "latest"
}
modVer := ModulePath + "@" + version
var stdout, stderr bytes.Buffer
cmd := exec.Command("go", "mod", "download", "-json", modVer)
needNoConsole(cmd)
cmd.Env = append(os.Environ(), envOverlay...)
cmd.Stdout = &stdout
cmd.Stderr = &stderr
if err := cmd.Run(); err != nil {
var info struct {
Error string
}
if err := json.Unmarshal(stdout.Bytes(), &info); err == nil && info.Error != "" {
return nil, "", fmt.Errorf("failed to download config module: %v", info.Error)
}
return nil, "", fmt.Errorf("failed to download config module: %w\n%s", err, &stderr)
}
var info struct {
Dir string
Version string
Error string
}
if err := json.Unmarshal(stdout.Bytes(), &info); err != nil || info.Dir == "" {
return nil, "", fmt.Errorf("failed to download config module (invalid JSON): %w", err)
}
data, err := os.ReadFile(filepath.Join(info.Dir, configFileName))
if err != nil {
return nil, "", fmt.Errorf("invalid config module: %w", err)
}
cfg := new(telemetry.UploadConfig)
if err := json.Unmarshal(data, cfg); err != nil {
return nil, "", fmt.Errorf("invalid config: %w", err)
}
return cfg, info.Version, nil
}

View File

@@ -0,0 +1,33 @@
// Copyright 2024 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:build windows
package configstore
import (
"os/exec"
"syscall"
"golang.org/x/sys/windows"
)
func init() {
needNoConsole = needNoConsoleWindows
}
func needNoConsoleWindows(cmd *exec.Cmd) {
// The uploader main process is likely a daemonized process with no console.
// (see x/telemetry/start_windows.go) The console creation behavior when
// a parent is a console process without console is not clearly documented
// but empirically we observed the new console is created and attached to the
// subprocess in the default setup.
//
// Ensure no new console is attached to the subprocess by setting CREATE_NO_WINDOW.
// https://learn.microsoft.com/en-us/windows/console/creation-of-a-console
// https://learn.microsoft.com/en-us/windows/win32/procthread/process-creation-flags
cmd.SysProcAttr = &syscall.SysProcAttr{
CreationFlags: windows.CREATE_NO_WINDOW,
}
}

View File

@@ -0,0 +1,401 @@
// Copyright 2023 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Package internal/counter implements the internals of the public counter package.
// In addition to the public API, this package also includes APIs to parse and
// manage the counter files, needed by the upload package.
package counter
import (
"fmt"
"os"
"runtime"
"strings"
"sync/atomic"
)
var (
// Note: not using internal/godebug, so that internal/godebug can use
// internal/counter.
debugCounter = strings.Contains(os.Getenv("GODEBUG"), "countertrace=1")
CrashOnBugs = false // for testing; if set, exit on fatal log messages
)
// debugPrintf formats a debug message if GODEBUG=countertrace=1.
func debugPrintf(format string, args ...any) {
if debugCounter {
if len(format) == 0 || format[len(format)-1] != '\n' {
format += "\n"
}
fmt.Fprintf(os.Stderr, "counter: "+format, args...)
}
}
// debugFatalf logs a fatal error if GODEBUG=countertrace=1.
func debugFatalf(format string, args ...any) {
if debugCounter || CrashOnBugs {
if len(format) == 0 || format[len(format)-1] != '\n' {
format += "\n"
}
fmt.Fprintf(os.Stderr, "counter bug: "+format, args...)
os.Exit(1)
}
}
// A Counter is a single named event counter.
// A Counter is safe for use by multiple goroutines simultaneously.
//
// Counters should typically be created using New
// and stored as global variables, like:
//
// package mypackage
// var errorCount = counter.New("mypackage/errors")
//
// (The initialization of errorCount in this example is handled
// entirely by the compiler and linker; this line executes no code
// at program startup.)
//
// Then code can call Add to increment the counter
// each time the corresponding event is observed.
//
// Although it is possible to use New to create
// a Counter each time a particular event needs to be recorded,
// that usage fails to amortize the construction cost over
// multiple calls to Add, so it is more expensive and not recommended.
type Counter struct {
name string
file *file
next atomic.Pointer[Counter]
state counterState
ptr counterPtr
}
func (c *Counter) Name() string {
return c.name
}
type counterPtr struct {
m *mappedFile
count *atomic.Uint64
}
type counterState struct {
bits atomic.Uint64
}
func (s *counterState) load() counterStateBits {
return counterStateBits(s.bits.Load())
}
func (s *counterState) update(old *counterStateBits, new counterStateBits) bool {
if s.bits.CompareAndSwap(uint64(*old), uint64(new)) {
*old = new
return true
}
return false
}
type counterStateBits uint64
const (
stateReaders counterStateBits = 1<<30 - 1
stateLocked counterStateBits = stateReaders
stateHavePtr counterStateBits = 1 << 30
stateExtraShift = 31
stateExtra counterStateBits = 1<<64 - 1<<stateExtraShift
)
func (b counterStateBits) readers() int { return int(b & stateReaders) }
func (b counterStateBits) locked() bool { return b&stateReaders == stateLocked }
func (b counterStateBits) havePtr() bool { return b&stateHavePtr != 0 }
func (b counterStateBits) extra() uint64 { return uint64(b&stateExtra) >> stateExtraShift }
func (b counterStateBits) incReader() counterStateBits { return b + 1 }
func (b counterStateBits) decReader() counterStateBits { return b - 1 }
func (b counterStateBits) setLocked() counterStateBits { return b | stateLocked }
func (b counterStateBits) clearLocked() counterStateBits { return b &^ stateLocked }
func (b counterStateBits) setHavePtr() counterStateBits { return b | stateHavePtr }
func (b counterStateBits) clearHavePtr() counterStateBits { return b &^ stateHavePtr }
func (b counterStateBits) clearExtra() counterStateBits { return b &^ stateExtra }
func (b counterStateBits) addExtra(n uint64) counterStateBits {
const maxExtra = uint64(stateExtra) >> stateExtraShift // 0x1ffffffff
x := b.extra()
if x+n < x || x+n > maxExtra {
x = maxExtra
} else {
x += n
}
return b.clearExtra() | counterStateBits(x)<<stateExtraShift
}
// New returns a counter with the given name.
// New can be called in global initializers and will be compiled down to
// linker-initialized data. That is, calling New to initialize a global
// has no cost at program startup.
func New(name string) *Counter {
// Note: not calling defaultFile.New in order to keep this
// function something the compiler can inline and convert
// into static data initializations, with no init-time footprint.
return &Counter{name: name, file: &defaultFile}
}
// Inc adds 1 to the counter.
func (c *Counter) Inc() {
c.Add(1)
}
// Add adds n to the counter. n cannot be negative, as counts cannot decrease.
func (c *Counter) Add(n int64) {
debugPrintf("Add %q += %d", c.name, n)
if n < 0 {
panic("Counter.Add negative")
}
if n == 0 {
return
}
c.file.register(c)
state := c.state.load()
for ; ; state = c.state.load() {
switch {
case !state.locked() && state.havePtr():
if !c.state.update(&state, state.incReader()) {
continue
}
// Counter unlocked or counter shared; has an initialized count pointer; acquired shared lock.
if c.ptr.count == nil {
for !c.state.update(&state, state.addExtra(uint64(n))) {
// keep trying - we already took the reader lock
state = c.state.load()
}
debugPrintf("Add %q += %d: nil extra=%d\n", c.name, n, state.extra())
} else {
sum := c.add(uint64(n))
debugPrintf("Add %q += %d: count=%d\n", c.name, n, sum)
}
c.releaseReader(state)
return
case state.locked():
if !c.state.update(&state, state.addExtra(uint64(n))) {
continue
}
debugPrintf("Add %q += %d: locked extra=%d\n", c.name, n, state.extra())
return
case !state.havePtr():
if !c.state.update(&state, state.addExtra(uint64(n)).setLocked()) {
continue
}
debugPrintf("Add %q += %d: noptr extra=%d\n", c.name, n, state.extra())
c.releaseLock(state)
return
}
}
}
func (c *Counter) releaseReader(state counterStateBits) {
for ; ; state = c.state.load() {
// If we are the last reader and havePtr was cleared
// while this batch of readers was using c.ptr,
// it's our job to update c.ptr by upgrading to a full lock
// and letting releaseLock do the work.
// Note: no new reader will attempt to add itself now that havePtr is clear,
// so we are only racing against possible additions to extra.
if state.readers() == 1 && !state.havePtr() {
if !c.state.update(&state, state.setLocked()) {
continue
}
debugPrintf("releaseReader %s: last reader, need ptr\n", c.name)
c.releaseLock(state)
return
}
// Release reader.
if !c.state.update(&state, state.decReader()) {
continue
}
debugPrintf("releaseReader %s: released (%d readers now)\n", c.name, state.readers())
return
}
}
func (c *Counter) releaseLock(state counterStateBits) {
for ; ; state = c.state.load() {
if !state.havePtr() {
// Set havePtr before updating ptr,
// to avoid race with the next clear of havePtr.
if !c.state.update(&state, state.setHavePtr()) {
continue
}
debugPrintf("releaseLock %s: reset havePtr (extra=%d)\n", c.name, state.extra())
// Optimization: only bother loading a new pointer
// if we have a value to add to it.
c.ptr = counterPtr{nil, nil}
if state.extra() != 0 {
c.ptr = c.file.lookup(c.name)
debugPrintf("releaseLock %s: ptr=%v\n", c.name, c.ptr)
}
}
if extra := state.extra(); extra != 0 && c.ptr.count != nil {
if !c.state.update(&state, state.clearExtra()) {
continue
}
sum := c.add(extra)
debugPrintf("releaseLock %s: flush extra=%d -> count=%d\n", c.name, extra, sum)
}
// Took care of refreshing ptr and flushing extra.
// Now we can release the lock, unless of course
// another goroutine cleared havePtr or added to extra,
// in which case we go around again.
if !c.state.update(&state, state.clearLocked()) {
continue
}
debugPrintf("releaseLock %s: unlocked\n", c.name)
return
}
}
// add wraps the atomic.Uint64.Add operation to handle integer overflow.
func (c *Counter) add(n uint64) uint64 {
count := c.ptr.count
for {
old := count.Load()
sum := old + n
if sum < old {
sum = ^uint64(0)
}
if count.CompareAndSwap(old, sum) {
runtime.KeepAlive(c.ptr.m)
return sum
}
}
}
func (c *Counter) invalidate() {
for {
state := c.state.load()
if !state.havePtr() {
debugPrintf("invalidate %s: no ptr\n", c.name)
return
}
if c.state.update(&state, state.clearHavePtr()) {
debugPrintf("invalidate %s: cleared havePtr\n", c.name)
return
}
}
}
func (c *Counter) refresh() {
for {
state := c.state.load()
if state.havePtr() || state.readers() > 0 || state.extra() == 0 {
debugPrintf("refresh %s: havePtr=%v readers=%d extra=%d\n", c.name, state.havePtr(), state.readers(), state.extra())
return
}
if c.state.update(&state, state.setLocked()) {
debugPrintf("refresh %s: locked havePtr=%v readers=%d extra=%d\n", c.name, state.havePtr(), state.readers(), state.extra())
c.releaseLock(state)
return
}
}
}
// Read reads the given counter.
// This is the implementation of x/telemetry/counter/countertest.ReadCounter.
func Read(c *Counter) (uint64, error) {
if c.file.current.Load() == nil {
return c.state.load().extra(), nil
}
pf, err := readFile(c.file)
if err != nil {
return 0, err
}
v, ok := pf.Count[DecodeStack(c.Name())]
if !ok {
return v, fmt.Errorf("not found:%q", DecodeStack(c.Name()))
}
return v, nil
}
func readFile(f *file) (*File, error) {
if f == nil {
debugPrintf("No file")
return nil, fmt.Errorf("counter is not initialized - was Open called?")
}
// Note: don't call f.rotate here as this will enqueue a follow-up rotation.
f.rotate1()
if f.err != nil {
return nil, fmt.Errorf("failed to rotate mapped file - %v", f.err)
}
current := f.current.Load()
if current == nil {
return nil, fmt.Errorf("counter has no mapped file")
}
name := current.f.Name()
data, err := ReadMapped(name)
if err != nil {
return nil, fmt.Errorf("failed to read from file: %v", err)
}
pf, err := Parse(name, data)
if err != nil {
return nil, fmt.Errorf("failed to parse: %v", err)
}
return pf, nil
}
// ReadFile reads the counters and stack counters from the given file.
// This is the implementation of x/telemetry/counter/countertest.ReadFile.
func ReadFile(name string) (counters, stackCounters map[string]uint64, _ error) {
// TODO: Document the format of the stackCounters names.
data, err := ReadMapped(name)
if err != nil {
return nil, nil, fmt.Errorf("failed to read from file: %v", err)
}
pf, err := Parse(name, data)
if err != nil {
return nil, nil, fmt.Errorf("failed to parse: %v", err)
}
counters = make(map[string]uint64)
stackCounters = make(map[string]uint64)
for k, v := range pf.Count {
if IsStackCounter(k) {
stackCounters[DecodeStack(k)] = v
} else {
counters[k] = v
}
}
return counters, stackCounters, nil
}
// ReadMapped reads the contents of the given file by memory mapping.
//
// This avoids file synchronization issues.
func ReadMapped(name string) ([]byte, error) {
f, err := os.OpenFile(name, os.O_RDWR, 0666)
if err != nil {
return nil, err
}
defer f.Close()
fi, err := f.Stat()
if err != nil {
return nil, err
}
mapping, err := memmap(f)
if err != nil {
return nil, err
}
data := make([]byte, fi.Size())
copy(data, mapping.Data)
munmap(mapping)
return data, nil
}

814
vendor/golang.org/x/telemetry/internal/counter/file.go generated vendored Normal file
View File

@@ -0,0 +1,814 @@
// Copyright 2023 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package counter
import (
"bytes"
"errors"
"fmt"
"math/rand"
"os"
"path"
"path/filepath"
"runtime"
"runtime/debug"
"sync"
"sync/atomic"
"time"
"unsafe"
"golang.org/x/telemetry/internal/mmap"
"golang.org/x/telemetry/internal/telemetry"
)
// A file is a counter file.
type file struct {
// Linked list of all known counters.
// (Linked list insertion is easy to make lock-free,
// and we don't want the initial counters incremented
// by a program to cause significant contention.)
counters atomic.Pointer[Counter] // head of list
end Counter // list ends at &end instead of nil
mu sync.Mutex
buildInfo *debug.BuildInfo
timeBegin, timeEnd time.Time
err error
// current holds the current file mapping, which may change when the file is
// rotated or extended.
//
// current may be read without holding mu, but may be nil.
//
// The cleanup logic for file mappings is complicated, because invalidating
// counter pointers is reentrant: [file.invalidateCounters] may call
// [file.lookup], which acquires mu. Therefore, writing current must be done
// as follows:
// 1. record the previous value of current
// 2. Store a new value in current
// 3. unlock mu
// 4. call invalidateCounters
// 5. close the previous mapped value from (1)
// TODO(rfindley): simplify
current atomic.Pointer[mappedFile]
}
var defaultFile file
// register ensures that the counter c is registered with the file.
func (f *file) register(c *Counter) {
debugPrintf("register %s %p\n", c.Name(), c)
// If counter is not registered with file, register it.
// Doing this lazily avoids init-time work
// as well as any execution cost at all for counters
// that are not used in a given program.
wroteNext := false
for wroteNext || c.next.Load() == nil {
head := f.counters.Load()
next := head
if next == nil {
next = &f.end
}
debugPrintf("register %s next %p\n", c.Name(), next)
if !wroteNext {
if !c.next.CompareAndSwap(nil, next) {
debugPrintf("register %s cas failed %p\n", c.Name(), c.next.Load())
continue
}
wroteNext = true
} else {
c.next.Store(next)
}
if f.counters.CompareAndSwap(head, c) {
debugPrintf("registered %s %p\n", c.Name(), f.counters.Load())
return
}
debugPrintf("register %s cas2 failed %p %p\n", c.Name(), f.counters.Load(), head)
}
}
// invalidateCounters marks as invalid all the pointers
// held by f's counters and then refreshes them.
//
// invalidateCounters cannot be called while holding f.mu,
// because a counter refresh may call f.lookup.
func (f *file) invalidateCounters() {
// Mark every counter as needing to refresh its count pointer.
if head := f.counters.Load(); head != nil {
for c := head; c != &f.end; c = c.next.Load() {
c.invalidate()
}
for c := head; c != &f.end; c = c.next.Load() {
c.refresh()
}
}
}
// lookup looks up the counter with the given name in the file,
// allocating it if needed, and returns a pointer to the atomic.Uint64
// containing the counter data.
// If the file has not been opened yet, lookup returns nil.
func (f *file) lookup(name string) counterPtr {
current := f.current.Load()
if current == nil {
debugPrintf("lookup %s - no mapped file\n", name)
return counterPtr{}
}
ptr := f.newCounter(name)
if ptr == nil {
return counterPtr{}
}
return counterPtr{current, ptr}
}
// ErrDisabled is the error returned when telemetry is disabled.
var ErrDisabled = errors.New("counter: disabled as Go telemetry is off")
var (
errNoBuildInfo = errors.New("counter: missing build info")
errCorrupt = errors.New("counter: corrupt counter file")
)
// weekEnd returns the day of the week on which uploads occur (and therefore
// counters expire).
//
// Reads the weekends file, creating one if none exists.
func weekEnd() (time.Weekday, error) {
// If there is no 'weekends' file create it and initialize it
// to a random day of the week. There is a short interval for
// a race.
weekends := filepath.Join(telemetry.Default.LocalDir(), "weekends")
day := fmt.Sprintf("%d\n", rand.Intn(7))
if _, err := os.ReadFile(weekends); err != nil {
if err := os.MkdirAll(telemetry.Default.LocalDir(), 0777); err != nil {
debugPrintf("%v: could not create telemetry.LocalDir %s", err, telemetry.Default.LocalDir())
return 0, err
}
if err = os.WriteFile(weekends, []byte(day), 0666); err != nil {
return 0, err
}
}
// race is over, read the file
buf, err := os.ReadFile(weekends)
// There is no reasonable way of recovering from errors
// so we just fail
if err != nil {
return 0, err
}
buf = bytes.TrimSpace(buf)
if len(buf) == 0 {
return 0, fmt.Errorf("empty weekends file")
}
weekend := time.Weekday(buf[0] - '0') // 0 is Sunday
// paranoia to make sure the value is legal
weekend %= 7
if weekend < 0 {
weekend += 7
}
return weekend, nil
}
// rotate checks to see whether the file f needs to be rotated,
// meaning to start a new counter file with a different date in the name.
// rotate is also used to open the file initially, meaning f.current can be nil.
// In general rotate should be called just once for each file.
// rotate will arrange a timer to call itself again when necessary.
func (f *file) rotate() {
expiry := f.rotate1()
if !expiry.IsZero() {
delay := time.Until(expiry)
// Some tests set CounterTime to a time in the past, causing delay to be
// negative. Avoid infinite loops by delaying at least a short interval.
//
// TODO(rfindley): instead, just also mock AfterFunc.
const minDelay = 1 * time.Minute
if delay < minDelay {
delay = minDelay
}
// TODO(rsc): Does this do the right thing for laptops closing?
time.AfterFunc(delay, f.rotate)
}
}
func nop() {}
// CounterTime returns the current UTC time.
// Mutable for testing.
var CounterTime = func() time.Time {
return time.Now().UTC()
}
// counterSpan returns the current time span for a counter file, as determined
// by [CounterTime] and the [weekEnd].
func counterSpan() (begin, end time.Time, _ error) {
year, month, day := CounterTime().Date()
begin = time.Date(year, month, day, 0, 0, 0, 0, time.UTC)
// files always begin today, but expire on the next day of the week
// from the 'weekends' file.
weekend, err := weekEnd()
if err != nil {
return time.Time{}, time.Time{}, err
}
incr := int(weekend - begin.Weekday())
if incr <= 0 {
incr += 7 // ensure that end is later than begin
}
end = time.Date(year, month, day+incr, 0, 0, 0, 0, time.UTC)
return begin, end, nil
}
// rotate1 rotates the current counter file, returning its expiry, or the zero
// time if rotation failed.
func (f *file) rotate1() time.Time {
// Cleanup must be performed while unlocked, since invalidateCounters may
// involve calls to f.lookup.
var previous *mappedFile // read below while holding the f.mu.
defer func() {
// Counters must be invalidated whenever the mapped file changes.
if next := f.current.Load(); next != previous {
f.invalidateCounters()
// Ensure that the previous counter mapped file is closed.
if previous != nil {
previous.close() // safe to call multiple times
}
}
}()
f.mu.Lock()
defer f.mu.Unlock()
previous = f.current.Load()
if f.err != nil {
return time.Time{} // already in failed state; nothing to do
}
fail := func(err error) {
debugPrintf("rotate: %v", err)
f.err = err
f.current.Store(nil)
}
if mode, _ := telemetry.Default.Mode(); mode == "off" {
// TODO(rfindley): do we ever want to make ErrDisabled recoverable?
// Specifically, if f.err is ErrDisabled, should we check again during when
// rotating?
fail(ErrDisabled)
return time.Time{}
}
if f.buildInfo == nil {
bi, ok := debug.ReadBuildInfo()
if !ok {
fail(errNoBuildInfo)
return time.Time{}
}
f.buildInfo = bi
}
begin, end, err := counterSpan()
if err != nil {
fail(err)
return time.Time{}
}
if f.timeBegin.Equal(begin) && f.timeEnd.Equal(end) {
return f.timeEnd // nothing to do
}
f.timeBegin, f.timeEnd = begin, end
goVers, progPath, progVers := telemetry.ProgramInfo(f.buildInfo)
meta := fmt.Sprintf("TimeBegin: %s\nTimeEnd: %s\nProgram: %s\nVersion: %s\nGoVersion: %s\nGOOS: %s\nGOARCH: %s\n\n",
f.timeBegin.Format(time.RFC3339), f.timeEnd.Format(time.RFC3339),
progPath, progVers, goVers, runtime.GOOS, runtime.GOARCH)
if len(meta) > maxMetaLen { // should be impossible for our use
fail(fmt.Errorf("metadata too long"))
return time.Time{}
}
if progVers != "" {
progVers = "@" + progVers
}
baseName := fmt.Sprintf("%s%s-%s-%s-%s-%s.%s.count",
path.Base(progPath),
progVers,
goVers,
runtime.GOOS,
runtime.GOARCH,
f.timeBegin.Format(telemetry.DateOnly),
FileVersion,
)
dir := telemetry.Default.LocalDir()
if err := os.MkdirAll(dir, 0777); err != nil {
fail(fmt.Errorf("making local dir: %v", err))
return time.Time{}
}
name := filepath.Join(dir, baseName)
m, err := openMapped(name, meta)
if err != nil {
// Mapping failed:
// If there used to be a mapped file, after cleanup
// incrementing counters will only change their internal state.
// (before cleanup the existing mapped file would be updated)
fail(fmt.Errorf("openMapped: %v", err))
return time.Time{}
}
debugPrintf("using %v", m.f.Name())
f.current.Store(m)
return f.timeEnd
}
func (f *file) newCounter(name string) *atomic.Uint64 {
v, cleanup := f.newCounter1(name)
cleanup()
return v
}
func (f *file) newCounter1(name string) (v *atomic.Uint64, cleanup func()) {
f.mu.Lock()
defer f.mu.Unlock()
current := f.current.Load()
if current == nil {
return nil, nop
}
debugPrintf("newCounter %s in %s\n", name, current.f.Name())
if v, _, _, _ := current.lookup(name); v != nil {
return v, nop
}
v, newM, err := current.newCounter(name)
if err != nil {
debugPrintf("newCounter %s: %v\n", name, err)
return nil, nop
}
cleanup = nop
if newM != nil {
f.current.Store(newM)
cleanup = func() {
f.invalidateCounters()
current.close()
}
}
return v, cleanup
}
var (
openOnce sync.Once
// rotating reports whether the call to Open had rotate = true.
//
// In golang/go#68497, we observed that file rotation can break runtime
// deadlock detection. To minimize the fix for 1.23, we are splitting the
// Open API into one version that rotates the counter file, and another that
// does not. The rotating variable guards against use of both APIs from the
// same process.
rotating bool
)
// Open associates counting with the defaultFile.
// The returned function is for testing only, and should
// be called after all Inc()s are finished, but before
// any reports are generated.
// (Otherwise expired count files will not be deleted on Windows.)
func Open(rotate bool) func() {
if telemetry.DisabledOnPlatform {
return func() {}
}
close := func() {}
openOnce.Do(func() {
rotating = rotate
if mode, _ := telemetry.Default.Mode(); mode == "off" {
// Don't open the file when telemetry is off.
defaultFile.err = ErrDisabled
// No need to clean up.
return
}
debugPrintf("Open(%v)", rotate)
if rotate {
defaultFile.rotate() // calls rotate1 and schedules a rotation
} else {
defaultFile.rotate1()
}
close = func() {
// Once this has been called, the defaultFile is no longer usable.
mf := defaultFile.current.Load()
if mf == nil {
// telemetry might have been off
return
}
mf.close()
}
})
if rotating != rotate {
panic("BUG: Open called with inconsistent values for 'rotate'")
}
return close
}
const (
FileVersion = "v1"
hdrPrefix = "# telemetry/counter file " + FileVersion + "\n"
recordUnit = 32
maxMetaLen = 512
numHash = 512 // 2kB for hash table
maxNameLen = 4 * 1024
limitOff = 0
hashOff = 4
pageSize = 16 * 1024
minFileLen = 16 * 1024
)
// A mappedFile is a counter file mmapped into memory.
//
// The file layout for a mappedFile m is as follows:
//
// offset, byte size: description
// ------------------ -----------
// 0, hdrLen: header, containing metadata; see [mappedHeader]
// hdrLen+limitOff, 4: uint32 allocation limit (byte offset of the end of counter records)
// hdrLen+hashOff, 4*numHash: hash table, stores uint32 heads of a linked list of records, keyed by name hash
// hdrLen+hashOff+4*numHash to limit: counter records: see record syntax below
//
// The record layout is as follows:
//
// offset, byte size: description
// ------------------ -----------
// 0, 8: uint64 counter value
// 8, 12: uint32 name length
// 12, 16: uint32 offset of next record in linked list
// 16, name length: counter name
type mappedFile struct {
meta string
hdrLen uint32
zero [4]byte
closeOnce sync.Once
f *os.File
mapping *mmap.Data
}
// openMapped opens and memory maps a file.
//
// name is the path to the file.
//
// meta is the file metadata, which must match the metadata of the file on disk
// exactly.
//
// existing should be nil the first time this is called for a file,
// and when remapping, should be the previous mappedFile.
func openMapped(name, meta string) (_ *mappedFile, err error) {
hdr, err := mappedHeader(meta)
if err != nil {
return nil, err
}
f, err := os.OpenFile(name, os.O_RDWR|os.O_CREATE, 0666)
if err != nil {
return nil, err
}
// Note: using local variable m here, not return value,
// so that return nil, err does not set m = nil and break the code in the defer.
m := &mappedFile{
f: f,
meta: meta,
}
defer func() {
if err != nil {
m.close()
}
}()
info, err := f.Stat()
if err != nil {
return nil, err
}
// Establish file header and initial data area if not already present.
if info.Size() < minFileLen {
if _, err := f.WriteAt(hdr, 0); err != nil {
return nil, err
}
// Write zeros at the end of the file to extend it to minFileLen.
if _, err := f.WriteAt(m.zero[:], int64(minFileLen-len(m.zero))); err != nil {
return nil, err
}
info, err = f.Stat()
if err != nil {
return nil, err
}
if info.Size() < minFileLen {
return nil, fmt.Errorf("counter: writing file did not extend it")
}
}
// Map into memory.
mapping, err := memmap(f)
if err != nil {
return nil, err
}
m.mapping = mapping
if !bytes.HasPrefix(m.mapping.Data, hdr) {
// TODO(rfindley): we can and should do better here, reading the mapped
// header length and comparing headers exactly.
return nil, fmt.Errorf("counter: header mismatch")
}
m.hdrLen = uint32(len(hdr))
return m, nil
}
func mappedHeader(meta string) ([]byte, error) {
if len(meta) > maxMetaLen {
return nil, fmt.Errorf("counter: metadata too large")
}
np := round(len(hdrPrefix), 4)
n := round(np+4+len(meta), 32)
hdr := make([]byte, n)
copy(hdr, hdrPrefix)
*(*uint32)(unsafe.Pointer(&hdr[np])) = uint32(n)
copy(hdr[np+4:], meta)
return hdr, nil
}
func (m *mappedFile) place(limit uint32, name string) (start, end uint32) {
if limit == 0 {
// first record in file
limit = m.hdrLen + hashOff + 4*numHash
}
n := round(uint32(16+len(name)), recordUnit)
start = round(limit, recordUnit) // should already be rounded but just in case
// Note: Checking for crossing a page boundary would be
// start/pageSize != (start+n-1)/pageSize,
// but we are checking for reaching the page end, so no -1.
// The page end is reserved for use by extend.
// See the comment in m.extend.
if start/pageSize != (start+n)/pageSize {
// bump start to next page
start = round(limit, pageSize)
}
return start, start + n
}
var memmap = mmap.Mmap
var munmap = mmap.Munmap
func (m *mappedFile) close() {
m.closeOnce.Do(func() {
if m.mapping != nil {
munmap(m.mapping)
m.mapping = nil
}
if m.f != nil {
m.f.Close() // best effort
m.f = nil
}
})
}
// hash returns the hash code for name.
// The implementation is FNV-1a.
// This hash function is a fixed detail of the file format.
// It cannot be changed without also changing the file format version.
func hash(name string) uint32 {
const (
offset32 = 2166136261
prime32 = 16777619
)
h := uint32(offset32)
for i := 0; i < len(name); i++ {
c := name[i]
h = (h ^ uint32(c)) * prime32
}
return (h ^ (h >> 16)) % numHash
}
func (m *mappedFile) load32(off uint32) uint32 {
if int64(off) >= int64(len(m.mapping.Data)) {
return 0
}
return (*atomic.Uint32)(unsafe.Pointer(&m.mapping.Data[off])).Load()
}
func (m *mappedFile) cas32(off, old, new uint32) bool {
if int64(off) >= int64(len(m.mapping.Data)) {
panic("bad cas32") // return false would probably loop
}
return (*atomic.Uint32)(unsafe.Pointer(&m.mapping.Data[off])).CompareAndSwap(old, new)
}
// entryAt reads a counter record at the given byte offset.
//
// See the documentation for [mappedFile] for a description of the counter record layout.
func (m *mappedFile) entryAt(off uint32) (name []byte, next uint32, v *atomic.Uint64, ok bool) {
if off < m.hdrLen+hashOff || int64(off)+16 > int64(len(m.mapping.Data)) {
return nil, 0, nil, false
}
nameLen := m.load32(off+8) & 0x00ffffff
if nameLen == 0 || int64(off)+16+int64(nameLen) > int64(len(m.mapping.Data)) {
return nil, 0, nil, false
}
name = m.mapping.Data[off+16 : off+16+nameLen]
next = m.load32(off + 12)
v = (*atomic.Uint64)(unsafe.Pointer(&m.mapping.Data[off]))
return name, next, v, true
}
// writeEntryAt writes a new counter record at the given offset.
//
// See the documentation for [mappedFile] for a description of the counter record layout.
//
// writeEntryAt only returns false in the presence of some form of corruption:
// an offset outside the bounds of the record region in the mapped file.
func (m *mappedFile) writeEntryAt(off uint32, name string) (next *atomic.Uint32, v *atomic.Uint64, ok bool) {
// TODO(rfindley): shouldn't this first condition be off < m.hdrLen+hashOff+4*numHash?
if off < m.hdrLen+hashOff || int64(off)+16+int64(len(name)) > int64(len(m.mapping.Data)) {
return nil, nil, false
}
copy(m.mapping.Data[off+16:], name)
atomic.StoreUint32((*uint32)(unsafe.Pointer(&m.mapping.Data[off+8])), uint32(len(name))|0xff000000)
next = (*atomic.Uint32)(unsafe.Pointer(&m.mapping.Data[off+12]))
v = (*atomic.Uint64)(unsafe.Pointer(&m.mapping.Data[off]))
return next, v, true
}
// lookup searches the mapped file for a counter record with the given name, returning:
// - v: the mapped counter value
// - headOff: the offset of the head pointer (see [mappedFile])
// - head: the value of the head pointer
// - ok: whether lookup succeeded
func (m *mappedFile) lookup(name string) (v *atomic.Uint64, headOff, head uint32, ok bool) {
h := hash(name)
headOff = m.hdrLen + hashOff + h*4
head = m.load32(headOff)
off := head
for off != 0 {
ename, next, v, ok := m.entryAt(off)
if !ok {
return nil, 0, 0, false
}
if string(ename) == name {
return v, headOff, head, true
}
off = next
}
return nil, headOff, head, true
}
// newCounter allocates and writes a new counter record with the given name.
//
// If name is already recorded in the file, newCounter returns the existing counter.
func (m *mappedFile) newCounter(name string) (v *atomic.Uint64, m1 *mappedFile, err error) {
if len(name) > maxNameLen {
return nil, nil, fmt.Errorf("counter name too long")
}
orig := m
defer func() {
if m != orig {
if err != nil {
m.close()
} else {
m1 = m
}
}
}()
v, headOff, head, ok := m.lookup(name)
for tries := 0; !ok; tries++ {
if tries >= 10 {
debugFatalf("corrupt: failed to remap after 10 tries")
return nil, nil, errCorrupt
}
// Lookup found an invalid pointer,
// perhaps because the file has grown larger than the mapping.
limit := m.load32(m.hdrLen + limitOff)
if limit, datalen := int64(limit), int64(len(m.mapping.Data)); limit <= datalen {
// Mapping doesn't need to grow, so lookup found actual corruption,
// in the form of an entry pointer that exceeds the recorded allocation
// limit. This should never happen, unless the actual file contents are
// corrupt.
debugFatalf("corrupt: limit %d is within mapping length %d", limit, datalen)
return nil, nil, errCorrupt
}
// That the recorded limit is greater than the mapped data indicates that
// an external process has extended the file. Re-map to pick up this extension.
newM, err := openMapped(m.f.Name(), m.meta)
if err != nil {
return nil, nil, err
}
if limit, datalen := int64(limit), int64(len(newM.mapping.Data)); limit > datalen {
// We've re-mapped, yet limit still exceeds the data length. This
// indicates that the underlying file was somehow truncated, or the
// recorded limit is corrupt.
debugFatalf("corrupt: limit %d exceeds file size %d", limit, datalen)
return nil, nil, errCorrupt
}
// If m != orig, this is at least the second time around the loop
// trying to open the mapping. Close the previous attempt.
if m != orig {
m.close()
}
m = newM
v, headOff, head, ok = m.lookup(name)
}
if v != nil {
return v, nil, nil
}
// Reserve space for new record.
// We are competing against other programs using the same file,
// so we use a compare-and-swap on the allocation limit in the header.
var start, end uint32
for {
// Determine where record should end, and grow file if needed.
limit := m.load32(m.hdrLen + limitOff)
start, end = m.place(limit, name)
debugPrintf("place %s at %#x-%#x\n", name, start, end)
if int64(end) > int64(len(m.mapping.Data)) {
newM, err := m.extend(end)
if err != nil {
return nil, nil, err
}
if m != orig {
m.close()
}
m = newM
continue
}
// Attempt to reserve that space for our record.
if m.cas32(m.hdrLen+limitOff, limit, end) {
break
}
}
// Write record.
next, v, ok := m.writeEntryAt(start, name)
if !ok {
debugFatalf("corrupt: failed to write entry: %#x+%d vs %#x\n", start, len(name), len(m.mapping.Data))
return nil, nil, errCorrupt // more likely our math is wrong
}
// Link record into hash chain, making sure not to introduce a duplicate.
// We know name does not appear in the chain starting at head.
for {
next.Store(head)
if m.cas32(headOff, head, start) {
return v, nil, nil
}
// Check new elements in chain for duplicates.
old := head
head = m.load32(headOff)
for off := head; off != old; {
ename, enext, v, ok := m.entryAt(off)
if !ok {
return nil, nil, errCorrupt
}
if string(ename) == name {
next.Store(^uint32(0)) // mark ours as dead
return v, nil, nil
}
off = enext
}
}
}
func (m *mappedFile) extend(end uint32) (*mappedFile, error) {
end = round(end, pageSize)
info, err := m.f.Stat()
if err != nil {
return nil, err
}
if info.Size() < int64(end) {
// Note: multiple processes could be calling extend at the same time,
// but this write only writes the last 4 bytes of the page.
// The last 4 bytes of the page are reserved for this purpose and hold no data.
// (In m.place, if a new record would extend to the very end of the page,
// it is placed in the next page instead.)
// So it is fine if multiple processes extend at the same time.
if _, err := m.f.WriteAt(m.zero[:], int64(end)-int64(len(m.zero))); err != nil {
return nil, err
}
}
newM, err := openMapped(m.f.Name(), m.meta)
if err != nil {
return nil, err
}
if int64(len(newM.mapping.Data)) < int64(end) {
// File system or logic bug: new file is somehow not extended.
// See go.dev/issue/68311, where this appears to have been happening.
newM.close()
return nil, errCorrupt
}
return newM, err
}
// round returns x rounded up to the next multiple of unit,
// which must be a power of two.
func round[T int | uint32](x T, unit T) T {
return (x + unit - 1) &^ (unit - 1)
}

View File

@@ -0,0 +1,82 @@
// Copyright 2023 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package counter
import (
"bytes"
"fmt"
"strings"
"unsafe"
"golang.org/x/telemetry/internal/mmap"
)
type File struct {
Meta map[string]string
Count map[string]uint64
}
func Parse(filename string, data []byte) (*File, error) {
if !bytes.HasPrefix(data, []byte(hdrPrefix)) || len(data) < pageSize {
if len(data) < pageSize {
return nil, fmt.Errorf("%s: file too short (%d<%d)", filename, len(data), pageSize)
}
return nil, fmt.Errorf("%s: wrong hdr (not %q)", filename, hdrPrefix)
}
corrupt := func() (*File, error) {
// TODO(rfindley): return a useful error message.
return nil, fmt.Errorf("%s: corrupt counter file", filename)
}
f := &File{
Meta: make(map[string]string),
Count: make(map[string]uint64),
}
np := round(len(hdrPrefix), 4)
hdrLen := *(*uint32)(unsafe.Pointer(&data[np]))
if hdrLen > pageSize {
return corrupt()
}
meta := data[np+4 : hdrLen]
if i := bytes.IndexByte(meta, 0); i >= 0 {
meta = meta[:i]
}
m := &mappedFile{
meta: string(meta),
hdrLen: hdrLen,
mapping: &mmap.Data{Data: data},
}
lines := strings.Split(m.meta, "\n")
for _, line := range lines {
if line == "" {
continue
}
k, v, ok := strings.Cut(line, ": ")
if !ok {
return corrupt()
}
f.Meta[k] = v
}
for i := uint32(0); i < numHash; i++ {
headOff := hdrLen + hashOff + i*4
head := m.load32(headOff)
off := head
for off != 0 {
ename, next, v, ok := m.entryAt(off)
if !ok {
return corrupt()
}
if _, ok := f.Count[string(ename)]; ok {
return corrupt()
}
ctrName := DecodeStack(string(ename))
f.Count[ctrName] = v.Load()
off = next
}
}
return f, nil
}

View File

@@ -0,0 +1,212 @@
// Copyright 2023 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package counter
import (
"fmt"
"runtime"
"strings"
"sync"
)
// On the disk, and upstream, stack counters look like sets of
// regular counters with names that include newlines.
// a StackCounter is the in-memory knowledge about a stack counter.
// StackCounters are more expensive to use than regular Counters,
// requiring, at a minimum, a call to runtime.Callers.
type StackCounter struct {
name string
depth int
file *file
mu sync.Mutex
// as this is a detail of the implementation, it could be replaced
// by a more efficient mechanism
stacks []stack
}
type stack struct {
pcs []uintptr
counter *Counter
}
func NewStack(name string, depth int) *StackCounter {
return &StackCounter{name: name, depth: depth, file: &defaultFile}
}
// Inc increments a stack counter. It computes the caller's stack and
// looks up the corresponding counter. It then increments that counter,
// creating it if necessary.
func (c *StackCounter) Inc() {
pcs := make([]uintptr, c.depth)
n := runtime.Callers(2, pcs) // caller of Inc
pcs = pcs[:n]
c.mu.Lock()
defer c.mu.Unlock()
// Existing counter?
var ctr *Counter
for _, s := range c.stacks {
if eq(s.pcs, pcs) {
if s.counter != nil {
ctr = s.counter
break
}
}
}
if ctr == nil {
// Create new counter.
ctr = &Counter{
name: EncodeStack(pcs, c.name),
file: c.file,
}
c.stacks = append(c.stacks, stack{pcs: pcs, counter: ctr})
}
ctr.Inc()
}
// EncodeStack returns the name of the counter to
// use for the given stack of program counters.
// The name encodes the stack.
func EncodeStack(pcs []uintptr, prefix string) string {
var locs []string
lastImport := ""
frs := runtime.CallersFrames(pcs)
for {
fr, more := frs.Next()
// TODO(adonovan): this CutLast(".") operation isn't
// appropriate for generic function symbols.
path, fname := cutLastDot(fr.Function)
if path == lastImport {
path = `"` // (a ditto mark)
} else {
lastImport = path
}
var loc string
if fr.Func != nil {
// Use function-relative line numbering.
// f:+2 means two lines into function f.
// f:-1 should never happen, but be conservative.
//
// An inlined call is replaced by a NOP instruction
// with the correct pclntab information.
_, entryLine := fr.Func.FileLine(fr.Entry)
loc = fmt.Sprintf("%s.%s:%+d,+0x%x", path, fname, fr.Line-entryLine, fr.PC-fr.Entry)
} else {
// The function is non-Go code or is fully inlined:
// use absolute line number within enclosing file.
//
// For inlined calls, the PC and Entry values
// both refer to the enclosing combined function.
// For example, both these PCs are relative to "caller":
//
// callee:=1,+0x12 ('=' means inlined)
// caller:+2,+0x34
loc = fmt.Sprintf("%s.%s:=%d,+0x%x", path, fname, fr.Line, fr.PC-fr.Entry)
}
locs = append(locs, loc)
if !more {
break
}
}
name := prefix + "\n" + strings.Join(locs, "\n")
if len(name) > maxNameLen {
const bad = "\ntruncated\n"
name = name[:maxNameLen-len(bad)] + bad
}
return name
}
// DecodeStack expands the (compressed) stack encoded in the counter name.
func DecodeStack(ename string) string {
if !strings.Contains(ename, "\n") {
return ename // not a stack counter
}
lines := strings.Split(ename, "\n")
var lastPath string // empty or ends with .
for i, line := range lines {
path, rest := cutLastDot(line)
if len(path) == 0 {
continue // unchanged
}
if len(path) == 1 && path[0] == '"' {
lines[i] = lastPath + rest
} else {
lastPath = path + "."
// line unchanged
}
}
return strings.Join(lines, "\n") // trailing \n?
}
// input is <import path>.<function name>
// output is (import path, function name)
func cutLastDot(x string) (before, after string) {
i := strings.LastIndex(x, ".")
if i < 0 {
return "", x
}
return x[:i], x[i+1:]
}
// Names reports all the counter names associated with a StackCounter.
func (c *StackCounter) Names() []string {
c.mu.Lock()
defer c.mu.Unlock()
names := make([]string, len(c.stacks))
for i, s := range c.stacks {
names[i] = s.counter.Name()
}
return names
}
// Counters returns the known Counters for a StackCounter.
// There may be more in the count file.
func (c *StackCounter) Counters() []*Counter {
c.mu.Lock()
defer c.mu.Unlock()
counters := make([]*Counter, len(c.stacks))
for i, s := range c.stacks {
counters[i] = s.counter
}
return counters
}
func eq(a, b []uintptr) bool {
if len(a) != len(b) {
return false
}
for i := range a {
if a[i] != b[i] {
return false
}
}
return true
}
// ReadStack reads the given stack counter.
// This is the implementation of
// golang.org/x/telemetry/counter/countertest.ReadStackCounter.
func ReadStack(c *StackCounter) (map[string]uint64, error) {
ret := map[string]uint64{}
for _, ctr := range c.Counters() {
v, err := Read(ctr)
if err != nil {
return nil, err
}
ret[DecodeStack(ctr.Name())] = v
}
return ret, nil
}
// IsStackCounter reports whether the counter name is for a stack counter.
func IsStackCounter(name string) bool {
return strings.Contains(name, "\n")
}

View File

@@ -0,0 +1,328 @@
// Copyright 2024 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package crashmonitor
// This file defines a monitor that reports arbitrary Go runtime
// crashes to telemetry.
import (
"bytes"
"fmt"
"io"
"log"
"os"
"reflect"
"runtime/debug"
"strconv"
"strings"
"golang.org/x/telemetry/internal/counter"
)
// Parent sets up the parent side of the crashmonitor. It requires
// exclusive use of a writable pipe connected to the child process's stdin.
func Parent(pipe *os.File) {
writeSentinel(pipe)
// Ensure that we get pc=0x%x values in the traceback.
debug.SetTraceback("system")
debug.SetCrashOutput(pipe, debug.CrashOptions{}) // ignore error
}
// Child runs the part of the crashmonitor that runs in the child process.
// It expects its stdin to be connected via a pipe to the parent which has
// run Parent.
func Child() {
// Wait for parent process's dying gasp.
// If the parent dies for any reason this read will return.
data, err := io.ReadAll(os.Stdin)
if err != nil {
log.Fatalf("failed to read from input pipe: %v", err)
}
// If the only line is the sentinel, it wasn't a crash.
if bytes.Count(data, []byte("\n")) < 2 {
childExitHook()
os.Exit(0) // parent exited without crash report
}
log.Printf("parent reported crash:\n%s", data)
// Parse the stack out of the crash report
// and record a telemetry count for it.
name, err := telemetryCounterName(data)
if err != nil {
// Keep count of how often this happens
// so that we can investigate if necessary.
incrementCounter("crash/malformed")
// Something went wrong.
// Save the crash securely in the file system.
f, err := os.CreateTemp(os.TempDir(), "*.crash")
if err != nil {
log.Fatal(err)
}
if _, err := f.Write(data); err != nil {
log.Fatal(err)
}
if err := f.Close(); err != nil {
log.Fatal(err)
}
log.Printf("failed to report crash to telemetry: %v", err)
log.Fatalf("crash report saved at %s", f.Name())
}
incrementCounter(name)
childExitHook()
log.Fatalf("telemetry crash recorded")
}
// (stubbed by test)
var (
incrementCounter = func(name string) { counter.New(name).Inc() }
childExitHook = func() {}
)
// The sentinel function returns its address. The difference between
// this value as observed by calls in two different processes of the
// same executable tells us the relative offset of their text segments.
//
// It would be nice if SetCrashOutput took care of this as it's fiddly
// and likely to confuse every user at first.
func sentinel() uint64 {
return uint64(reflect.ValueOf(sentinel).Pointer())
}
func writeSentinel(out io.Writer) {
fmt.Fprintf(out, "sentinel %x\n", sentinel())
}
// telemetryCounterName parses a crash report produced by the Go
// runtime, extracts the stack of the first runnable goroutine,
// converts each line into telemetry form ("symbol:relative-line"),
// and returns this as the name of a counter.
func telemetryCounterName(crash []byte) (string, error) {
pcs, err := parseStackPCs(string(crash))
if err != nil {
return "", err
}
// Limit the number of frames we request.
pcs = pcs[:min(len(pcs), 16)]
if len(pcs) == 0 {
// This can occur if all goroutines are idle, as when
// caught in a deadlock, or killed by an async signal
// while blocked.
//
// TODO(adonovan): consider how to report such
// situations. Reporting a goroutine in [sleep] or
// [select] state could be quite confusing without
// further information about the nature of the crash,
// as the problem is not local to the code location.
//
// For now, we keep count of this situation so that we
// can access whether it needs a more involved solution.
return "crash/no-running-goroutine", nil
}
// This string appears at the start of all
// crashmonitor-generated counter names.
//
// It is tempting to expose this as a parameter of Start, but
// it is not without risk. What value should most programs
// provide? There's no point giving the name of the executable
// as this is already recorded by telemetry. What if the
// application runs in multiple modes? Then it might be useful
// to record the mode. The problem is that an application with
// multiple modes probably doesn't know its mode by line 1 of
// main.main: it might require flag or argument parsing, or
// even validation of an environment variable, and we really
// want to steer users aware from any logic before Start. The
// flags and arguments will be wrong in the child process, and
// every extra conditional branch creates a risk that the
// recursively executed child program will behave not like the
// monitor but like the application. If the child process
// exits before calling Start, then the parent application
// will not have a monitor, and its crash reports will be
// discarded (written in to a pipe that is never read).
//
// So for now, we use this constant string.
const prefix = "crash/crash"
return counter.EncodeStack(pcs, prefix), nil
}
// parseStackPCs parses the parent process's program counters for the
// first running goroutine out of a GOTRACEBACK=system traceback,
// adjusting them so that they are valid for the child process's text
// segment.
//
// This function returns only program counter values, ensuring that
// there is no possibility of strings from the crash report (which may
// contain PII) leaking into the telemetry system.
func parseStackPCs(crash string) ([]uintptr, error) {
// getSymbol parses the symbol name out of a line of the form:
// SYMBOL(ARGS)
//
// Note: SYMBOL may contain parens "pkg.(*T).method". However, type
// parameters are always replaced with ..., so they cannot introduce
// more parens. e.g., "pkg.(*T[...]).method".
//
// ARGS can contain parens. We want the first paren that is not
// immediately preceded by a ".".
//
// TODO(prattmic): This is mildly complicated and is only used to find
// runtime.sigpanic, so perhaps simplify this by checking explicitly
// for sigpanic.
getSymbol := func(line string) (string, error) {
var prev rune
for i, c := range line {
if line[i] != '(' {
prev = c
continue
}
if prev == '.' {
prev = c
continue
}
return line[:i], nil
}
return "", fmt.Errorf("no symbol for stack frame: %s", line)
}
// getPC parses the PC out of a line of the form:
// \tFILE:LINE +0xRELPC sp=... fp=... pc=...
getPC := func(line string) (uint64, error) {
_, pcstr, ok := strings.Cut(line, " pc=") // e.g. pc=0x%x
if !ok {
return 0, fmt.Errorf("no pc= for stack frame: %s", line)
}
return strconv.ParseUint(pcstr, 0, 64) // 0 => allow 0x prefix
}
var (
pcs []uintptr
parentSentinel uint64
childSentinel = sentinel()
on = false // are we in the first running goroutine?
lines = strings.Split(crash, "\n")
symLine = true // within a goroutine, every other line is a symbol or file/line/pc location, starting with symbol.
currSymbol string
prevSymbol string // symbol of the most recent previous frame with a PC.
)
for i := 0; i < len(lines); i++ {
line := lines[i]
// Read sentinel value.
if parentSentinel == 0 && strings.HasPrefix(line, "sentinel ") {
_, err := fmt.Sscanf(line, "sentinel %x", &parentSentinel)
if err != nil {
return nil, fmt.Errorf("can't read sentinel line")
}
continue
}
// Search for "goroutine GID [STATUS]"
if !on {
if strings.HasPrefix(line, "goroutine ") &&
strings.Contains(line, " [running]:") {
on = true
if parentSentinel == 0 {
return nil, fmt.Errorf("no sentinel value in crash report")
}
}
continue
}
// A blank line marks end of a goroutine stack.
if line == "" {
break
}
// Skip the final "created by SYMBOL in goroutine GID" part.
if strings.HasPrefix(line, "created by ") {
break
}
// Expect a pair of lines:
// SYMBOL(ARGS)
// \tFILE:LINE +0xRELPC sp=0x%x fp=0x%x pc=0x%x
// Note: SYMBOL may contain parens "pkg.(*T).method"
// The RELPC is sometimes missing.
if symLine {
var err error
currSymbol, err = getSymbol(line)
if err != nil {
return nil, fmt.Errorf("error extracting symbol: %v", err)
}
symLine = false // Next line is FILE:LINE.
} else {
// Parse the PC, and correct for the parent and child's
// different mappings of the text section.
pc, err := getPC(line)
if err != nil {
// Inlined frame, perhaps; skip it.
// Done with this frame. Next line is a new frame.
//
// Don't update prevSymbol; we only want to
// track frames with a PC.
currSymbol = ""
symLine = true
continue
}
pc = pc - parentSentinel + childSentinel
// If the previous frame was sigpanic, then this frame
// was a trap (e.g., SIGSEGV).
//
// Typically all middle frames are calls, and report
// the "return PC". That is, the instruction following
// the CALL where the callee will eventually return to.
//
// runtime.CallersFrames is aware of this property and
// will decrement each PC by 1 to "back up" to the
// location of the CALL, which is the actual line
// number the user expects.
//
// This does not work for traps, as a trap is not a
// call, so the reported PC is not the return PC, but
// the actual PC of the trap.
//
// runtime.Callers is aware of this and will
// intentionally increment trap PCs in order to correct
// for the decrement performed by
// runtime.CallersFrames. See runtime.tracebackPCs and
// runtume.(*unwinder).symPC.
//
// We must emulate the same behavior, otherwise we will
// report the location of the instruction immediately
// prior to the trap, which may be on a different line,
// or even a different inlined functions.
//
// TODO(prattmic): The runtime applies the same trap
// behavior for other "injected calls", see injectCall
// in runtime.(*unwinder).next. Do we want to handle
// those as well? I don't believe we'd ever see
// runtime.asyncPreempt or runtime.debugCallV2 in a
// typical crash.
if prevSymbol == "runtime.sigpanic" {
pc++
}
pcs = append(pcs, uintptr(pc))
// Done with this frame. Next line is a new frame.
prevSymbol = currSymbol
currSymbol = ""
symLine = true
}
}
return pcs, nil
}

36
vendor/golang.org/x/telemetry/internal/mmap/mmap.go generated vendored Normal file
View File

@@ -0,0 +1,36 @@
// Copyright 2011 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// This package is a lightly modified version of the mmap code
// in github.com/google/codesearch/index.
// The mmap package provides an abstraction for memory mapping files
// on different platforms.
package mmap
import (
"os"
)
// The backing file is never closed, so Data
// remains valid for the lifetime of the process.
type Data struct {
// TODO(pjw): might be better to define versions of Data
// for the 3 specializations
f *os.File
Data []byte
// Some windows magic
Windows interface{}
}
// Mmap maps the given file into memory.
// When remapping a file, pass the most recently returned Data.
func Mmap(f *os.File) (*Data, error) {
return mmapFile(f)
}
// Munmap unmaps the given file from memory.
func Munmap(d *Data) error {
return munmapFile(d)
}

View File

@@ -0,0 +1,25 @@
// Copyright 2022 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:build (js && wasm) || wasip1 || plan9
package mmap
import (
"io"
"os"
)
// mmapFile on other systems doesn't mmap the file. It just reads everything.
func mmapFile(f *os.File) (*Data, error) {
b, err := io.ReadAll(f)
if err != nil {
return nil, err
}
return &Data{f, b, nil}, nil
}
func munmapFile(_ *Data) error {
return nil
}

View File

@@ -0,0 +1,47 @@
// Copyright 2011 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:build unix
package mmap
import (
"fmt"
"io/fs"
"os"
"syscall"
)
func mmapFile(f *os.File) (*Data, error) {
st, err := f.Stat()
if err != nil {
return nil, err
}
size := st.Size()
pagesize := int64(os.Getpagesize())
if int64(int(size+(pagesize-1))) != size+(pagesize-1) {
return nil, fmt.Errorf("%s: too large for mmap", f.Name())
}
n := int(size)
if n == 0 {
return &Data{f, nil, nil}, nil
}
mmapLength := int(((size + pagesize - 1) / pagesize) * pagesize) // round up to page size
data, err := syscall.Mmap(int(f.Fd()), 0, mmapLength, syscall.PROT_READ|syscall.PROT_WRITE, syscall.MAP_SHARED)
if err != nil {
return nil, &fs.PathError{Op: "mmap", Path: f.Name(), Err: err}
}
return &Data{f, data[:n], nil}, nil
}
func munmapFile(d *Data) error {
if len(d.Data) == 0 {
return nil
}
err := syscall.Munmap(d.Data)
if err != nil {
return &fs.PathError{Op: "munmap", Path: d.f.Name(), Err: err}
}
return nil
}

View File

@@ -0,0 +1,52 @@
// Copyright 2011 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package mmap
import (
"fmt"
"os"
"syscall"
"unsafe"
"golang.org/x/sys/windows"
)
func mmapFile(f *os.File) (*Data, error) {
st, err := f.Stat()
if err != nil {
return nil, err
}
size := st.Size()
if size == 0 {
return &Data{f, nil, nil}, nil
}
// set the min and max sizes to zero to map the whole file, as described in
// https://learn.microsoft.com/en-us/windows/win32/memory/creating-a-file-mapping-object#file-mapping-size
h, err := windows.CreateFileMapping(windows.Handle(f.Fd()), nil, syscall.PAGE_READWRITE, 0, 0, nil)
if err != nil {
return nil, fmt.Errorf("CreateFileMapping %s: %w", f.Name(), err)
}
// the mapping extends from zero to the end of the file mapping
// https://learn.microsoft.com/en-us/windows/win32/api/memoryapi/nf-memoryapi-mapviewoffile
addr, err := windows.MapViewOfFile(h, syscall.FILE_MAP_READ|syscall.FILE_MAP_WRITE, 0, 0, 0)
if err != nil {
return nil, fmt.Errorf("MapViewOfFile %s: %w", f.Name(), err)
}
// Note: previously, we called windows.VirtualQuery here to get the exact
// size of the memory mapped region, but VirtualQuery reported sizes smaller
// than the actual file size (hypothesis: VirtualQuery only reports pages in
// a certain state, and newly written pages may not be counted).
return &Data{f, unsafe.Slice((*byte)(unsafe.Pointer(addr)), size), h}, nil
}
func munmapFile(d *Data) error {
err := windows.UnmapViewOfFile(uintptr(unsafe.Pointer(&d.Data[0])))
x, ok := d.Windows.(windows.Handle)
if ok {
windows.CloseHandle(x)
}
d.f.Close()
return err
}

View File

@@ -0,0 +1,9 @@
// Copyright 2024 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package telemetry
// TODO(rfindley): replace uses of DateOnly with time.DateOnly once we no
// longer support building gopls with go 1.19.
const DateOnly = "2006-01-02"

163
vendor/golang.org/x/telemetry/internal/telemetry/dir.go generated vendored Normal file
View File

@@ -0,0 +1,163 @@
// Copyright 2023 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Package telemetry manages the telemetry mode file.
package telemetry
import (
"fmt"
"os"
"path/filepath"
"runtime"
"strings"
"time"
)
// Default is the default directory containing Go telemetry configuration and
// data.
//
// If Default is uninitialized, Default.Mode will be "off". As a consequence,
// no data should be written to the directory, and so the path values of
// LocalDir, UploadDir, etc. must not matter.
//
// Default is a global for convenience and testing, but should not be mutated
// outside of tests.
//
// TODO(rfindley): it would be nice to completely eliminate this global state,
// or at least push it in the golang.org/x/telemetry package
var Default Dir
// A Dir holds paths to telemetry data inside a directory.
type Dir struct {
dir, local, upload, debug, modefile string
}
// NewDir creates a new Dir encapsulating paths in the given dir.
//
// NewDir does not create any new directories or files--it merely encapsulates
// the telemetry directory layout.
func NewDir(dir string) Dir {
return Dir{
dir: dir,
local: filepath.Join(dir, "local"),
upload: filepath.Join(dir, "upload"),
debug: filepath.Join(dir, "debug"),
modefile: filepath.Join(dir, "mode"),
}
}
func init() {
cfgDir, err := os.UserConfigDir()
if err != nil {
return
}
Default = NewDir(filepath.Join(cfgDir, "go", "telemetry"))
}
func (d Dir) Dir() string {
return d.dir
}
func (d Dir) LocalDir() string {
return d.local
}
func (d Dir) UploadDir() string {
return d.upload
}
func (d Dir) DebugDir() string {
return d.debug
}
func (d Dir) ModeFile() string {
return d.modefile
}
// SetMode updates the telemetry mode with the given mode.
// Acceptable values for mode are "on", "off", or "local".
//
// SetMode always writes the mode file, and explicitly records the date at
// which the modefile was updated. This means that calling SetMode with "on"
// effectively resets the timeout before the next telemetry report is uploaded.
func (d Dir) SetMode(mode string) error {
return d.SetModeAsOf(mode, time.Now())
}
// SetModeAsOf is like SetMode, but accepts an explicit time to use to
// back-date the mode state. This exists only for testing purposes.
func (d Dir) SetModeAsOf(mode string, asofTime time.Time) error {
mode = strings.TrimSpace(mode)
switch mode {
case "on", "off", "local":
default:
return fmt.Errorf("invalid telemetry mode: %q", mode)
}
if d.modefile == "" {
return fmt.Errorf("cannot determine telemetry mode file name")
}
// TODO(rfindley): why is this not 777, consistent with the use of 666 below?
if err := os.MkdirAll(filepath.Dir(d.modefile), 0755); err != nil {
return fmt.Errorf("cannot create a telemetry mode file: %w", err)
}
asof := asofTime.UTC().Format(DateOnly)
// Defensively guarantee that we can parse the asof time.
if _, err := time.Parse(DateOnly, asof); err != nil {
return fmt.Errorf("internal error: invalid mode date %q: %v", asof, err)
}
data := []byte(mode + " " + asof)
return os.WriteFile(d.modefile, data, 0666)
}
// Mode returns the current telemetry mode, as well as the time that the mode
// was effective.
//
// If there is no effective time, the second result is the zero time.
//
// If Mode is "off", no data should be written to the telemetry directory, and
// the other paths values referenced by Dir should be considered undefined.
// This accounts for the case where initializing [Default] fails, and therefore
// local telemetry paths are unknown.
func (d Dir) Mode() (string, time.Time) {
if d.modefile == "" {
return "off", time.Time{} // it's likely LocalDir/UploadDir are empty too. Turn off telemetry.
}
data, err := os.ReadFile(d.modefile)
if err != nil {
return "local", time.Time{} // default
}
mode := string(data)
mode = strings.TrimSpace(mode)
// Forward compatibility for https://go.dev/issue/63142#issuecomment-1734025130
//
// If the modefile contains a date, return it.
if idx := strings.Index(mode, " "); idx >= 0 {
d, err := time.Parse(DateOnly, mode[idx+1:])
if err != nil {
d = time.Time{}
}
return mode[:idx], d
}
return mode, time.Time{}
}
// DisabledOnPlatform indicates whether telemetry is disabled
// due to bugs in the current platform.
//
// TODO(rfindley): move to a more appropriate file.
const DisabledOnPlatform = false ||
// The following platforms could potentially be supported in the future:
runtime.GOOS == "openbsd" || // #60614
runtime.GOOS == "solaris" || // #60968 #60970
runtime.GOOS == "android" || // #60967
runtime.GOOS == "illumos" || // #65544
// These platforms fundamentally can't be supported:
runtime.GOOS == "js" || // #60971
runtime.GOOS == "wasip1" || // #60971
runtime.GOOS == "plan9" || // https://github.com/golang/go/issues/57540#issuecomment-1470766639
runtime.GOARCH == "mips" || runtime.GOARCH == "mipsle" // mips lacks cross-process 64-bit atomics

View File

@@ -0,0 +1,57 @@
// Copyright 2024 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package telemetry
import (
"go/version"
"os"
"path/filepath"
"runtime/debug"
"strings"
)
// IsToolchainProgram reports whether a program with the given path is a Go
// toolchain program.
func IsToolchainProgram(progPath string) bool {
return strings.HasPrefix(progPath, "cmd/")
}
// ProgramInfo extracts the go version, program package path, and program
// version to use for counter files.
//
// For programs in the Go toolchain, the program version will be the same as
// the Go version, and will typically be of the form "go1.2.3", not a semantic
// version of the form "v1.2.3". Go versions may also include spaces and
// special characters.
func ProgramInfo(info *debug.BuildInfo) (goVers, progPath, progVers string) {
goVers = info.GoVersion
if strings.Contains(goVers, "devel") || strings.Contains(goVers, "-") || !version.IsValid(goVers) {
goVers = "devel"
}
progPath = info.Path
if progPath == "" {
progPath = strings.TrimSuffix(filepath.Base(os.Args[0]), ".exe")
}
// Main module version information is not populated for the cmd module, but
// we can re-use the Go version here.
if IsToolchainProgram(progPath) {
progVers = goVers
} else {
progVers = info.Main.Version
if strings.Contains(progVers, "devel") || strings.Count(progVers, "-") > 1 {
// Heuristically mark all pseudo-version-like version strings as "devel"
// to avoid creating too many counter files.
// We should not use regexp that pulls in large dependencies.
// Pseudo-versions have at least three parts (https://go.dev/ref/mod#pseudo-versions).
// This heuristic still allows use to track prerelease
// versions (e.g. gopls@v0.16.0-pre.1, vscgo@v0.42.0-rc.1).
progVers = "devel"
}
}
return goVers, progPath, progVers
}

View File

@@ -0,0 +1,51 @@
// Copyright 2023 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package telemetry
// Common types and directories used by multiple packages.
// An UploadConfig controls what data is uploaded.
type UploadConfig struct {
GOOS []string
GOARCH []string
GoVersion []string
SampleRate float64
Programs []*ProgramConfig
}
type ProgramConfig struct {
// the counter names may have to be
// repeated for each program. (e.g., if the counters are in a package
// that is used in more than one program.)
Name string
Versions []string // versions present in a counterconfig
Counters []CounterConfig `json:",omitempty"`
Stacks []CounterConfig `json:",omitempty"`
}
type CounterConfig struct {
Name string // The "collapsed" counter: <chart>:{<bucket1>,<bucket2>,...}
Rate float64 // If X <= Rate, report this counter
Depth int `json:",omitempty"` // for stack counters
}
// A Report is the weekly aggregate of counters.
type Report struct {
Week string // End day this report covers (YYYY-MM-DD)
LastWeek string // Week field from latest previous report uploaded
X float64 // A random probability used to determine which counters are uploaded
Programs []*ProgramReport
Config string // version of UploadConfig used
}
type ProgramReport struct {
Program string // Package path of the program.
Version string // Program version. Go version if the program is part of the go distribution. Module version, otherwise.
GoVersion string // Go version used to build the program.
GOOS string
GOARCH string
Counters map[string]int64
Stacks map[string]int64
}

45
vendor/golang.org/x/telemetry/internal/upload/Doc.txt generated vendored Normal file
View File

@@ -0,0 +1,45 @@
The upload process converts count files into reports, and
uploads reports. There will be only one report, named YYYY-MM-DD.json,
for a given day.
First phase. Look at the localdir (os.UserConfigdir()/go/telemetry/local)
and find all .count and .json files. Find the count files that are no
longer active by looking at their metadata.
Second phase. Group the inactive count files by their expiry date, and
for each date generate the local report and the upload report. (The upload
report only contains the counters in the upload configuration.) The upload
report is saved in the local directory with a name like YYYY-MM-DD.json, if
there is no file already existing with that name.
If the local report is different, it is saved in the local directory
with a name like local.YYYY-MM-DD.json. The new upload report is
added to the list of .json files from the first phase. At this point
the count files are no longer needed and can be deleted.
Third phase. Look at the .json files in the list from the first phase.
If the name starts with local, skip it. If there is a file with the
identical name in the upload directory, remove the one in the local directory.
Otherwise try to upload the one in the local directory,
If the upload succeeds, move the file to the uploaded directory.
There are various error conditions.
1. Several processes could look at localdir and see work to do.
1A. They could see different sets of expired count files for some day.
This could happen if another process is removing count files. In this
case there is already a YYYY-MM-DD.json file either in localdir
or updatedir, so the process seeing fewer count files will not generate
a report.
1B. They could see the same count files, and no report in either directory.
They will both generate (in memory) reports and check to see if there
is a YYYY-MM-DD.json file in either directory. They could both then
write two files with the same name, but different X values, but
otherwise the same contents. The X values are very close to the front
of the file. Assuming reasonable file system semantics one version of
the file will be written. To minimize this, just before writing reports
the code checks again to see if they exist.
1C. Once there is an existing well-formed file YYYY-MM-DD.json in localdir
eventually the upload will succeed, and the file will be moved to updatedir.
It is possible that other processes will not see the file in updatedir and
upload it again and also move it to uploaddir. This is harmless as all
the uploaded files are identical.

85
vendor/golang.org/x/telemetry/internal/upload/date.go generated vendored Normal file
View File

@@ -0,0 +1,85 @@
// Copyright 2023 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package upload
import (
"fmt"
"os"
"sync"
"time"
"golang.org/x/telemetry/internal/counter"
"golang.org/x/telemetry/internal/telemetry"
)
// time and date handling
var distantPast = 21 * 24 * time.Hour
// reports that are too old (21 days) are not uploaded
func (u *uploader) tooOld(date string, uploadStartTime time.Time) bool {
t, err := time.Parse(telemetry.DateOnly, date)
if err != nil {
u.logger.Printf("tooOld: %v", err)
return false
}
age := uploadStartTime.Sub(t)
return age > distantPast
}
// counterDateSpan parses the counter file named fname and returns the (begin,
// end) span recorded in its metadata, or an error if this data could not be
// extracted.
func (u *uploader) counterDateSpan(fname string) (begin, end time.Time, _ error) {
parsed, err := u.parseCountFile(fname)
if err != nil {
return time.Time{}, time.Time{}, err
}
timeBegin, ok := parsed.Meta["TimeBegin"]
if !ok {
return time.Time{}, time.Time{}, fmt.Errorf("missing counter metadata for TimeBegin")
}
begin, err = time.Parse(time.RFC3339, timeBegin)
if err != nil {
return time.Time{}, time.Time{}, fmt.Errorf("failed to parse TimeBegin: %v", err)
}
timeEnd, ok := parsed.Meta["TimeEnd"]
if !ok {
return time.Time{}, time.Time{}, fmt.Errorf("missing counter metadata for TimeEnd")
}
end, err = time.Parse(time.RFC3339, timeEnd)
if err != nil {
return time.Time{}, time.Time{}, fmt.Errorf("failed to parse TimeEnd: %v", err)
}
return begin, end, nil
}
// avoid parsing count files multiple times
type parsedCache struct {
mu sync.Mutex
m map[string]*counter.File
}
func (u *uploader) parseCountFile(fname string) (*counter.File, error) {
u.cache.mu.Lock()
defer u.cache.mu.Unlock()
if u.cache.m == nil {
u.cache.m = make(map[string]*counter.File)
}
if f, ok := u.cache.m[fname]; ok {
return f, nil
}
buf, err := os.ReadFile(fname)
if err != nil {
return nil, fmt.Errorf("parse ReadFile: %v for %s", err, fname)
}
f, err := counter.Parse(fname, buf)
if err != nil {
return nil, fmt.Errorf("parse Parse: %v for %s", err, fname)
}
u.cache.m[fname] = f
return f, nil
}

View File

@@ -0,0 +1,102 @@
// Copyright 2023 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package upload
import (
"os"
"path/filepath"
"strings"
)
// files to handle
type work struct {
// absolute file names
countfiles []string // count files to process
readyfiles []string // old reports to upload
// relative names
uploaded map[string]bool // reports that have been uploaded
}
// find all the files that look like counter files or reports
// that need to be uploaded. (There may be unexpected leftover files
// and uploading is supposed to be idempotent.)
func (u *uploader) findWork() work {
localdir, uploaddir := u.dir.LocalDir(), u.dir.UploadDir()
var ans work
fis, err := os.ReadDir(localdir)
if err != nil {
u.logger.Printf("Could not find work: failed to read local dir %s: %v", localdir, err)
return ans
}
mode, asof := u.dir.Mode()
u.logger.Printf("Finding work: mode %s asof %s", mode, asof)
// count files end in .v1.count
// reports end in .json. If they are not to be uploaded they
// start with local.
for _, fi := range fis {
if strings.HasSuffix(fi.Name(), ".v1.count") {
fname := filepath.Join(localdir, fi.Name())
_, expiry, err := u.counterDateSpan(fname)
switch {
case err != nil:
u.logger.Printf("Error reading expiry for count file %s: %v", fi.Name(), err)
case expiry.After(u.startTime):
u.logger.Printf("Skipping count file %s: still active", fi.Name())
default:
u.logger.Printf("Collecting count file %s", fi.Name())
ans.countfiles = append(ans.countfiles, fname)
}
} else if strings.HasPrefix(fi.Name(), "local.") {
// skip
} else if strings.HasSuffix(fi.Name(), ".json") && mode == "on" {
// Collect reports that are ready for upload.
reportDate := u.uploadReportDate(fi.Name())
if !asof.IsZero() && !reportDate.IsZero() {
// If both the mode asof date and the report date are present, do the
// right thing...
//
// (see https://github.com/golang/go/issues/63142#issuecomment-1734025130)
if asof.Before(reportDate) {
// Note: since this report was created after telemetry was enabled,
// we can only assume that the process that created it checked that
// the counter data contained therein was all from after the asof
// date.
//
// TODO(rfindley): store the begin date in reports, so that we can
// verify this assumption.
u.logger.Printf("Uploadable: %s", fi.Name())
ans.readyfiles = append(ans.readyfiles, filepath.Join(localdir, fi.Name()))
}
} else {
// ...otherwise fall back on the old behavior of uploading all
// unuploaded files.
//
// TODO(rfindley): invert this logic following more testing. We
// should only upload if we know both the asof date and the report
// date, and they are acceptable.
u.logger.Printf("Uploadable (missing date): %s", fi.Name())
ans.readyfiles = append(ans.readyfiles, filepath.Join(localdir, fi.Name()))
}
}
}
fis, err = os.ReadDir(uploaddir)
if err != nil {
os.MkdirAll(uploaddir, 0777)
return ans
}
// There should be only one of these per day; maybe sometime
// we'll want to clean the directory.
ans.uploaded = make(map[string]bool)
for _, fi := range fis {
if strings.HasSuffix(fi.Name(), ".json") {
u.logger.Printf("Already uploaded: %s", fi.Name())
ans.uploaded[fi.Name()] = true
}
}
return ans
}

View File

@@ -0,0 +1,344 @@
// Copyright 2023 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package upload
import (
"crypto/rand"
"encoding/binary"
"encoding/json"
"fmt"
"math"
"os"
"path/filepath"
"strings"
"time"
"golang.org/x/telemetry/internal/config"
"golang.org/x/telemetry/internal/counter"
"golang.org/x/telemetry/internal/telemetry"
)
// reports generates reports from inactive count files
func (u *uploader) reports(todo *work) ([]string, error) {
if mode, _ := u.dir.Mode(); mode == "off" {
return nil, nil // no reports
}
thisInstant := u.startTime
today := thisInstant.Format(telemetry.DateOnly)
lastWeek := latestReport(todo.uploaded)
if lastWeek >= today { //should never happen
lastWeek = ""
}
u.logger.Printf("Last week: %s, today: %s", lastWeek, today)
countFiles := make(map[string][]string) // expiry date string->filenames
earliest := make(map[string]time.Time) // earliest begin time for any counter
for _, f := range todo.countfiles {
begin, end, err := u.counterDateSpan(f)
if err != nil {
// This shouldn't happen: we should have already skipped count files that
// don't contain valid start or end times.
u.logger.Printf("BUG: failed to parse expiry for collected count file: %v", err)
continue
}
if end.Before(thisInstant) {
expiry := end.Format(dateFormat)
countFiles[expiry] = append(countFiles[expiry], f)
if earliest[expiry].IsZero() || earliest[expiry].After(begin) {
earliest[expiry] = begin
}
}
}
for expiry, files := range countFiles {
if notNeeded(expiry, *todo) {
u.logger.Printf("Files for %s not needed, deleting %v", expiry, files)
// The report already exists.
// There's another check in createReport.
u.deleteFiles(files)
continue
}
fname, err := u.createReport(earliest[expiry], expiry, files, lastWeek)
if err != nil {
u.logger.Printf("Failed to create report for %s: %v", expiry, err)
continue
}
if fname != "" {
u.logger.Printf("Ready to upload: %s", filepath.Base(fname))
todo.readyfiles = append(todo.readyfiles, fname)
}
}
return todo.readyfiles, nil
}
// latestReport returns the YYYY-MM-DD of the last report uploaded
// or the empty string if there are no reports.
func latestReport(uploaded map[string]bool) string {
var latest string
for name := range uploaded {
if strings.HasSuffix(name, ".json") {
if name > latest {
latest = name
}
}
}
if latest == "" {
return ""
}
// strip off the .json
return latest[:len(latest)-len(".json")]
}
// notNeeded returns true if the report for date has already been created
func notNeeded(date string, todo work) bool {
if todo.uploaded != nil && todo.uploaded[date+".json"] {
return true
}
// maybe the report is already in todo.readyfiles
for _, f := range todo.readyfiles {
if strings.Contains(f, date) {
return true
}
}
return false
}
func (u *uploader) deleteFiles(files []string) {
for _, f := range files {
if err := os.Remove(f); err != nil {
// this could be a race condition.
// conversely, on Windows, err may be nil and
// the file not deleted if anyone has it open.
u.logger.Printf("%v failed to remove %s", err, f)
}
}
}
// createReport creates local and upload report files by
// combining all the count files for the expiryDate, and
// returns the upload report file's path.
// It may delete the count files once local and upload report
// files are successfully created.
func (u *uploader) createReport(start time.Time, expiryDate string, countFiles []string, lastWeek string) (string, error) {
uploadOK := true
mode, asof := u.dir.Mode()
if mode != "on" {
u.logger.Printf("No upload config or mode %q is not 'on'", mode)
uploadOK = false // no config, nothing to upload
}
if u.tooOld(expiryDate, u.startTime) {
u.logger.Printf("Expiry date %s is too old", expiryDate)
uploadOK = false
}
// If the mode is recorded with an asof date, don't upload if the report
// includes any data on or before the asof date.
if !asof.IsZero() && !asof.Before(start) {
u.logger.Printf("As-of date %s is not before start %s", asof, start)
uploadOK = false
}
// TODO(rfindley): check that all the x.Meta are consistent for GOOS, GOARCH, etc.
report := &telemetry.Report{
Config: u.configVersion,
X: computeRandom(), // json encodes all the bits
Week: expiryDate,
LastWeek: lastWeek,
}
if report.X > u.config.SampleRate && u.config.SampleRate > 0 {
u.logger.Printf("X: %f > SampleRate:%f, not uploadable", report.X, u.config.SampleRate)
uploadOK = false
}
var succeeded bool
for _, f := range countFiles {
fok := false
x, err := u.parseCountFile(f)
if err != nil {
u.logger.Printf("Unparseable count file %s: %v", filepath.Base(f), err)
continue
}
prog := findProgReport(x.Meta, report)
for k, v := range x.Count {
if counter.IsStackCounter(k) {
// stack
prog.Stacks[k] += int64(v)
} else {
// counter
prog.Counters[k] += int64(v)
}
succeeded = true
fok = true
}
if !fok {
u.logger.Printf("no counters found in %s", f)
}
}
if !succeeded {
return "", fmt.Errorf("none of the %d count files for %s contained counters", len(countFiles), expiryDate)
}
// 1. generate the local report
localContents, err := json.MarshalIndent(report, "", " ")
if err != nil {
return "", fmt.Errorf("failed to marshal report for %s: %v", expiryDate, err)
}
// check that the report can be read back
// TODO(pjw): remove for production?
var report2 telemetry.Report
if err := json.Unmarshal(localContents, &report2); err != nil {
return "", fmt.Errorf("failed to unmarshal local report for %s: %v", expiryDate, err)
}
var uploadContents []byte
if uploadOK {
// 2. create the uploadable version
cfg := config.NewConfig(u.config)
upload := &telemetry.Report{
Week: report.Week,
LastWeek: report.LastWeek,
X: report.X,
Config: report.Config,
}
for _, p := range report.Programs {
// does the uploadConfig want this program?
// if so, copy over the Stacks and Counters
// that the uploadConfig mentions.
if !cfg.HasGoVersion(p.GoVersion) || !cfg.HasProgram(p.Program) || !cfg.HasVersion(p.Program, p.Version) {
continue
}
x := &telemetry.ProgramReport{
Program: p.Program,
Version: p.Version,
GOOS: p.GOOS,
GOARCH: p.GOARCH,
GoVersion: p.GoVersion,
Counters: make(map[string]int64),
Stacks: make(map[string]int64),
}
upload.Programs = append(upload.Programs, x)
for k, v := range p.Counters {
if cfg.HasCounter(p.Program, k) && report.X <= cfg.Rate(p.Program, k) {
x.Counters[k] = v
}
}
// and the same for Stacks
// this can be made more efficient, when it matters
for k, v := range p.Stacks {
before, _, _ := strings.Cut(k, "\n")
if cfg.HasStack(p.Program, before) && report.X <= cfg.Rate(p.Program, before) {
x.Stacks[k] = v
}
}
}
uploadContents, err = json.MarshalIndent(upload, "", " ")
if err != nil {
return "", fmt.Errorf("failed to marshal upload report for %s: %v", expiryDate, err)
}
}
localFileName := filepath.Join(u.dir.LocalDir(), "local."+expiryDate+".json")
uploadFileName := filepath.Join(u.dir.LocalDir(), expiryDate+".json")
/* Prepare to write files */
// if either file exists, someone has been here ahead of us
// (there is still a race, but this check shortens the open window)
if _, err := os.Stat(localFileName); err == nil {
u.deleteFiles(countFiles)
return "", fmt.Errorf("local report %s already exists", localFileName)
}
if _, err := os.Stat(uploadFileName); err == nil {
u.deleteFiles(countFiles)
return "", fmt.Errorf("report %s already exists", uploadFileName)
}
// write the uploadable file
var errUpload, errLocal error
if uploadOK {
_, errUpload = exclusiveWrite(uploadFileName, uploadContents)
}
// write the local file
_, errLocal = exclusiveWrite(localFileName, localContents)
/* Wrote the files */
// even though these errors won't occur, what should happen
// if errUpload == nil and it is ok to upload, and errLocal != nil?
if errLocal != nil {
return "", fmt.Errorf("failed to write local file %s (%v)", localFileName, errLocal)
}
if errUpload != nil {
return "", fmt.Errorf("failed to write upload file %s (%v)", uploadFileName, errUpload)
}
u.logger.Printf("Created %s, deleting %d count files", filepath.Base(uploadFileName), len(countFiles))
u.deleteFiles(countFiles)
if uploadOK {
return uploadFileName, nil
}
return "", nil
}
// exclusiveWrite attempts to create filename exclusively, and if successful,
// writes content to the resulting file handle.
//
// It returns a boolean indicating whether the exclusive handle was acquired,
// and an error indicating whether the operation succeeded.
// If the file already exists, exclusiveWrite returns (false, nil).
func exclusiveWrite(filename string, content []byte) (_ bool, rerr error) {
f, err := os.OpenFile(filename, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0644)
if err != nil {
if os.IsExist(err) {
return false, nil
}
return false, err
}
defer func() {
if err := f.Close(); err != nil && rerr == nil {
rerr = err
}
}()
if _, err := f.Write(content); err != nil {
return false, err
}
return true, nil
}
// return an existing ProgremReport, or create anew
func findProgReport(meta map[string]string, report *telemetry.Report) *telemetry.ProgramReport {
for _, prog := range report.Programs {
if prog.Program == meta["Program"] && prog.Version == meta["Version"] &&
prog.GoVersion == meta["GoVersion"] && prog.GOOS == meta["GOOS"] &&
prog.GOARCH == meta["GOARCH"] {
return prog
}
}
prog := telemetry.ProgramReport{
Program: meta["Program"],
Version: meta["Version"],
GoVersion: meta["GoVersion"],
GOOS: meta["GOOS"],
GOARCH: meta["GOARCH"],
Counters: make(map[string]int64),
Stacks: make(map[string]int64),
}
report.Programs = append(report.Programs, &prog)
return &prog
}
// computeRandom returns a cryptographic random float64 in the range [0, 1],
// with 52 bits of precision.
func computeRandom() float64 {
for {
b := make([]byte, 8)
_, err := rand.Read(b)
if err != nil {
panic(fmt.Sprintf("rand.Read failed: %v", err))
}
// and turn it into a float64
x := math.Float64frombits(binary.LittleEndian.Uint64(b))
if math.IsNaN(x) || math.IsInf(x, 0) {
continue
}
x = math.Abs(x)
if x < 0x1p-1000 { // avoid underflow patterns
continue
}
frac, _ := math.Frexp(x) // 52 bits of randomness
return frac*2 - 1
}
}

226
vendor/golang.org/x/telemetry/internal/upload/run.go generated vendored Normal file
View File

@@ -0,0 +1,226 @@
// Copyright 2023 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package upload
import (
"fmt"
"io"
"log"
"os"
"path"
"path/filepath"
"runtime/debug"
"strings"
"time"
"golang.org/x/telemetry/internal/configstore"
"golang.org/x/telemetry/internal/telemetry"
)
// RunConfig configures non-default behavior of a call to Run.
//
// All fields are optional, for testing or observability.
type RunConfig struct {
TelemetryDir string // if set, overrides the telemetry data directory
UploadURL string // if set, overrides the telemetry upload endpoint
LogWriter io.Writer // if set, used for detailed logging of the upload process
Env []string // if set, appended to the config download environment
StartTime time.Time // if set, overrides the upload start time
}
// Run generates and uploads reports, as allowed by the mode file.
func Run(config RunConfig) error {
defer func() {
if err := recover(); err != nil {
log.Printf("upload recover: %v", err)
}
}()
uploader, err := newUploader(config)
if err != nil {
return err
}
defer uploader.Close()
return uploader.Run()
}
// uploader encapsulates a single upload operation, carrying parameters and
// shared state.
type uploader struct {
// config is used to select counters to upload.
config *telemetry.UploadConfig //
configVersion string // version of the config
dir telemetry.Dir // the telemetry dir to process
uploadServerURL string
startTime time.Time
cache parsedCache
logFile *os.File
logger *log.Logger
}
// newUploader creates a new uploader to use for running the upload for the
// given config.
//
// Uploaders should only be used for one call to [uploader.Run].
func newUploader(rcfg RunConfig) (*uploader, error) {
// Determine the upload directory.
var dir telemetry.Dir
if rcfg.TelemetryDir != "" {
dir = telemetry.NewDir(rcfg.TelemetryDir)
} else {
dir = telemetry.Default
}
// Determine the upload URL.
uploadURL := rcfg.UploadURL
if uploadURL == "" {
uploadURL = "https://telemetry.go.dev/upload"
}
// Determine the upload logger.
//
// This depends on the provided rcfg.LogWriter and the presence of
// dir.DebugDir, as follows:
// 1. If LogWriter is present, log to it.
// 2. If DebugDir is present, log to a file within it.
// 3. If both LogWriter and DebugDir are present, log to a multi writer.
// 4. If neither LogWriter nor DebugDir are present, log to a noop logger.
var logWriters []io.Writer
logFile, err := debugLogFile(dir.DebugDir())
if err != nil {
logFile = nil
}
if logFile != nil {
logWriters = append(logWriters, logFile)
}
if rcfg.LogWriter != nil {
logWriters = append(logWriters, rcfg.LogWriter)
}
var logWriter io.Writer
switch len(logWriters) {
case 0:
logWriter = io.Discard
case 1:
logWriter = logWriters[0]
default:
logWriter = io.MultiWriter(logWriters...)
}
logger := log.New(logWriter, "", log.Ltime|log.Lmicroseconds|log.Lshortfile)
// Fetch the upload config, if it is not provided.
var (
config *telemetry.UploadConfig
configVersion string
)
if mode, _ := dir.Mode(); mode == "on" {
// golang/go#68946: only download the upload config if it will be used.
//
// TODO(rfindley): This is a narrow change aimed at minimally fixing the
// associated bug. In the future, we should read the mode only once during
// the upload process.
config, configVersion, err = configstore.Download("latest", rcfg.Env)
if err != nil {
return nil, err
}
} else {
config = &telemetry.UploadConfig{}
configVersion = "v0.0.0-0"
}
// Set the start time, if it is not provided.
startTime := time.Now().UTC()
if !rcfg.StartTime.IsZero() {
startTime = rcfg.StartTime
}
return &uploader{
config: config,
configVersion: configVersion,
dir: dir,
uploadServerURL: uploadURL,
startTime: startTime,
logFile: logFile,
logger: logger,
}, nil
}
// Close cleans up any resources associated with the uploader.
func (u *uploader) Close() error {
if u.logFile == nil {
return nil
}
return u.logFile.Close()
}
// Run generates and uploads reports
func (u *uploader) Run() error {
if telemetry.DisabledOnPlatform {
return nil
}
todo := u.findWork()
ready, err := u.reports(&todo)
if err != nil {
u.logger.Printf("Error building reports: %v", err)
return fmt.Errorf("reports failed: %v", err)
}
u.logger.Printf("Uploading %d reports", len(ready))
for _, f := range ready {
u.uploadReport(f)
}
return nil
}
// debugLogFile arranges to write a log file in the given debug directory, if
// it exists.
func debugLogFile(debugDir string) (*os.File, error) {
fd, err := os.Stat(debugDir)
if os.IsNotExist(err) {
return nil, nil
}
if err != nil {
return nil, err
}
if !fd.IsDir() {
return nil, fmt.Errorf("debug path %q is not a directory", debugDir)
}
info, ok := debug.ReadBuildInfo()
if !ok {
return nil, fmt.Errorf("no build info")
}
year, month, day := time.Now().UTC().Date()
goVers := info.GoVersion
// E.g., goVers:"go1.22-20240109-RC01 cl/597041403 +dcbe772469 X:loopvar"
words := strings.Fields(goVers)
goVers = words[0]
progPkgPath := info.Path
if progPkgPath == "" {
progPkgPath = strings.TrimSuffix(filepath.Base(os.Args[0]), ".exe")
}
prog := path.Base(progPkgPath)
progVers := info.Main.Version
if progVers == "(devel)" { // avoid special characters in created file names
progVers = "devel"
}
logBase := strings.ReplaceAll(
fmt.Sprintf("%s-%s-%s-%4d%02d%02d-%d.log", prog, progVers, goVers, year, month, day, os.Getpid()),
" ", "")
fname := filepath.Join(debugDir, logBase)
if _, err := os.Stat(fname); err == nil {
// This process previously called upload.Run
return nil, nil
}
f, err := os.OpenFile(fname, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0666)
if err != nil {
if os.IsExist(err) {
return nil, nil // this process previously called upload.Run
}
return nil, err
}
return f, nil
}

117
vendor/golang.org/x/telemetry/internal/upload/upload.go generated vendored Normal file
View File

@@ -0,0 +1,117 @@
// Copyright 2023 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package upload
import (
"bytes"
"net/http"
"os"
"path/filepath"
"regexp"
"strings"
"time"
"golang.org/x/telemetry/internal/telemetry"
)
var (
dateRE = regexp.MustCompile(`(\d\d\d\d-\d\d-\d\d)[.]json$`)
dateFormat = telemetry.DateOnly
// TODO(rfindley): use dateFormat throughout.
)
// uploadReportDate returns the date component of the upload file name, or "" if the
// date was unmatched.
func (u *uploader) uploadReportDate(fname string) time.Time {
match := dateRE.FindStringSubmatch(fname)
if match == nil || len(match) < 2 {
u.logger.Printf("malformed report name: missing date: %q", filepath.Base(fname))
return time.Time{}
}
d, err := time.Parse(dateFormat, match[1])
if err != nil {
u.logger.Printf("malformed report name: bad date: %q", filepath.Base(fname))
return time.Time{}
}
return d
}
func (u *uploader) uploadReport(fname string) {
thisInstant := u.startTime
// TODO(rfindley): use uploadReportDate here, once we've done a gopls release.
// first make sure it is not in the future
today := thisInstant.Format(telemetry.DateOnly)
match := dateRE.FindStringSubmatch(fname)
if match == nil || len(match) < 2 {
u.logger.Printf("Report name %q missing date", filepath.Base(fname))
} else if match[1] > today {
u.logger.Printf("Report date for %q is later than today (%s)", filepath.Base(fname), today)
return // report is in the future, which shouldn't happen
}
buf, err := os.ReadFile(fname)
if err != nil {
u.logger.Printf("%v reading %s", err, fname)
return
}
if u.uploadReportContents(fname, buf) {
// anything left to do?
}
}
// try to upload the report, 'true' if successful
func (u *uploader) uploadReportContents(fname string, buf []byte) bool {
fdate := strings.TrimSuffix(filepath.Base(fname), ".json")
fdate = fdate[len(fdate)-len(telemetry.DateOnly):]
newname := filepath.Join(u.dir.UploadDir(), fdate+".json")
// Lock the upload, to prevent duplicate uploads.
{
lockname := newname + ".lock"
lockfile, err := os.OpenFile(lockname, os.O_CREATE|os.O_EXCL, 0666)
if err != nil {
u.logger.Printf("Failed to acquire lock %s: %v", lockname, err)
return false
}
_ = lockfile.Close()
defer os.Remove(lockname)
}
if _, err := os.Stat(newname); err == nil {
// Another process uploaded but failed to clean up (or hasn't yet cleaned
// up). Ensure that cleanup occurs.
u.logger.Printf("After acquire: report already uploaded")
_ = os.Remove(fname)
return false
}
endpoint := u.uploadServerURL + "/" + fdate
b := bytes.NewReader(buf)
resp, err := http.Post(endpoint, "application/json", b)
if err != nil {
u.logger.Printf("Error upload %s to %s: %v", filepath.Base(fname), endpoint, err)
return false
}
// hope for a 200, remove file on a 4xx, otherwise it will be retried by another process
if resp.StatusCode != 200 {
u.logger.Printf("Failed to upload %s to %s: %s", filepath.Base(fname), endpoint, resp.Status)
if resp.StatusCode >= 400 && resp.StatusCode < 500 {
err := os.Remove(fname)
if err == nil {
u.logger.Printf("Removed local/%s", filepath.Base(fname))
} else {
u.logger.Printf("Error removing local/%s: %v", filepath.Base(fname), err)
}
}
return false
}
// Store a copy of the uploaded report in the uploaded directory.
if err := os.WriteFile(newname, buf, 0644); err == nil {
os.Remove(fname) // if it exists
}
u.logger.Printf("Uploaded %s to %q", fdate+".json", endpoint)
return true
}

38
vendor/golang.org/x/telemetry/mode.go generated vendored Normal file
View File

@@ -0,0 +1,38 @@
// Copyright 2023 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package telemetry
import "golang.org/x/telemetry/internal/telemetry"
// Mode returns the current telemetry mode.
//
// The telemetry mode is a global value that controls both the local collection
// and uploading of telemetry data. Possible mode values are:
// - "on": both collection and uploading is enabled
// - "local": collection is enabled, but uploading is disabled
// - "off": both collection and uploading are disabled
//
// When mode is "on", or "local", telemetry data is written to the local file
// system and may be inspected with the [gotelemetry] command.
//
// If an error occurs while reading the telemetry mode from the file system,
// Mode returns the default value "local".
//
// [gotelemetry]: https://pkg.go.dev/golang.org/x/telemetry/cmd/gotelemetry
func Mode() string {
mode, _ := telemetry.Default.Mode()
return mode
}
// SetMode sets the global telemetry mode to the given value.
//
// See the documentation of [Mode] for a description of the supported mode
// values.
//
// An error is returned if the provided mode value is invalid, or if an error
// occurs while persisting the mode value to the file system.
func SetMode(mode string) error {
return telemetry.Default.SetMode(mode)
}

13
vendor/golang.org/x/telemetry/npm generated vendored Normal file
View File

@@ -0,0 +1,13 @@
#!/bin/bash
# Copyright 2022 The Go Authors. All rights reserved.
# Use of this source code is governed by a BSD-style
# license that can be found in the LICENSE file.
docker run \
--rm \
--volume $(pwd):/workspace \
--workdir /workspace \
--env NODE_OPTIONS="--dns-result-order=ipv4first" \
--entrypoint npm \
node:18.16.0-slim \
$@

13
vendor/golang.org/x/telemetry/npx generated vendored Normal file
View File

@@ -0,0 +1,13 @@
#!/bin/bash
# Copyright 2022 The Go Authors. All rights reserved.
# Use of this source code is governed by a BSD-style
# license that can be found in the LICENSE file.
docker run \
--rm \
--volume $(pwd):/workspace \
--workdir /workspace \
--env NODE_OPTIONS="--dns-result-order=ipv4first" \
--entrypoint npx \
node:18.16.0-slim \
$@

4363
vendor/golang.org/x/telemetry/package-lock.json generated vendored Normal file

File diff suppressed because it is too large Load Diff

23
vendor/golang.org/x/telemetry/package.json generated vendored Normal file
View File

@@ -0,0 +1,23 @@
{
"scripts": {
"eslint": "eslint . --fix",
"stylelint": "stylelint '**/*.css' --fix",
"prettier": "prettier --write **/*.{css,ts,md,yaml} !**/*.min.css",
"all": "run-s --continue-on-error eslint stylelint prettier"
},
"devDependencies": {
"@typescript-eslint/eslint-plugin": "5.59.6",
"@typescript-eslint/parser": "5.59.6",
"eslint": "8.40.0",
"eslint-config-prettier": "8.8.0",
"npm-run-all": "4.1.5",
"prettier": "2.8.8",
"stylelint": "15.6.2",
"stylelint-config-standard": "33.0.0",
"typescript": "5.0.4"
},
"dependencies": {
"@observablehq/plot": "0.6.9",
"d3": "7.8.5"
}
}

354
vendor/golang.org/x/telemetry/start.go generated vendored Normal file
View File

@@ -0,0 +1,354 @@
// Copyright 2024 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package telemetry
import (
"fmt"
"log"
"os"
"os/exec"
"path/filepath"
"sync"
"time"
"golang.org/x/sync/errgroup"
"golang.org/x/telemetry/counter"
"golang.org/x/telemetry/internal/crashmonitor"
"golang.org/x/telemetry/internal/telemetry"
"golang.org/x/telemetry/internal/upload"
)
// Config controls the behavior of [Start].
type Config struct {
// ReportCrashes, if set, will enable crash reporting.
// ReportCrashes uses the [debug.SetCrashOutput] mechanism, which is a
// process-wide resource.
// Do not make other calls to that function within your application.
// ReportCrashes is a non-functional unless the program is built with go1.23+.
ReportCrashes bool
// Upload causes this program to periodically upload approved counters
// from the local telemetry database to telemetry.go.dev.
//
// This option has no effect unless the user has given consent
// to enable data collection, for example by running
// cmd/gotelemetry or affirming the gopls dialog.
//
// (This feature is expected to be used only by gopls.
// Longer term, the go command may become the sole program
// responsible for uploading.)
Upload bool
// TelemetryDir, if set, will specify an alternate telemetry
// directory to write data to. If not set, it uses the default
// directory.
// This field is intended to be used for isolating testing environments.
TelemetryDir string
// UploadStartTime, if set, overrides the time used as the upload start time,
// which is the time used by the upload logic to determine whether counter
// file data should be uploaded. Only counter files that have expired before
// the start time are considered for upload.
//
// This field can be used to simulate a future upload that collects recently
// modified counters.
UploadStartTime time.Time
// UploadURL, if set, overrides the URL used to receive uploaded reports. If
// unset, this URL defaults to https://telemetry.go.dev/upload.
UploadURL string
}
// Start initializes telemetry using the specified configuration.
//
// Start opens the local telemetry database so that counter increment
// operations are durably recorded in the local file system.
//
// If [Config.Upload] is set, and the user has opted in to telemetry
// uploading, this process may attempt to upload approved counters
// to telemetry.go.dev.
//
// If [Config.ReportCrashes] is set, any fatal crash will be
// recorded by incrementing a counter named for the stack of the
// first running goroutine in the traceback.
//
// If either of these flags is set, Start re-executes the current
// executable as a child process, in a special mode in which it
// acts as a telemetry sidecar for the parent process (the application).
// In that mode, the call to Start will never return, so Start must
// be called immediately within main, even before such things as
// inspecting the command line. The application should avoid expensive
// steps or external side effects in init functions, as they will
// be executed twice (parent and child).
//
// Start returns a StartResult, which may be awaited via [StartResult.Wait] to
// wait for all work done by Start to complete.
func Start(config Config) *StartResult {
switch v := os.Getenv(telemetryChildVar); v {
case "":
// The subprocess started by parent has GO_TELEMETRY_CHILD=1.
return parent(config)
case "1":
child(config) // child will exit the process when it's done.
case "2":
// Do nothing: this was executed directly or indirectly by a child.
default:
log.Fatalf("unexpected value for %q: %q", telemetryChildVar, v)
}
return &StartResult{}
}
// MaybeChild executes the telemetry child logic if the calling program is
// the telemetry child process, and does nothing otherwise. It is meant to be
// called as the first thing in a program that uses telemetry.Start but cannot
// call telemetry.Start immediately when it starts.
func MaybeChild(config Config) {
if v := os.Getenv(telemetryChildVar); v == "1" {
child(config) // child will exit the process when it's done.
}
// other values of the telemetryChildVar environment variable
// will be handled by telemetry.Start.
}
// A StartResult is a handle to the result of a call to [Start]. Call
// [StartResult.Wait] to wait for the completion of all work done on behalf of
// Start.
type StartResult struct {
wg sync.WaitGroup
}
// Wait waits for the completion of all work initiated by [Start].
func (res *StartResult) Wait() {
if res == nil {
return
}
res.wg.Wait()
}
var daemonize = func(cmd *exec.Cmd) {}
// If telemetryChildVar is set to "1" in the environment, this is the telemetry
// child.
//
// If telemetryChildVar is set to "2", this is a child of the child, and no
// further forking should occur.
const telemetryChildVar = "GO_TELEMETRY_CHILD"
// If telemetryUploadVar is set to "1" in the environment, the upload token has been
// acquired by the parent, and the child should attempt an upload.
const telemetryUploadVar = "GO_TELEMETRY_CHILD_UPLOAD"
func parent(config Config) *StartResult {
if config.TelemetryDir != "" {
telemetry.Default = telemetry.NewDir(config.TelemetryDir)
}
result := new(StartResult)
mode, _ := telemetry.Default.Mode()
if mode == "off" {
// Telemetry is turned off. Crash reporting doesn't work without telemetry
// at least set to "local". The upload process runs in both "on" and "local" modes.
// In local mode the upload process builds local reports but does not do the upload.
return result
}
counter.Open()
if _, err := os.Stat(telemetry.Default.LocalDir()); err != nil {
// There was a problem statting LocalDir, which is needed for both
// crash monitoring and counter uploading. Most likely, there was an
// error creating telemetry.LocalDir in the counter.Open call above.
// Don't start the child.
return result
}
childShouldUpload := config.Upload && acquireUploadToken()
reportCrashes := config.ReportCrashes
if reportCrashes || childShouldUpload {
startChild(reportCrashes, childShouldUpload, result)
}
return result
}
func startChild(reportCrashes, upload bool, result *StartResult) {
// This process is the application (parent).
// Fork+exec the telemetry child.
exe, err := os.Executable()
if err != nil {
// There was an error getting os.Executable. It's possible
// for this to happen on AIX if os.Args[0] is not an absolute
// path and we can't find os.Args[0] in PATH.
log.Printf("failed to start telemetry sidecar: os.Executable: %v", err)
return
}
cmd := exec.Command(exe, "** telemetry **") // this unused arg is just for ps(1)
daemonize(cmd)
cmd.Env = append(os.Environ(), telemetryChildVar+"=1")
if upload {
cmd.Env = append(cmd.Env, telemetryUploadVar+"=1")
}
cmd.Dir = telemetry.Default.LocalDir()
// The child process must write to a log file, not
// the stderr file it inherited from the parent, as
// the child may outlive the parent but should not prolong
// the life of any pipes created (by the grandparent)
// to gather the output of the parent.
//
// By default, we discard the child process's stderr,
// but in line with the uploader, log to a file in debug
// only if that directory was created by the user.
fd, err := os.Stat(telemetry.Default.DebugDir())
if err != nil {
if !os.IsNotExist(err) {
log.Printf("failed to stat debug directory: %v", err)
return
}
} else if fd.IsDir() {
// local/debug exists and is a directory. Set stderr to a log file path
// in local/debug.
childLogPath := filepath.Join(telemetry.Default.DebugDir(), "sidecar.log")
childLog, err := os.OpenFile(childLogPath, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0600)
if err != nil {
log.Printf("opening sidecar log file for child: %v", err)
return
}
defer childLog.Close()
cmd.Stderr = childLog
}
var crashOutputFile *os.File
if reportCrashes {
pipe, err := cmd.StdinPipe()
if err != nil {
log.Printf("StdinPipe: %v", err)
return
}
crashOutputFile = pipe.(*os.File) // (this conversion is safe)
}
if err := cmd.Start(); err != nil {
// The child couldn't be started. Log the failure.
log.Printf("can't start telemetry child process: %v", err)
return
}
if reportCrashes {
crashmonitor.Parent(crashOutputFile)
}
result.wg.Add(1)
go func() {
cmd.Wait() // Release resources if cmd happens not to outlive this process.
result.wg.Done()
}()
}
func child(config Config) {
log.SetPrefix(fmt.Sprintf("telemetry-sidecar (pid %v): ", os.Getpid()))
if config.TelemetryDir != "" {
telemetry.Default = telemetry.NewDir(config.TelemetryDir)
}
// golang/go#67211: be sure to set telemetryChildVar before running the
// child, because the child itself invokes the go command to download the
// upload config. If the telemetryChildVar variable is still set to "1",
// that delegated go command may think that it is itself a telemetry
// child.
//
// On the other hand, if telemetryChildVar were simply unset, then the
// delegated go commands would fork themselves recursively. Short-circuit
// this recursion.
os.Setenv(telemetryChildVar, "2")
upload := os.Getenv(telemetryUploadVar) == "1"
// The crashmonitor and/or upload process may themselves record counters.
counter.Open()
// Start crashmonitoring and uploading depending on what's requested
// and wait for the longer running child to complete before exiting:
// if we collected a crash before the upload finished, wait for the
// upload to finish before exiting
var g errgroup.Group
if config.ReportCrashes {
g.Go(func() error {
crashmonitor.Child()
return nil
})
}
if upload {
g.Go(func() error {
uploaderChild(config.UploadStartTime, config.UploadURL)
return nil
})
}
g.Wait()
os.Exit(0)
}
func uploaderChild(asof time.Time, uploadURL string) {
if err := upload.Run(upload.RunConfig{
UploadURL: uploadURL,
LogWriter: os.Stderr,
StartTime: asof,
}); err != nil {
log.Printf("upload failed: %v", err)
}
}
// acquireUploadToken acquires a token permitting the caller to upload.
// To limit the frequency of uploads, only one token is issue per
// machine per time period.
// The boolean indicates whether the token was acquired.
func acquireUploadToken() bool {
if telemetry.Default.LocalDir() == "" {
// The telemetry dir wasn't initialized properly, probably because
// os.UserConfigDir did not complete successfully. In that case
// there are no counters to upload, so we should just do nothing.
return false
}
tokenfile := filepath.Join(telemetry.Default.LocalDir(), "upload.token")
const period = 24 * time.Hour
// A process acquires a token by successfully creating a
// well-known file. If the file already exists and has an
// mtime age less then than the period, the process does
// not acquire the token. If the file is older than the
// period, the process is allowed to remove the file and
// try to re-create it.
fi, err := os.Stat(tokenfile)
if err == nil {
if time.Since(fi.ModTime()) < period {
return false
}
// There's a possible race here where two processes check the
// token file and see that it's older than the period, then the
// first one removes it and creates another, and then a second one
// removes the newly created file and creates yet another
// file. Then both processes would act as though they had the token.
// This is very rare, but it's also okay because we're only grabbing
// the token to do rate limiting, not for correctness.
_ = os.Remove(tokenfile)
} else if !os.IsNotExist(err) {
log.Printf("error acquiring upload taken: statting token file: %v", err)
return false
}
f, err := os.OpenFile(tokenfile, os.O_CREATE|os.O_EXCL, 0666)
if err != nil {
if os.IsExist(err) {
return false
}
log.Printf("error acquiring upload token: creating token file: %v", err)
return false
}
_ = f.Close()
return true
}

22
vendor/golang.org/x/telemetry/start_posix.go generated vendored Normal file
View File

@@ -0,0 +1,22 @@
// Copyright 2024 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:build darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris
package telemetry
import (
"os/exec"
"syscall"
)
func init() {
daemonize = daemonizePosix
}
func daemonizePosix(cmd *exec.Cmd) {
cmd.SysProcAttr = &syscall.SysProcAttr{
Setsid: true,
}
}

29
vendor/golang.org/x/telemetry/start_windows.go generated vendored Normal file
View File

@@ -0,0 +1,29 @@
// Copyright 2024 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:build windows
package telemetry
import (
"os/exec"
"syscall"
"golang.org/x/sys/windows"
)
func init() {
daemonize = daemonizeWindows
}
func daemonizeWindows(cmd *exec.Cmd) {
// Set DETACHED_PROCESS creation flag so that closing
// the console window the parent process was run in
// does not kill the child.
// See documentation of creation flags in the Microsoft documentation:
// https://learn.microsoft.com/en-us/windows/win32/procthread/process-creation-flags
cmd.SysProcAttr = &syscall.SysProcAttr{
CreationFlags: windows.DETACHED_PROCESS,
}
}

26
vendor/golang.org/x/telemetry/tsconfig.json generated vendored Normal file
View File

@@ -0,0 +1,26 @@
{
/* Visit https://aka.ms/tsconfig.json to read more about this file */
"compilerOptions": {
"target": "ES2022",
"module": "ES2022",
"moduleResolution": "node",
"strict": true,
"allowUnusedLabels": false,
"allowUnreachableCode": false,
"exactOptionalPropertyTypes": true,
"noFallthroughCasesInSwitch": true,
"noImplicitOverride": true,
"noImplicitReturns": true,
"noPropertyAccessFromIndexSignature": true,
"noUncheckedIndexedAccess": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"checkJs": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
},
}

21
vendor/golang.org/x/telemetry/types_alias.go generated vendored Normal file
View File

@@ -0,0 +1,21 @@
// Copyright 2024 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package telemetry
import "golang.org/x/telemetry/internal/telemetry"
// Common types and directories used by multiple packages.
// An UploadConfig controls what data is uploaded.
type UploadConfig = telemetry.UploadConfig
type ProgramConfig = telemetry.ProgramConfig
type CounterConfig = telemetry.CounterConfig
// A Report is what's uploaded (or saved locally)
type Report = telemetry.Report
type ProgramReport = telemetry.ProgramReport