16 Commits

Author SHA1 Message Date
a31a0b4829 Modernize invertergui: MQTT write support, HA integration, UI updates
Some checks failed
build / inverter_gui_pipeline (push) Has been cancelled
2026-02-19 12:03:52 +11:00
Nicholas Thompson
959d1e3c1f Merge pull request #45 from bclermont/patch-1
Improve MK2 frames handling
2026-02-17 08:39:20 +02:00
Nicholas Thompson
bda5aa9238 Merge branch 'master' into patch-1 2026-02-17 08:24:24 +02:00
Nicholas Thompson
4e5da3f322 Merge pull request #47 from serhii-vasylkiv/mqtt-password-file
Some checks failed
build / inverter_gui_pipeline (push) Has been cancelled
Add support for reading MQTT password from file
2026-02-17 08:23:37 +02:00
Zergus
826576516f Add support for reading MQTT password from file
Add `--mqtt.password-file` flag and `MQTT_PASSWORD_FILE` env var to allow
reading the MQTT password from a file (e.g. Docker secrets). When set,
the file contents overrides the --mqtt.password value.

Addresses diebietse/invertergui#46
2026-02-15 23:41:11 +02:00
Jean-Luc Mongrain
933c7cc307 Improve logging and handle Victron reboot 2024-07-22 15:54:48 -04:00
Nicholas Thompson
9471f0b5ac Merge pull request #42 from diebietse/vendor-upgrades
Vendor upgrades
2024-03-18 21:45:11 +02:00
Nicholas Thompson
4d774937df Vendor updates 2024-03-18 21:09:54 +02:00
Nicholas Thompson
3702654ec3 Upgrade libraries
Upgrade libraries to fix CVEs
2024-03-18 21:09:39 +02:00
Nicholas Thompson
7d63422555 Merge pull request #40 from diebietse/vendor-upgrades
Upgrade Go to v1.22 and libraries
2024-02-20 14:45:17 +02:00
Nicholas Thompson
fa6fc5fb34 Fix linter issues
Fix all new linter issues that are raised by golanglint-ci 1.56
2024-02-19 20:08:46 +02:00
Nicholas Thompson
86d865ce1b Update golanglint-ci to 1.56 2024-02-19 20:06:54 +02:00
Nicholas Thompson
516dd6b611 Go vendor update 2024-02-19 20:06:50 +02:00
Nicholas Thompson
df421fcccb Upgrade library versions 2024-02-19 20:06:47 +02:00
Nicholas Thompson
a050189e12 Go version to 1.22 2024-02-19 20:06:43 +02:00
Nicholas Thompson
7bcdc3276f Add upgrade-vendor helper to Makefile 2024-02-19 19:51:13 +02:00
707 changed files with 50389 additions and 55777 deletions

View File

@@ -19,7 +19,7 @@ jobs:
- name: "Setup" - name: "Setup"
uses: actions/setup-go@v3 uses: actions/setup-go@v3
with: with:
go-version: '1.20' go-version: '1.22'
- name: Lint - name: Lint
uses: golangci/golangci-lint-action@v3 uses: golangci/golangci-lint-action@v3

2
.gitignore vendored
View File

@@ -22,3 +22,5 @@ _testmain.go
*.exe *.exe
*.test *.test
*.prof *.prof
vendor/

View File

@@ -1,22 +0,0 @@
run:
deadline: 10m
linters:
enable-all: false
enable:
- errcheck
- govet
- ineffassign
- typecheck
- dupl
- goconst
- gofmt
- unconvert
- revive
- staticcheck
- gosimple
- unused
issues:
max-per-linter: 0
max-same-issues: 0

View File

@@ -1,4 +1,4 @@
FROM golang:1.20-alpine as builder FROM golang:1.22-alpine as builder
RUN mkdir /build RUN mkdir /build
COPY . /build/ COPY . /build/

View File

@@ -47,7 +47,7 @@ docker:
lint: lint:
docker run --rm -it \ docker run --rm -it \
-w /src -v $(shell pwd):/src \ -w /src -v $(shell pwd):/src \
golangci/golangci-lint:v1.51 golangci-lint run \ golangci/golangci-lint:v1.56 golangci-lint run \
-v -c .golangci.yml -v -c .golangci.yml
clean: clean:
@@ -56,3 +56,7 @@ clean:
vendor: vendor:
go mod tidy go mod tidy
go mod vendor go mod vendor
.PHONY: upgrade-vendor
upgrade-vendor:
go get -u ./...

110
README.md
View File

@@ -21,7 +21,7 @@ docker run --name invertergui --device /dev/ttyUSB0:/dev/ttyUSB0 -p 8080:8080 gh
## Requirements ## Requirements
This project makes use of [Go Modules](https://github.com/golang/go/wiki/Modules). The minimum version for Go is 1.16 This project makes use of [Go Modules](https://github.com/golang/go/wiki/Modules). The minimum supported version for Go is 1.22
## Getting started ## Getting started
@@ -37,10 +37,17 @@ Application Options:
--cli.enabled Enable CLI output. [$CLI_ENABLED] --cli.enabled Enable CLI output. [$CLI_ENABLED]
--mqtt.enabled Enable MQTT publishing. [$MQTT_ENABLED] --mqtt.enabled Enable MQTT publishing. [$MQTT_ENABLED]
--mqtt.broker= Set the host port and scheme of the MQTT broker. (default: tcp://localhost:1883) [$MQTT_BROKER] --mqtt.broker= Set the host port and scheme of the MQTT broker. (default: tcp://localhost:1883) [$MQTT_BROKER]
--mqtt.client_id= Set the client ID for the MQTT connection. (default: interter-gui) [$MQTT_CLIENT_ID] --mqtt.client_id= Set the client ID for the MQTT connection. (default: inverter-gui) [$MQTT_CLIENT_ID]
--mqtt.topic= Set the MQTT topic updates published to. (default: invertergui/updates) [$MQTT_TOPIC] --mqtt.topic= Set the MQTT topic updates published to. (default: invertergui/updates) [$MQTT_TOPIC]
--mqtt.command_topic= Set the MQTT topic that receives write commands for Victron settings/RAM variables. (default: invertergui/settings/set) [$MQTT_COMMAND_TOPIC]
--mqtt.status_topic= Set the MQTT topic where write command status updates are published. (default: invertergui/settings/status) [$MQTT_STATUS_TOPIC]
--mqtt.ha.enabled Enable Home Assistant MQTT discovery integration. [$MQTT_HA_ENABLED]
--mqtt.ha.discovery_prefix= Set Home Assistant MQTT discovery prefix. (default: homeassistant) [$MQTT_HA_DISCOVERY_PREFIX]
--mqtt.ha.node_id= Set Home Assistant node ID used for discovery topics and unique IDs. (default: invertergui) [$MQTT_HA_NODE_ID]
--mqtt.ha.device_name= Set Home Assistant device display name. (default: Victron Inverter) [$MQTT_HA_DEVICE_NAME]
--mqtt.username= Set the MQTT username [$MQTT_USERNAME] --mqtt.username= Set the MQTT username [$MQTT_USERNAME]
--mqtt.password= Set the MQTT password [$MQTT_PASSWORD] --mqtt.password= Set the MQTT password [$MQTT_PASSWORD]
--mqtt.password-file= Path to a file containing the MQTT password [$MQTT_PASSWORD_FILE]
--loglevel= The log level to generate logs at. ("panic", "fatal", "error", "warn", "info", "debug", "trace") (default: info) [$LOGLEVEL] --loglevel= The log level to generate logs at. ("panic", "fatal", "error", "warn", "info", "debug", "trace") (default: info) [$LOGLEVEL]
Help Options: Help Options:
@@ -82,6 +89,20 @@ Battery Power: -0.659 W
Battery Charge: 100.000 % Battery Charge: 100.000 %
``` ```
The web UI also includes a **Remote Panel Control** section for:
- Remote Panel Mode (`on`, `off`, `charger_only`, `inverter_only`)
- Remote Panel Current Limit (AC input current limit in amps)
- Remote Panel Standby (prevent sleep while turned off)
The combined mode + current limit action maps to the same behavior as
`set_remote_panel_state` in `victron-mk3`.
The backing HTTP API endpoints are:
- `GET/POST /api/remote-panel/state`
- `GET/POST /api/remote-panel/standby`
### Munin ### Munin
The Munin plugin location is at /munin (http://localhost:8080/munin). The Munin plugin location is at /munin (http://localhost:8080/munin).
@@ -281,16 +302,97 @@ The MQTT client will publish updates to the given broker at the set topic.
```bash ```bash
--mqtt.enabled Enable MQTT publishing. [$MQTT_ENABLED] --mqtt.enabled Enable MQTT publishing. [$MQTT_ENABLED]
--mqtt.broker= Set the host port and scheme of the MQTT broker. (default: tcp://localhost:1883) [$MQTT_BROKER] --mqtt.broker= Set the host port and scheme of the MQTT broker. (default: tcp://localhost:1883) [$MQTT_BROKER]
--mqtt.client_id= Set the client ID for the MQTT connection. (default: interter-gui) [$MQTT_CLIENT_ID] --mqtt.client_id= Set the client ID for the MQTT connection. (default: inverter-gui) [$MQTT_CLIENT_ID]
--mqtt.topic= Set the MQTT topic updates published to. (default: invertergui/updates) [$MQTT_TOPIC] --mqtt.topic= Set the MQTT topic updates published to. (default: invertergui/updates) [$MQTT_TOPIC]
--mqtt.command_topic= Set the MQTT topic that receives write commands for Victron settings/RAM variables. (default: invertergui/settings/set) [$MQTT_COMMAND_TOPIC]
--mqtt.status_topic= Set the MQTT topic where write command status updates are published. (default: invertergui/settings/status) [$MQTT_STATUS_TOPIC]
--mqtt.ha.enabled Enable Home Assistant MQTT discovery integration. [$MQTT_HA_ENABLED]
--mqtt.ha.discovery_prefix= Set Home Assistant MQTT discovery prefix. (default: homeassistant) [$MQTT_HA_DISCOVERY_PREFIX]
--mqtt.ha.node_id= Set Home Assistant node ID used for discovery topics and unique IDs. (default: invertergui) [$MQTT_HA_NODE_ID]
--mqtt.ha.device_name= Set Home Assistant device display name. (default: Victron Inverter) [$MQTT_HA_DEVICE_NAME]
--mqtt.username= Set the MQTT username [$MQTT_USERNAME] --mqtt.username= Set the MQTT username [$MQTT_USERNAME]
--mqtt.password= Set the MQTT password [$MQTT_PASSWORD] --mqtt.password= Set the MQTT password [$MQTT_PASSWORD]
--mqtt.password-file= Path to a file containing the MQTT password [$MQTT_PASSWORD_FILE]
``` ```
The MQTT client can be enabled by setting the environment variable `MQTT_ENABLED=true` or flag `--mqtt.enabled`. The MQTT client can be enabled by setting the environment variable `MQTT_ENABLED=true` or flag `--mqtt.enabled`.
All MQTT configuration can be done via flags or as environment variables. All MQTT configuration can be done via flags or as environment variables.
The URI for the broker can be configured format should be `scheme://host:port`, where "scheme" is one of "tcp", "ssl", or "ws". The URI for the broker can be configured format should be `scheme://host:port`, where "scheme" is one of "tcp", "ssl", or "ws".
When `--mqtt.command_topic` is configured, the application subscribes to that topic and accepts JSON write commands.
The recommended command for inverter control follows the same model used by `victron-mk3`:
```json
{
"request_id": "optional-correlation-id",
"kind": "panel_state",
"switch": "on",
"current_limit": 16.5
}
```
`switch` supports `charger_only`, `inverter_only`, `on`, and `off` (or numeric values `1..4`).
`current_limit` is in amps and optional. If omitted, only the switch state is changed.
To update only the current limit (while preserving the last known mode), send:
```json
{
"kind": "panel_state",
"current_limit": 12.0
}
```
If no prior mode is known (for example on a fresh broker state), this command is rejected until a mode command is sent once.
Standby can be controlled with:
```json
{
"kind": "standby",
"standby": true
}
```
Low-level writes are still supported:
```json
{
"kind": "setting",
"id": 15,
"value": 1
}
```
`kind` supports `panel_state`, `setting`, and `ram_var` (with aliases for each).
The result is published to `--mqtt.status_topic` with status `ok` or `error`.
### Home Assistant
Enable Home Assistant auto-discovery with:
```bash
--mqtt.ha.enabled
```
When enabled, `invertergui` publishes retained discovery payloads and availability under:
- `{topic-root}/homeassistant/availability` (`online`/`offline`)
- `{discovery_prefix}/sensor/{node_id}/.../config`
- `{discovery_prefix}/binary_sensor/{node_id}/.../config`
- `{discovery_prefix}/select/{node_id}/remote_panel_mode/config` (if command topic is configured)
- `{discovery_prefix}/number/{node_id}/remote_panel_current_limit/config` (if command topic is configured)
- `{discovery_prefix}/switch/{node_id}/remote_panel_standby/config` (if command topic is configured)
The discovered entities include battery/input/output sensors, a data-valid diagnostic binary sensor,
plus remote panel controls for:
- `Remote Panel Mode` (`on`, `off`, `charger_only`, `inverter_only`)
- `Remote Panel Current Limit` (AC input current limit in amps)
- `Remote Panel Standby` (prevent device sleep while off)
The combined mode + current limit behavior is provided through the `panel_state` MQTT command kind,
which mirrors `victron_mk3.set_remote_panel_state`.
## TTY Device ## TTY Device
The intertergui application makes use of a serial tty device to monitor the Multiplus. The intertergui application makes use of a serial tty device to monitor the Multiplus.
@@ -360,4 +462,4 @@ This repos includes a [Grafana](https://grafana.com/) dashboard in the [grafana
[release-badge]: https://img.shields.io/github/v/release/diebietse/invertergui [release-badge]: https://img.shields.io/github/v/release/diebietse/invertergui
[release-link]: https://github.com/diebietse/invertergui/releases [release-link]: https://github.com/diebietse/invertergui/releases
[codecov-badge]: https://codecov.io/gh/diebietse/invertergui/branch/master/graph/badge.svg?token=xTLfEzoqYF [codecov-badge]: https://codecov.io/gh/diebietse/invertergui/branch/master/graph/badge.svg?token=xTLfEzoqYF
[codecov-link]: https://codecov.io/gh/diebietse/invertergui [codecov-link]: https://codecov.io/gh/diebietse/invertergui

View File

@@ -1,6 +1,10 @@
package main package main
import ( import (
"fmt"
"os"
"strings"
"github.com/jessevdk/go-flags" "github.com/jessevdk/go-flags"
) )
@@ -15,12 +19,21 @@ type config struct {
Enabled bool `long:"cli.enabled" env:"CLI_ENABLED" description:"Enable CLI output."` Enabled bool `long:"cli.enabled" env:"CLI_ENABLED" description:"Enable CLI output."`
} }
MQTT struct { MQTT struct {
Enabled bool `long:"mqtt.enabled" env:"MQTT_ENABLED" description:"Enable MQTT publishing."` Enabled bool `long:"mqtt.enabled" env:"MQTT_ENABLED" description:"Enable MQTT publishing."`
Broker string `long:"mqtt.broker" env:"MQTT_BROKER" default:"tcp://localhost:1883" description:"Set the host port and scheme of the MQTT broker."` Broker string `long:"mqtt.broker" env:"MQTT_BROKER" default:"tcp://localhost:1883" description:"Set the host port and scheme of the MQTT broker."`
ClientID string `long:"mqtt.client_id" env:"MQTT_CLIENT_ID" default:"interter-gui" description:"Set the client ID for the MQTT connection."` ClientID string `long:"mqtt.client_id" env:"MQTT_CLIENT_ID" default:"inverter-gui" description:"Set the client ID for the MQTT connection."`
Topic string `long:"mqtt.topic" env:"MQTT_TOPIC" default:"invertergui/updates" description:"Set the MQTT topic updates published to."` Topic string `long:"mqtt.topic" env:"MQTT_TOPIC" default:"invertergui/updates" description:"Set the MQTT topic updates published to."`
Username string `long:"mqtt.username" env:"MQTT_USERNAME" default:"" description:"Set the MQTT username"` CommandTopic string `long:"mqtt.command_topic" env:"MQTT_COMMAND_TOPIC" default:"invertergui/settings/set" description:"Set the MQTT topic that receives write commands for Victron settings/RAM variables."`
Password string `long:"mqtt.password" env:"MQTT_PASSWORD" default:"" description:"Set the MQTT password"` StatusTopic string `long:"mqtt.status_topic" env:"MQTT_STATUS_TOPIC" default:"invertergui/settings/status" description:"Set the MQTT topic where write command status updates are published."`
HA struct {
Enabled bool `long:"mqtt.ha.enabled" env:"MQTT_HA_ENABLED" description:"Enable Home Assistant MQTT discovery integration."`
DiscoveryPrefix string `long:"mqtt.ha.discovery_prefix" env:"MQTT_HA_DISCOVERY_PREFIX" default:"homeassistant" description:"Set Home Assistant MQTT discovery prefix."`
NodeID string `long:"mqtt.ha.node_id" env:"MQTT_HA_NODE_ID" default:"invertergui" description:"Set Home Assistant node ID used for discovery topics and unique IDs."`
DeviceName string `long:"mqtt.ha.device_name" env:"MQTT_HA_DEVICE_NAME" default:"Victron Inverter" description:"Set Home Assistant device display name."`
}
Username string `long:"mqtt.username" env:"MQTT_USERNAME" default:"" description:"Set the MQTT username"`
Password string `long:"mqtt.password" env:"MQTT_PASSWORD" default:"" description:"Set the MQTT password"`
PasswordFile string `long:"mqtt.password-file" env:"MQTT_PASSWORD_FILE" default:"" description:"Path to a file containing the MQTT password"`
} }
Loglevel string `long:"loglevel" env:"LOGLEVEL" default:"info" description:"The log level to generate logs at. (\"panic\", \"fatal\", \"error\", \"warn\", \"info\", \"debug\", \"trace\")"` Loglevel string `long:"loglevel" env:"LOGLEVEL" default:"info" description:"The log level to generate logs at. (\"panic\", \"fatal\", \"error\", \"warn\", \"info\", \"debug\", \"trace\")"`
} }
@@ -31,5 +44,30 @@ func parseConfig() (*config, error) {
if _, err := parser.Parse(); err != nil { if _, err := parser.Parse(); err != nil {
return nil, err return nil, err
} }
if err := resolvePasswordFile(conf); err != nil {
return nil, err
}
return conf, nil return conf, nil
} }
func resolvePasswordFile(conf *config) error {
if conf.MQTT.PasswordFile != "" && conf.MQTT.Password != "" {
return fmt.Errorf("mqtt.password and mqtt.password-file are mutually exclusive")
}
if conf.MQTT.PasswordFile != "" {
password, err := readPasswordFile(conf.MQTT.PasswordFile)
if err != nil {
return err
}
conf.MQTT.Password = password
}
return nil
}
func readPasswordFile(path string) (string, error) {
data, err := os.ReadFile(path)
if err != nil {
return "", fmt.Errorf("could not read MQTT password file: %w", err)
}
return strings.TrimRight(string(data), "\n\r"), nil
}

View File

@@ -0,0 +1,107 @@
package main
import (
"os"
"path/filepath"
"testing"
)
const testInlineSecret = "inline-secret"
func TestReadPasswordFile(t *testing.T) {
tests := []struct {
name string
content string
expected string
}{
{
name: "plain password",
content: "secret",
expected: "secret",
},
{
name: "password with trailing newline",
content: "secret\n",
expected: "secret",
},
{
name: "password with trailing carriage return and newline",
content: "secret\r\n",
expected: "secret",
},
{
name: "empty file",
content: "",
expected: "",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
path := filepath.Join(t.TempDir(), "password")
if err := os.WriteFile(path, []byte(tt.content), 0o600); err != nil {
t.Fatalf("failed to write test file: %v", err)
}
got, err := readPasswordFile(path)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if got != tt.expected {
t.Errorf("got %q, want %q", got, tt.expected)
}
})
}
}
func TestReadPasswordFile_NotFound(t *testing.T) {
_, err := readPasswordFile("/nonexistent/path/password")
if err == nil {
t.Fatal("expected error for missing file, got nil")
}
}
func TestResolvePassword_MutuallyExclusive(t *testing.T) {
path := filepath.Join(t.TempDir(), "password")
if err := os.WriteFile(path, []byte("secret"), 0o600); err != nil {
t.Fatalf("failed to write test file: %v", err)
}
conf := &config{}
conf.MQTT.Password = testInlineSecret
conf.MQTT.PasswordFile = path
err := resolvePasswordFile(conf)
if err == nil {
t.Fatal("expected error when both mqtt.password and mqtt.password-file are set, got nil")
}
}
func TestResolvePassword_FromFile(t *testing.T) {
path := filepath.Join(t.TempDir(), "password")
if err := os.WriteFile(path, []byte("file-secret\n"), 0o600); err != nil {
t.Fatalf("failed to write test file: %v", err)
}
conf := &config{}
conf.MQTT.PasswordFile = path
if err := resolvePasswordFile(conf); err != nil {
t.Fatalf("unexpected error: %v", err)
}
if conf.MQTT.Password != "file-secret" {
t.Errorf("got %q, want %q", conf.MQTT.Password, "file-secret")
}
}
func TestResolvePassword_NoFile(t *testing.T) {
conf := &config{}
conf.MQTT.Password = testInlineSecret
if err := resolvePasswordFile(conf); err != nil {
t.Fatalf("unexpected error: %v", err)
}
if conf.MQTT.Password != testInlineSecret {
t.Errorf("got %q, want %q", conf.MQTT.Password, testInlineSecret)
}
}

View File

@@ -37,14 +37,14 @@ import (
"net/http" "net/http"
"os" "os"
"github.com/diebietse/invertergui/mk2core" "invertergui/mk2core"
"github.com/diebietse/invertergui/mk2driver" "invertergui/mk2driver"
"github.com/diebietse/invertergui/plugins/cli" "invertergui/plugins/cli"
"github.com/diebietse/invertergui/plugins/mqttclient" "invertergui/plugins/mqttclient"
"github.com/diebietse/invertergui/plugins/munin" "invertergui/plugins/munin"
"github.com/diebietse/invertergui/plugins/prometheus" "invertergui/plugins/prometheus"
"github.com/diebietse/invertergui/plugins/webui" "invertergui/plugins/webui"
"github.com/diebietse/invertergui/plugins/webui/static" "invertergui/plugins/webui/static"
"github.com/prometheus/client_golang/prometheus/promhttp" "github.com/prometheus/client_golang/prometheus/promhttp"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"github.com/tarm/serial" "github.com/tarm/serial"
@@ -77,9 +77,15 @@ func main() {
} }
// Webgui // Webgui
gui := webui.NewWebGui(core.NewSubscription()) var writer mk2driver.SettingsWriter
if w, ok := mk2.(mk2driver.SettingsWriter); ok {
writer = w
}
gui := webui.NewWebGui(core.NewSubscription(), writer)
http.Handle("/", static.New()) http.Handle("/", static.New())
http.Handle("/ws", http.HandlerFunc(gui.ServeHub)) http.Handle("/ws", http.HandlerFunc(gui.ServeHub))
http.Handle("/api/remote-panel/state", http.HandlerFunc(gui.ServeRemotePanelState))
http.Handle("/api/remote-panel/standby", http.HandlerFunc(gui.ServeRemotePanelStandby))
// Munin // Munin
mu := munin.NewMunin(core.NewSubscription()) mu := munin.NewMunin(core.NewSubscription())
@@ -93,13 +99,24 @@ func main() {
// MQTT // MQTT
if conf.MQTT.Enabled { if conf.MQTT.Enabled {
mqttConf := mqttclient.Config{ mqttConf := mqttclient.Config{
Broker: conf.MQTT.Broker, Broker: conf.MQTT.Broker,
Topic: conf.MQTT.Topic, Topic: conf.MQTT.Topic,
ClientID: conf.MQTT.ClientID, CommandTopic: conf.MQTT.CommandTopic,
StatusTopic: conf.MQTT.StatusTopic,
ClientID: conf.MQTT.ClientID,
HomeAssistant: mqttclient.HomeAssistantConfig{
Enabled: conf.MQTT.HA.Enabled,
DiscoveryPrefix: conf.MQTT.HA.DiscoveryPrefix,
NodeID: conf.MQTT.HA.NodeID,
DeviceName: conf.MQTT.HA.DeviceName,
},
Username: conf.MQTT.Username, Username: conf.MQTT.Username,
Password: conf.MQTT.Password, Password: conf.MQTT.Password,
} }
if err := mqttclient.New(core.NewSubscription(), mqttConf); err != nil { if writer == nil {
log.Warn("MK2 data source does not support write commands; MQTT command topic will be ignored")
}
if err := mqttclient.New(core.NewSubscription(), writer, mqttConf); err != nil {
log.Fatalf("Could not setup MQTT client: %v", err) log.Fatalf("Could not setup MQTT client: %v", err)
} }
} }

View File

@@ -1,4 +0,0 @@
coverage:
precision: 2
round: down
range: "65...100"

37
go.mod
View File

@@ -1,30 +1,31 @@
module github.com/diebietse/invertergui module invertergui
go 1.20 go 1.26
require ( require (
github.com/eclipse/paho.mqtt.golang v1.4.2 github.com/eclipse/paho.mqtt.golang v1.5.1
github.com/gorilla/websocket v1.5.0 github.com/gorilla/websocket v1.5.3
github.com/jessevdk/go-flags v1.5.0 github.com/jessevdk/go-flags v1.6.1
github.com/prometheus/client_golang v1.14.0 github.com/prometheus/client_golang v1.23.2
github.com/sirupsen/logrus v1.9.0 github.com/sirupsen/logrus v1.9.4
github.com/stretchr/testify v1.8.2 github.com/stretchr/testify v1.11.1
github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07 github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07
) )
require ( require (
github.com/beorn7/perks v1.0.1 // indirect github.com/beorn7/perks v1.0.1 // indirect
github.com/cespare/xxhash/v2 v2.1.2 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect
github.com/golang/protobuf v1.5.2 // indirect github.com/kr/text v0.2.0 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/prometheus/client_model v0.3.0 // indirect github.com/prometheus/client_model v0.6.2 // indirect
github.com/prometheus/common v0.37.0 // indirect github.com/prometheus/common v0.67.5 // indirect
github.com/prometheus/procfs v0.8.0 // indirect github.com/prometheus/procfs v0.19.2 // indirect
golang.org/x/net v0.0.0-20220225172249-27dd8689420f // indirect go.yaml.in/yaml/v2 v2.4.3 // indirect
golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f // indirect golang.org/x/net v0.50.0 // indirect
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 // indirect golang.org/x/sync v0.19.0 // indirect
google.golang.org/protobuf v1.28.1 // indirect golang.org/x/sys v0.41.0 // indirect
google.golang.org/protobuf v1.36.11 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect
) )

552
go.sum
View File

@@ -1,509 +1,93 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=
cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=
cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=
cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To=
cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4=
cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M=
cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc=
cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk=
cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs=
cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc=
cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY=
cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=
cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=
cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg=
cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc=
cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ=
cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=
cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=
cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA=
cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU=
cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos=
cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=
cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/eclipse/paho.mqtt.golang v1.4.2 h1:66wOzfUHSSI1zamx7jR6yMEI5EuHnT1G6rNA5PM12m4= github.com/eclipse/paho.mqtt.golang v1.4.3 h1:2kwcUGn8seMUfWndX0hGbvH8r7crgcJguQNCyp70xik=
github.com/eclipse/paho.mqtt.golang v1.4.2/go.mod h1:JGt0RsEwEX+Xa/agj90YJ9d9DH2b7upDZMK9HRbFvCA= github.com/eclipse/paho.mqtt.golang v1.4.3/go.mod h1:CSYvoAlsMkhYOXh/oKyxa8EcBci6dVkLCbo5tTC1RIE=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/eclipse/paho.mqtt.golang v1.5.1 h1:/VSOv3oDLlpqR2Epjn1Q7b2bSTplJIeV2ISgCl2W7nE=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/eclipse/paho.mqtt.golang v1.5.1/go.mod h1:1/yJCneuyOoCOzKSsOTUc0AJfpsItBGWvYpBLimhArU=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY=
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY=
github.com/go-kit/log v0.2.0/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBjv0=
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
github.com/go-logfmt/logfmt v0.5.1/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw=
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/jessevdk/go-flags v1.5.0 h1:1jKYvbxEjfUl0fmqTCOfonvskHHXMjBySTLW4y9LFvc= github.com/jessevdk/go-flags v1.5.0 h1:1jKYvbxEjfUl0fmqTCOfonvskHHXMjBySTLW4y9LFvc=
github.com/jessevdk/go-flags v1.5.0/go.mod h1:Fw0T6WPc1dYxT4mKEZRfG5kJhaTDP9pj1c2EWnYs/m4= github.com/jessevdk/go-flags v1.5.0/go.mod h1:Fw0T6WPc1dYxT4mKEZRfG5kJhaTDP9pj1c2EWnYs/m4=
github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= github.com/jessevdk/go-flags v1.6.1 h1:Cvu5U8UGrLay1rZfv/zP7iLpSHGUZ/Ou68T0iX1bBK4=
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/jessevdk/go-flags v1.6.1/go.mod h1:Mk8T1hIAWpOiJiHa9rJASDK2UGWji0EuPGBnNLMooyc=
github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= github.com/prometheus/client_golang v1.19.0 h1:ygXvpU1AoN1MhdzckN+PyD9QJOSD4x7kmXYlnfbA6JU=
github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= github.com/prometheus/client_golang v1.19.0/go.mod h1:ZRM9uEAypZakd+q/x7+gmsvXdURP+DABIEIjnmDdp+k=
github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o=
github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0= github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg=
github.com/prometheus/client_golang v1.12.1/go.mod h1:3Z9XVyYiZYEO+YQWt3RD2R3jrbd179Rt297l4aS6nDY= github.com/prometheus/client_model v0.6.0 h1:k1v3CzpSRUTrKMppY35TLwPvxHqBu0bYgxZzqGIgaos=
github.com/prometheus/client_golang v1.14.0 h1:nJdhIvne2eSX/XRAFV9PcvFFRbrjbcTUj0VP62TMhnw= github.com/prometheus/client_model v0.6.0/go.mod h1:NTQHnmxFpouOD0DpvP4XujX3CdOAGQPoaGhyTchlyt8=
github.com/prometheus/client_golang v1.14.0/go.mod h1:8vpkKitgIVNcqrRBWh1C4TIUQgYNtG/XQE4E/Zae36Y= github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/common v0.50.0 h1:YSZE6aa9+luNa2da6/Tik0q0A5AbR+U003TItK57CPQ=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/common v0.50.0/go.mod h1:wHFBCEVWVmHMUpg7pYcOm2QUR/ocQdYSJVQJKnHc3xQ=
github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTUGI4=
github.com/prometheus/client_model v0.3.0 h1:UBgGFHqYdG/TPFD1B1ogZywDqEkwp3fBMvqdiQ7Xew4= github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw=
github.com/prometheus/client_model v0.3.0/go.mod h1:LDGWKZIo7rky3hgvBe+caln+Dr3dPggB5dvjtD7w9+w= github.com/prometheus/procfs v0.13.0 h1:GqzLlQyfsPbaEHaQkO7tbDlriv/4o5Hudv6OXHGKX7o=
github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= github.com/prometheus/procfs v0.13.0/go.mod h1:cd4PFCR54QLnGKPaKGA6l+cfuNXtht43ZKY6tow0Y1g=
github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws=
github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc= github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw=
github.com/prometheus/common v0.32.1/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls= github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
github.com/prometheus/common v0.37.0 h1:ccBbHCgIiT9uSoFY0vX8H3zsNR5eLt17/RQLUvn8pXE= github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
github.com/prometheus/common v0.37.0/go.mod h1:phzohg0JFMnBEFGxTDbfu3QyL5GI8gTQJFhYO5B3mfA= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w=
github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g=
github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
github.com/prometheus/procfs v0.8.0 h1:ODq8ZFEaYeCaZOJlZZdJA2AbQR98dSHSM1KW/You5mo=
github.com/prometheus/procfs v0.8.0/go.mod h1:z7EfXMXOkbkqb9IINtpCn86r/to3BnA0uaxHdg830/4=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0=
github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8= github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8=
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07 h1:UyzmZLoiDWMRywV4DUYb9Fbt8uiOSooupjTq10vpvnU= github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07 h1:UyzmZLoiDWMRywV4DUYb9Fbt8uiOSooupjTq10vpvnU=
github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07/go.mod h1:kDXzergiv9cbyO7IOYJZWg1U88JhDg3PB6klq9Hg2pA= github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07/go.mod h1:kDXzergiv9cbyO7IOYJZWg1U88JhDg3PB6klq9Hg2pA=
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8=
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= golang.org/x/net v0.22.0 h1:9sGLhx7iRIHEiX0oAJ3MRZMUCElJgy7Br1nO+AMN3Tc=
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60=
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM=
go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ=
go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=
golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=
golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200425230154-ff2c4b7c35a0/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/net v0.0.0-20220225172249-27dd8689420f h1:oA4XRj0qtSt8Yo1Zms0CUlsT3KG69V2UGQWPBxujDmc=
golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f h1:Ax0t5p6N38Ga0dThY21weqDEyz2oklo4IvDkpigvkD8=
golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 h1:0A+M6Uqn+Eje4kHMK80dtF3JCXC4ykBgQG4Fe06QRhQ=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=
golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM=
google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA=
google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U=
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA=
google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60=
google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=
google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w=
google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=

View File

@@ -1,7 +1,7 @@
package mk2core package mk2core
import ( import (
"github.com/diebietse/invertergui/mk2driver" "invertergui/mk2driver"
) )
type Core struct { type Core struct {

View File

@@ -4,6 +4,7 @@ import (
"errors" "errors"
"fmt" "fmt"
"io" "io"
"math"
"sync" "sync"
"time" "time"
@@ -41,20 +42,33 @@ const (
) )
const ( const (
infoFrameHeader = 0x20 infoFrameHeader = 0x20
frameHeader = 0xff frameHeader = 0xff
bootupFrameHeader = 0x0
) )
const ( const (
acL1InfoFrame = 0x08 acL1InfoFrame = 0x08
dcInfoFrame = 0x0C dcInfoFrame = 0x0C
interfaceFrame = 0x48 // H
setTargetFrame = 0x41 setTargetFrame = 0x41
infoReqFrame = 0x46 //F infoReqFrame = 0x46 //F
ledFrame = 0x4C ledFrame = 0x4C
stateFrame = 0x53 // S
vFrame = 0x56 vFrame = 0x56
winmonFrame = 0x57 winmonFrame = 0x57
) )
const (
panelStateVariant2Flags = 0x80
interfacePanelDetectFlag = 0x01
interfaceStandbyFlag = 0x02
panelCurrentLimitUnknown = 0x8000
panelCurrentLimitMax = 0x7FFF
)
// info frame types // info frame types
const ( const (
infoReqAddrDC = 0x00 infoReqAddrDC = 0x00
@@ -64,12 +78,21 @@ const (
// winmon frame commands // winmon frame commands
const ( const (
commandReadRAMVar = 0x30 commandReadRAMVar = 0x30
commandWriteRAMVar = 0x32
commandWriteSetting = 0x33
commandWriteData = 0x34
commandGetRAMVarInfo = 0x36 commandGetRAMVarInfo = 0x36
commandReadRAMResponse = 0x85 commandUnsupportedResponse = 0x80
commandGetRAMVarInfoResponse = 0x8E commandReadRAMResponse = 0x85
commandWriteRAMResponse = 0x87
commandWriteSettingResponse = 0x88
commandWriteNotAllowedResponse = 0x9B
commandGetRAMVarInfoResponse = 0x8E
) )
const writeResponseTimeout = 3 * time.Second
type mk2Ser struct { type mk2Ser struct {
info *Mk2Info info *Mk2Info
p io.ReadWriter p io.ReadWriter
@@ -78,6 +101,10 @@ type mk2Ser struct {
run chan struct{} run chan struct{}
frameLock bool frameLock bool
infochan chan *Mk2Info infochan chan *Mk2Info
commandMu sync.Mutex
writeAck chan byte
stateAck chan struct{}
ifaceAck chan byte
wg sync.WaitGroup wg sync.WaitGroup
} }
@@ -88,6 +115,9 @@ func NewMk2Connection(dev io.ReadWriter) (Mk2, error) {
mk2.scaleCount = 0 mk2.scaleCount = 0
mk2.frameLock = false mk2.frameLock = false
mk2.scales = make([]scaling, 0, ramVarMaxOffset) mk2.scales = make([]scaling, 0, ramVarMaxOffset)
mk2.writeAck = make(chan byte, 4)
mk2.stateAck = make(chan struct{}, 1)
mk2.ifaceAck = make(chan byte, 1)
mk2.setTarget() mk2.setTarget()
mk2.run = make(chan struct{}) mk2.run = make(chan struct{})
mk2.infochan = make(chan *Mk2Info) mk2.infochan = make(chan *Mk2Info)
@@ -152,6 +182,233 @@ func (m *mk2Ser) C() chan *Mk2Info {
return m.infochan return m.infochan
} }
func (m *mk2Ser) WriteRAMVar(id uint16, value int16) error {
return m.writeByID(commandWriteRAMVar, commandWriteRAMResponse, id, value)
}
func (m *mk2Ser) WriteSetting(id uint16, value int16) error {
return m.writeByID(commandWriteSetting, commandWriteSettingResponse, id, value)
}
func (m *mk2Ser) SetPanelState(switchState PanelSwitchState, currentLimitA *float64) error {
if !validPanelSwitchState(switchState) {
return fmt.Errorf("invalid panel switch state: %d", switchState)
}
currentLimitRaw, err := encodePanelCurrentLimit(currentLimitA)
if err != nil {
return err
}
m.commandMu.Lock()
defer m.commandMu.Unlock()
m.clearStateResponses()
m.sendCommandLocked([]byte{
stateFrame,
byte(switchState),
byte(currentLimitRaw),
byte(currentLimitRaw >> 8),
0x01,
panelStateVariant2Flags,
})
return m.waitForStateResponse()
}
func (m *mk2Ser) SetStandby(enabled bool) error {
lineState := byte(interfacePanelDetectFlag)
if enabled {
lineState |= interfaceStandbyFlag
}
m.commandMu.Lock()
defer m.commandMu.Unlock()
m.clearInterfaceResponses()
m.sendCommandLocked([]byte{
interfaceFrame,
lineState,
})
return m.waitForInterfaceResponse(enabled)
}
func validPanelSwitchState(switchState PanelSwitchState) bool {
switch switchState {
case PanelSwitchChargerOnly, PanelSwitchInverterOnly, PanelSwitchOn, PanelSwitchOff:
return true
default:
return false
}
}
func encodePanelCurrentLimit(currentLimitA *float64) (uint16, error) {
if currentLimitA == nil {
return panelCurrentLimitUnknown, nil
}
if *currentLimitA < 0 {
return 0, fmt.Errorf("current_limit must be >= 0, got %.3f", *currentLimitA)
}
raw := math.Round(*currentLimitA * 10)
if raw > panelCurrentLimitMax {
return 0, fmt.Errorf("current_limit %.3f A is above MK2 maximum %.1f A", *currentLimitA, panelCurrentLimitMax/10.0)
}
return uint16(raw), nil
}
func (m *mk2Ser) writeByID(selectCommand, expectedResponse byte, id uint16, value int16) error {
m.commandMu.Lock()
defer m.commandMu.Unlock()
m.clearWriteResponses()
m.sendCommandLocked([]byte{
winmonFrame,
selectCommand,
byte(id),
byte(id >> 8),
})
rawValue := uint16(value)
m.sendCommandLocked([]byte{
winmonFrame,
commandWriteData,
byte(rawValue),
byte(rawValue >> 8),
})
return m.waitForWriteResponse(expectedResponse)
}
func (m *mk2Ser) clearWriteResponses() {
if m.writeAck == nil {
m.writeAck = make(chan byte, 4)
return
}
for {
select {
case <-m.writeAck:
default:
return
}
}
}
func (m *mk2Ser) waitForWriteResponse(expectedResponse byte) error {
if m.writeAck == nil {
return errors.New("write response channel is not initialized")
}
select {
case response := <-m.writeAck:
switch response {
case expectedResponse:
return nil
case commandUnsupportedResponse:
return errors.New("write command is not supported by this device firmware")
case commandWriteNotAllowedResponse:
return errors.New("write command rejected by device access level")
default:
return fmt.Errorf("unexpected write response 0x%02x", response)
}
case <-time.After(writeResponseTimeout):
return fmt.Errorf("timed out waiting for write response after %s", writeResponseTimeout)
}
}
func (m *mk2Ser) pushWriteResponse(response byte) {
if m.writeAck == nil {
return
}
select {
case m.writeAck <- response:
default:
}
}
func (m *mk2Ser) clearStateResponses() {
if m.stateAck == nil {
m.stateAck = make(chan struct{}, 1)
return
}
for {
select {
case <-m.stateAck:
default:
return
}
}
}
func (m *mk2Ser) waitForStateResponse() error {
if m.stateAck == nil {
return errors.New("panel state response channel is not initialized")
}
select {
case <-m.stateAck:
return nil
case <-time.After(writeResponseTimeout):
return fmt.Errorf("timed out waiting for panel state response after %s", writeResponseTimeout)
}
}
func (m *mk2Ser) pushStateResponse() {
if m.stateAck == nil {
return
}
select {
case m.stateAck <- struct{}{}:
default:
}
}
func (m *mk2Ser) clearInterfaceResponses() {
if m.ifaceAck == nil {
m.ifaceAck = make(chan byte, 1)
return
}
for {
select {
case <-m.ifaceAck:
default:
return
}
}
}
func (m *mk2Ser) waitForInterfaceResponse(expectedStandby bool) error {
if m.ifaceAck == nil {
return errors.New("interface response channel is not initialized")
}
select {
case lineState := <-m.ifaceAck:
standbyEnabled := lineState&interfaceStandbyFlag != 0
if standbyEnabled != expectedStandby {
return fmt.Errorf("unexpected standby line state 0x%02x", lineState)
}
return nil
case <-time.After(writeResponseTimeout):
return fmt.Errorf("timed out waiting for standby response after %s", writeResponseTimeout)
}
}
func (m *mk2Ser) pushInterfaceResponse(lineState byte) {
if m.ifaceAck == nil {
return
}
select {
case m.ifaceAck <- lineState:
default:
}
}
func (m *mk2Ser) readByte() byte { func (m *mk2Ser) readByte() byte {
buffer := make([]byte, 1) buffer := make([]byte, 1)
_, err := io.ReadFull(m.p, buffer) _, err := io.ReadFull(m.p, buffer)
@@ -164,6 +421,7 @@ func (m *mk2Ser) readByte() byte {
// Adds error to error slice. // Adds error to error slice.
func (m *mk2Ser) addError(err error) { func (m *mk2Ser) addError(err error) {
logrus.Errorf("Mk2 serial slice error: %q", err.Error())
if m.info.Errors == nil { if m.info.Errors == nil {
m.info.Errors = make([]error, 0) m.info.Errors = make([]error, 0)
} }
@@ -183,11 +441,19 @@ func (m *mk2Ser) updateReport() {
// Checks for valid frame and chooses decoding. // Checks for valid frame and chooses decoding.
func (m *mk2Ser) handleFrame(l byte, frame []byte) { func (m *mk2Ser) handleFrame(l byte, frame []byte) {
logrus.Debugf("frame %#v", frame) logrus.Debugf("[handleFrame] frame %#v", frame)
if checkChecksum(l, frame[0], frame[1:]) { if checkChecksum(l, frame[0], frame[1:]) {
switch frame[0] { switch frame[0] {
case bootupFrameHeader:
m.setTarget()
case frameHeader: case frameHeader:
switch frame[1] { switch frame[1] {
case interfaceFrame:
if len(frame) > 2 {
m.pushInterfaceResponse(frame[2])
}
case stateFrame:
m.pushStateResponse()
case vFrame: case vFrame:
m.versionDecode(frame[2:]) m.versionDecode(frame[2:])
case winmonFrame: case winmonFrame:
@@ -196,10 +462,16 @@ func (m *mk2Ser) handleFrame(l byte, frame []byte) {
m.scaleDecode(frame[2:]) m.scaleDecode(frame[2:])
case commandReadRAMResponse: case commandReadRAMResponse:
m.stateDecode(frame[2:]) m.stateDecode(frame[2:])
case commandWriteRAMResponse, commandWriteSettingResponse, commandUnsupportedResponse, commandWriteNotAllowedResponse:
m.pushWriteResponse(frame[2])
default:
logrus.Warnf("[handleFrame] invalid winmonFrame %v", frame[2:])
} }
case ledFrame: case ledFrame:
m.ledDecode(frame[2:]) m.ledDecode(frame[2:])
default:
logrus.Warnf("[handleFrame] invalid frameHeader %v", frame[1])
} }
case infoFrameHeader: case infoFrameHeader:
@@ -208,10 +480,14 @@ func (m *mk2Ser) handleFrame(l byte, frame []byte) {
m.dcDecode(frame[1:]) m.dcDecode(frame[1:])
case acL1InfoFrame: case acL1InfoFrame:
m.acDecode(frame[1:]) m.acDecode(frame[1:])
default:
logrus.Warnf("[handleFrame] invalid infoFrameHeader %v", frame[5])
} }
default:
logrus.Warnf("[handleFrame] Invalid frame %v", frame[0])
} }
} else { } else {
logrus.Errorf("Invalid incoming frame checksum: %x", frame) logrus.Errorf("[handleFrame] Invalid incoming frame checksum: %x", frame)
m.frameLock = false m.frameLock = false
} }
} }
@@ -418,6 +694,12 @@ func getLEDs(ledsOn, ledsBlink byte) map[Led]LEDstate {
// Adds header and trailing crc for frame to send. // Adds header and trailing crc for frame to send.
func (m *mk2Ser) sendCommand(data []byte) { func (m *mk2Ser) sendCommand(data []byte) {
m.commandMu.Lock()
defer m.commandMu.Unlock()
m.sendCommandLocked(data)
}
func (m *mk2Ser) sendCommandLocked(data []byte) {
l := len(data) l := len(data)
dataOut := make([]byte, l+3) dataOut := make([]byte, l+3)
dataOut[0] = byte(l + 1) dataOut[0] = byte(l + 1)

View File

@@ -4,6 +4,7 @@ import (
"bytes" "bytes"
"io" "io"
"testing" "testing"
"time"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
@@ -42,6 +43,7 @@ type testIo struct {
} }
func NewIOStub(readBuffer []byte) io.ReadWriter { func NewIOStub(readBuffer []byte) io.ReadWriter {
writeBuffer = bytes.NewBuffer(nil)
return &testIo{ return &testIo{
Reader: bytes.NewBuffer(readBuffer), Reader: bytes.NewBuffer(readBuffer),
Writer: writeBuffer, Writer: writeBuffer,
@@ -309,3 +311,127 @@ func Test_mk2Ser_calcFreq(t *testing.T) {
}) })
} }
} }
func Test_mk2Ser_WriteSetting(t *testing.T) {
testIO := NewIOStub(nil)
m := &mk2Ser{
p: testIO,
writeAck: make(chan byte, 1),
}
go func() {
time.Sleep(10 * time.Millisecond)
m.pushWriteResponse(commandWriteSettingResponse)
}()
err := m.WriteSetting(0x1234, 1234)
assert.NoError(t, err)
expected := []byte{
0x05, 0xff, 0x57, 0x33, 0x34, 0x12, 0x2c,
0x05, 0xff, 0x57, 0x34, 0xd2, 0x04, 0x9b,
}
assert.Equal(t, expected, writeBuffer.Bytes())
}
func Test_mk2Ser_WriteRAMVarRejected(t *testing.T) {
testIO := NewIOStub(nil)
m := &mk2Ser{
p: testIO,
writeAck: make(chan byte, 1),
}
go func() {
time.Sleep(10 * time.Millisecond)
m.pushWriteResponse(commandWriteNotAllowedResponse)
}()
err := m.WriteRAMVar(0x000d, 1)
assert.Error(t, err)
assert.ErrorContains(t, err, "rejected")
}
func Test_mk2Ser_SetPanelState(t *testing.T) {
testIO := NewIOStub(nil)
m := &mk2Ser{
p: testIO,
stateAck: make(chan struct{}, 1),
}
go func() {
time.Sleep(10 * time.Millisecond)
m.pushStateResponse()
}()
currentLimit := 16.5
err := m.SetPanelState(PanelSwitchOn, &currentLimit)
assert.NoError(t, err)
expected := []byte{
0x07, 0xff, 0x53, 0x03, 0xa5, 0x00, 0x01, 0x80, 0x7e,
}
assert.Equal(t, expected, writeBuffer.Bytes())
}
func Test_mk2Ser_SetPanelState_SwitchOnly(t *testing.T) {
testIO := NewIOStub(nil)
m := &mk2Ser{
p: testIO,
stateAck: make(chan struct{}, 1),
}
go func() {
time.Sleep(10 * time.Millisecond)
m.pushStateResponse()
}()
err := m.SetPanelState(PanelSwitchOff, nil)
assert.NoError(t, err)
expected := []byte{
0x07, 0xff, 0x53, 0x04, 0x00, 0x80, 0x01, 0x80, 0xa2,
}
assert.Equal(t, expected, writeBuffer.Bytes())
}
func Test_mk2Ser_SetStandby(t *testing.T) {
testIO := NewIOStub(nil)
m := &mk2Ser{
p: testIO,
ifaceAck: make(chan byte, 1),
}
go func() {
time.Sleep(10 * time.Millisecond)
m.pushInterfaceResponse(interfacePanelDetectFlag | interfaceStandbyFlag)
}()
err := m.SetStandby(true)
assert.NoError(t, err)
expected := []byte{
0x03, 0xff, 0x48, 0x03, 0xb3,
}
assert.Equal(t, expected, writeBuffer.Bytes())
}
func Test_mk2Ser_SetStandby_Disabled(t *testing.T) {
testIO := NewIOStub(nil)
m := &mk2Ser{
p: testIO,
ifaceAck: make(chan byte, 1),
}
go func() {
time.Sleep(10 * time.Millisecond)
m.pushInterfaceResponse(interfacePanelDetectFlag)
}()
err := m.SetStandby(false)
assert.NoError(t, err)
expected := []byte{
0x03, 0xff, 0x48, 0x01, 0xb5,
}
assert.Equal(t, expected, writeBuffer.Bytes())
}

View File

@@ -76,3 +76,29 @@ type Mk2 interface {
C() chan *Mk2Info C() chan *Mk2Info
Close() Close()
} }
type PanelSwitchState byte
const (
// PanelSwitchChargerOnly enables charging only.
PanelSwitchChargerOnly PanelSwitchState = 0x01
// PanelSwitchInverterOnly enables inverter output and disables charging.
PanelSwitchInverterOnly PanelSwitchState = 0x02
// PanelSwitchOn enables both inverter and charger.
PanelSwitchOn PanelSwitchState = 0x03
// PanelSwitchOff disables inverter and charger.
PanelSwitchOff PanelSwitchState = 0x04
)
type SettingsWriter interface {
// WriteRAMVar writes a signed 16-bit value to a RAM variable id.
WriteRAMVar(id uint16, value int16) error
// WriteSetting writes a signed 16-bit value to a setting id.
WriteSetting(id uint16, value int16) error
// SetPanelState sends the MK2 "S" command using a virtual panel switch state.
// If currentLimitA is nil, the command does not update the AC current limit.
SetPanelState(switchState PanelSwitchState, currentLimitA *float64) error
// SetStandby configures the remote panel standby line.
// When enabled, the inverter is prevented from sleeping while switched off.
SetStandby(enabled bool) error
}

View File

@@ -37,6 +37,22 @@ func (m *mock) Close() {
} }
func (m *mock) WriteRAMVar(_ uint16, _ int16) error {
return nil
}
func (m *mock) WriteSetting(_ uint16, _ int16) error {
return nil
}
func (m *mock) SetPanelState(_ PanelSwitchState, _ *float64) error {
return nil
}
func (m *mock) SetStandby(_ bool) error {
return nil
}
func (m *mock) genMockValues() { func (m *mock) genMockValues() {
mult := 1.0 mult := 1.0
ledState := LedOff ledState := LedOff

View File

@@ -1,7 +1,7 @@
package cli package cli
import ( import (
"github.com/diebietse/invertergui/mk2driver" "invertergui/mk2driver"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
) )

View File

@@ -2,9 +2,14 @@ package mqttclient
import ( import (
"encoding/json" "encoding/json"
"errors"
"fmt"
"strconv"
"strings"
"sync"
"time" "time"
"github.com/diebietse/invertergui/mk2driver" "invertergui/mk2driver"
mqtt "github.com/eclipse/paho.mqtt.golang" mqtt "github.com/eclipse/paho.mqtt.golang"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
) )
@@ -13,42 +18,758 @@ var log = logrus.WithField("ctx", "inverter-gui-mqtt")
const keepAlive = 5 * time.Second const keepAlive = 5 * time.Second
// Config sets MQTT client configuration const (
type Config struct { commandKindSetting = "setting"
Broker string commandKindRAMVar = "ram_var"
ClientID string commandKindPanel = "panel_state"
Topic string commandKindStandby = "standby"
Username string
Password string writeStatusOK = "ok"
writeStatusError = "error"
)
type HomeAssistantConfig struct {
Enabled bool
DiscoveryPrefix string
NodeID string
DeviceName string
} }
// New creates an MQTT client that starts publishing MK2 data as it is received. // Config sets MQTT client configuration
func New(mk2 mk2driver.Mk2, config Config) error { type Config struct {
Broker string
ClientID string
Topic string
CommandTopic string
StatusTopic string
HomeAssistant HomeAssistantConfig
Username string
Password string
}
type writeCommand struct {
RequestID string
Kind string
ID uint16
Value int16
HasSwitch bool
SwitchState mk2driver.PanelSwitchState
SwitchName string
CurrentLimitA *float64
Standby *bool
}
type writeCommandPayload struct {
RequestID string `json:"request_id"`
Kind string `json:"kind"`
Type string `json:"type"`
ID *uint16 `json:"id"`
Value json.RawMessage `json:"value"`
Switch string `json:"switch"`
SwitchState string `json:"switch_state"`
CurrentLimitA *float64 `json:"current_limit"`
Standby *bool `json:"standby"`
}
type writeStatus struct {
RequestID string `json:"request_id,omitempty"`
Status string `json:"status"`
Kind string `json:"kind,omitempty"`
ID uint16 `json:"id"`
Value int16 `json:"value"`
Switch string `json:"switch,omitempty"`
CurrentLimitA *float64 `json:"current_limit,omitempty"`
Standby *bool `json:"standby,omitempty"`
Error string `json:"error,omitempty"`
Timestamp time.Time `json:"timestamp"`
}
type haDiscoveryDefinition struct {
Component string
ObjectID string
Config map[string]any
}
type panelStateCache struct {
mu sync.Mutex
hasSwitch bool
switchName string
switchState mk2driver.PanelSwitchState
}
// New creates an MQTT client that publishes MK2 updates and optionally handles setting write commands.
func New(mk2 mk2driver.Mk2, writer mk2driver.SettingsWriter, config Config) error {
c := mqtt.NewClient(getOpts(config)) c := mqtt.NewClient(getOpts(config))
if token := c.Connect(); token.Wait() && token.Error() != nil { if token := c.Connect(); token.Wait() && token.Error() != nil {
return token.Error() return token.Error()
} }
cache := &panelStateCache{}
if config.HomeAssistant.Enabled {
if err := publishHAAvailability(c, config, "online"); err != nil {
return fmt.Errorf("could not publish Home Assistant availability: %w", err)
}
if err := publishHADiscovery(c, config); err != nil {
return fmt.Errorf("could not publish Home Assistant discovery payloads: %w", err)
}
if writer != nil {
if err := subscribeHAPanelModeState(c, config, cache); err != nil {
log.Warnf("Could not subscribe to Home Assistant panel mode state topic: %v", err)
}
}
}
if config.CommandTopic != "" {
if writer == nil {
log.Warnf("MQTT command topic %q configured, but no settings writer is available", config.CommandTopic)
} else {
t := c.Subscribe(config.CommandTopic, 1, commandHandler(c, writer, config, cache))
t.Wait()
if t.Error() != nil {
return fmt.Errorf("could not subscribe to MQTT command topic %q: %w", config.CommandTopic, t.Error())
}
log.Infof("Subscribed to MQTT command topic: %s", config.CommandTopic)
}
}
go func() { go func() {
for e := range mk2.C() { for e := range mk2.C() {
if e.Valid { if e == nil || !e.Valid {
data, err := json.Marshal(e) continue
if err != nil { }
log.Errorf("Could not parse data source: %v", err) if err := publishJSON(c, config.Topic, e, 0, false); err != nil {
continue log.Errorf("Could not publish update to MQTT topic %q: %v", config.Topic, err)
}
t := c.Publish(config.Topic, 0, false, data)
t.Wait()
if t.Error() != nil {
log.Errorf("Could not publish data: %v", t.Error())
}
} }
} }
}() }()
return nil return nil
} }
func subscribeHAPanelModeState(client mqtt.Client, config Config, cache *panelStateCache) error {
if cache == nil {
return nil
}
stateTopic := haPanelSwitchStateTopic(config)
t := client.Subscribe(stateTopic, 1, func(_ mqtt.Client, msg mqtt.Message) {
switchState, switchName, err := normalizePanelSwitch(string(msg.Payload()))
if err != nil {
return
}
cache.remember(writeCommand{
Kind: commandKindPanel,
HasSwitch: true,
SwitchState: switchState,
SwitchName: switchName,
})
})
t.Wait()
return t.Error()
}
func commandHandler(client mqtt.Client, writer mk2driver.SettingsWriter, config Config, cache *panelStateCache) mqtt.MessageHandler {
if cache == nil {
cache = &panelStateCache{}
}
return func(_ mqtt.Client, msg mqtt.Message) {
cmd, err := decodeWriteCommand(msg.Payload())
if err != nil {
log.Errorf("Invalid MQTT write command payload from topic %q: %v", msg.Topic(), err)
publishWriteStatus(client, config.StatusTopic, writeStatus{
Status: writeStatusError,
Error: err.Error(),
Timestamp: time.Now().UTC(),
})
return
}
execCmd := cmd
status := writeStatus{
RequestID: cmd.RequestID,
Status: writeStatusOK,
Kind: cmd.Kind,
Timestamp: time.Now().UTC(),
}
switch cmd.Kind {
case commandKindPanel:
execCmd, err = cache.resolvePanelCommand(cmd)
if err != nil {
status.Status = writeStatusError
status.Error = err.Error()
log.Errorf("Invalid MQTT write command %s: %v", formatWriteCommandLog(cmd), err)
publishWriteStatus(client, config.StatusTopic, status)
return
}
status.Switch = execCmd.SwitchName
status.CurrentLimitA = execCmd.CurrentLimitA
case commandKindStandby:
status.Standby = copyBoolPtr(execCmd.Standby)
default:
status.ID = cmd.ID
status.Value = cmd.Value
}
if err := executeWriteCommand(writer, execCmd); err != nil {
status.Status = writeStatusError
status.Error = err.Error()
log.Errorf("Failed MQTT write command %s: %v", formatWriteCommandLog(execCmd), err)
} else {
log.Infof("Applied MQTT write command %s", formatWriteCommandLog(execCmd))
cache.remember(execCmd)
if config.HomeAssistant.Enabled {
if err := publishHAControlState(client, config, execCmd); err != nil {
log.Errorf("Could not publish Home Assistant control state update: %v", err)
}
}
}
publishWriteStatus(client, config.StatusTopic, status)
}
}
func (c *panelStateCache) resolvePanelCommand(cmd writeCommand) (writeCommand, error) {
if cmd.Kind != commandKindPanel {
return cmd, nil
}
if cmd.HasSwitch {
return cmd, nil
}
c.mu.Lock()
defer c.mu.Unlock()
if !c.hasSwitch {
return writeCommand{}, errors.New("panel_state command missing switch and no prior mode is known; set mode once first")
}
cmd.HasSwitch = true
cmd.SwitchName = c.switchName
cmd.SwitchState = c.switchState
return cmd, nil
}
func (c *panelStateCache) remember(cmd writeCommand) {
if cmd.Kind != commandKindPanel || !cmd.HasSwitch {
return
}
c.mu.Lock()
c.hasSwitch = true
c.switchName = cmd.SwitchName
c.switchState = cmd.SwitchState
c.mu.Unlock()
}
func decodeWriteCommand(payload []byte) (writeCommand, error) {
msg := writeCommandPayload{}
if err := json.Unmarshal(payload, &msg); err != nil {
return writeCommand{}, fmt.Errorf("invalid JSON payload: %w", err)
}
kind := msg.Kind
if kind == "" {
kind = msg.Type
}
normalizedKind, err := normalizeWriteKind(kind)
if err != nil {
return writeCommand{}, err
}
if normalizedKind == commandKindPanel {
switchName := msg.Switch
if switchName == "" {
switchName = msg.SwitchState
}
hasSwitch := false
switchState := mk2driver.PanelSwitchState(0)
normalizedSwitchName := ""
if switchName != "" {
var err error
switchState, normalizedSwitchName, err = normalizePanelSwitch(switchName)
if err != nil {
return writeCommand{}, err
}
hasSwitch = true
}
if msg.CurrentLimitA != nil && *msg.CurrentLimitA < 0 {
return writeCommand{}, fmt.Errorf("current_limit must be >= 0, got %.3f", *msg.CurrentLimitA)
}
if !hasSwitch && msg.CurrentLimitA == nil {
return writeCommand{}, errors.New(`missing required field "switch" (or "switch_state"), or "current_limit"`)
}
return writeCommand{
RequestID: msg.RequestID,
Kind: normalizedKind,
HasSwitch: hasSwitch,
SwitchState: switchState,
SwitchName: normalizedSwitchName,
CurrentLimitA: msg.CurrentLimitA,
}, nil
}
if normalizedKind == commandKindStandby {
standby, err := decodeStandbyValue(msg)
if err != nil {
return writeCommand{}, err
}
return writeCommand{
RequestID: msg.RequestID,
Kind: normalizedKind,
Standby: &standby,
}, nil
}
if msg.ID == nil {
return writeCommand{}, errors.New(`missing required field "id"`)
}
value, err := decodeInt16Value(msg.Value)
if err != nil {
return writeCommand{}, err
}
return writeCommand{
RequestID: msg.RequestID,
Kind: normalizedKind,
ID: *msg.ID,
Value: value,
}, nil
}
func normalizeWriteKind(raw string) (string, error) {
switch strings.ToLower(strings.TrimSpace(raw)) {
case "setting", "settings":
return commandKindSetting, nil
case "ram", "ramvar", "ram_var", "ram-variable", "ramvariable":
return commandKindRAMVar, nil
case "panel", "panel_state", "switch", "remote_panel":
return commandKindPanel, nil
case "standby", "panel_standby", "remote_panel_standby":
return commandKindStandby, nil
default:
return "", fmt.Errorf("unsupported write command kind %q", raw)
}
}
func normalizePanelSwitch(raw string) (mk2driver.PanelSwitchState, string, error) {
switch strings.ToLower(strings.TrimSpace(raw)) {
case "1", "charger_only", "charger-only", "charger":
return mk2driver.PanelSwitchChargerOnly, "charger_only", nil
case "2", "inverter_only", "inverter-only", "inverter":
return mk2driver.PanelSwitchInverterOnly, "inverter_only", nil
case "3", "on":
return mk2driver.PanelSwitchOn, "on", nil
case "4", "off":
return mk2driver.PanelSwitchOff, "off", nil
default:
return 0, "", fmt.Errorf("unsupported panel switch %q", raw)
}
}
func executeWriteCommand(writer mk2driver.SettingsWriter, cmd writeCommand) error {
if writer == nil {
return errors.New("settings writer is not available")
}
switch cmd.Kind {
case commandKindPanel:
if !cmd.HasSwitch {
return errors.New("panel_state command requires a switch state")
}
return writer.SetPanelState(cmd.SwitchState, cmd.CurrentLimitA)
case commandKindStandby:
if cmd.Standby == nil {
return errors.New("standby command missing standby value")
}
return writer.SetStandby(*cmd.Standby)
case commandKindRAMVar:
return writer.WriteRAMVar(cmd.ID, cmd.Value)
case commandKindSetting:
return writer.WriteSetting(cmd.ID, cmd.Value)
default:
return fmt.Errorf("unsupported write command kind %q", cmd.Kind)
}
}
func formatWriteCommandLog(cmd writeCommand) string {
switch cmd.Kind {
case commandKindPanel:
switchName := cmd.SwitchName
if switchName == "" {
switchName = "<cached>"
}
if cmd.CurrentLimitA == nil {
return fmt.Sprintf("kind=%s switch=%s", cmd.Kind, switchName)
}
return fmt.Sprintf("kind=%s switch=%s current_limit=%.3f", cmd.Kind, switchName, *cmd.CurrentLimitA)
case commandKindStandby:
if cmd.Standby == nil {
return fmt.Sprintf("kind=%s standby=<unset>", cmd.Kind)
}
return fmt.Sprintf("kind=%s standby=%t", cmd.Kind, *cmd.Standby)
default:
return fmt.Sprintf("kind=%s id=%d value=%d", cmd.Kind, cmd.ID, cmd.Value)
}
}
func decodeInt16Value(raw json.RawMessage) (int16, error) {
if len(raw) == 0 {
return 0, errors.New(`missing required field "value"`)
}
var value int16
if err := json.Unmarshal(raw, &value); err != nil {
return 0, fmt.Errorf(`field "value" must be a signed integer: %w`, err)
}
return value, nil
}
func decodeStandbyValue(msg writeCommandPayload) (bool, error) {
if msg.Standby != nil {
return *msg.Standby, nil
}
if len(msg.Value) == 0 {
return false, errors.New(`missing required field "standby" (or boolean "value")`)
}
var boolValue bool
if err := json.Unmarshal(msg.Value, &boolValue); err == nil {
return boolValue, nil
}
var stringValue string
if err := json.Unmarshal(msg.Value, &stringValue); err == nil {
switch strings.ToLower(strings.TrimSpace(stringValue)) {
case "1", "true", "on", "enable", "enabled":
return true, nil
case "0", "false", "off", "disable", "disabled":
return false, nil
}
}
var intValue int
if err := json.Unmarshal(msg.Value, &intValue); err == nil {
switch intValue {
case 1:
return true, nil
case 0:
return false, nil
}
}
return false, errors.New(`field "standby" must be true/false`)
}
func publishWriteStatus(client mqtt.Client, topic string, status writeStatus) {
if topic == "" {
return
}
if err := publishJSON(client, topic, status, 1, false); err != nil {
log.Errorf("Could not publish command status to MQTT topic %q: %v", topic, err)
}
}
func publishHADiscovery(client mqtt.Client, config Config) error {
definitions := buildHADiscoveryDefinitions(config)
prefix := haDiscoveryPrefix(config)
nodeID := haNodeID(config)
for _, def := range definitions {
topic := fmt.Sprintf("%s/%s/%s/%s/config", prefix, def.Component, nodeID, def.ObjectID)
if err := publishJSON(client, topic, def.Config, 1, true); err != nil {
return fmt.Errorf("could not publish discovery for %s/%s: %w", def.Component, def.ObjectID, err)
}
}
return nil
}
func buildHADiscoveryDefinitions(config Config) []haDiscoveryDefinition {
if !config.HomeAssistant.Enabled {
return nil
}
nodeID := haNodeID(config)
device := map[string]any{
"identifiers": []string{fmt.Sprintf("invertergui_%s", nodeID)},
"name": haDeviceName(config),
"manufacturer": "Victron Energy",
"model": "MultiPlus",
"sw_version": "invertergui",
}
availabilityTopic := haAvailabilityTopic(config)
stateTopic := config.Topic
sensors := []haDiscoveryDefinition{
buildHASensor(device, availabilityTopic, stateTopic, nodeID, "battery_voltage", "Battery Voltage", "{{ value_json.BatVoltage }}", "V", "voltage", "measurement"),
buildHASensor(device, availabilityTopic, stateTopic, nodeID, "battery_current", "Battery Current", "{{ value_json.BatCurrent }}", "A", "current", "measurement"),
buildHASensor(device, availabilityTopic, stateTopic, nodeID, "battery_charge", "Battery Charge", "{{ ((value_json.ChargeState | float(0)) * 100) | round(1) }}", "%", "battery", "measurement"),
buildHASensor(device, availabilityTopic, stateTopic, nodeID, "input_voltage", "Input Voltage", "{{ value_json.InVoltage }}", "V", "voltage", "measurement"),
buildHASensor(device, availabilityTopic, stateTopic, nodeID, "input_current", "Input Current", "{{ value_json.InCurrent }}", "A", "current", "measurement"),
buildHASensor(device, availabilityTopic, stateTopic, nodeID, "input_frequency", "Input Frequency", "{{ value_json.InFrequency }}", "Hz", "frequency", "measurement"),
buildHASensor(device, availabilityTopic, stateTopic, nodeID, "output_voltage", "Output Voltage", "{{ value_json.OutVoltage }}", "V", "voltage", "measurement"),
buildHASensor(device, availabilityTopic, stateTopic, nodeID, "output_current", "Output Current", "{{ value_json.OutCurrent }}", "A", "current", "measurement"),
buildHASensor(device, availabilityTopic, stateTopic, nodeID, "output_frequency", "Output Frequency", "{{ value_json.OutFrequency }}", "Hz", "frequency", "measurement"),
buildHASensor(device, availabilityTopic, stateTopic, nodeID, "input_power", "Input Power", "{{ ((value_json.InVoltage | float(0)) * (value_json.InCurrent | float(0))) | round(1) }}", "VA", "", "measurement"),
buildHASensor(device, availabilityTopic, stateTopic, nodeID, "output_power", "Output Power", "{{ ((value_json.OutVoltage | float(0)) * (value_json.OutCurrent | float(0))) | round(1) }}", "VA", "", "measurement"),
{
Component: "binary_sensor",
ObjectID: "data_valid",
Config: map[string]any{
"name": "Data Valid",
"unique_id": fmt.Sprintf("%s_data_valid", nodeID),
"state_topic": stateTopic,
"value_template": "{{ value_json.Valid }}",
"payload_on": "true",
"payload_off": "false",
"availability_topic": availabilityTopic,
"device": device,
"entity_category": "diagnostic",
},
},
}
if config.CommandTopic != "" {
sensors = append(sensors,
haDiscoveryDefinition{
Component: "select",
ObjectID: "remote_panel_mode",
Config: map[string]any{
"name": "Remote Panel Mode",
"unique_id": fmt.Sprintf("%s_remote_panel_mode", nodeID),
"state_topic": haPanelSwitchStateTopic(config),
"command_topic": config.CommandTopic,
"command_template": "{\"kind\":\"panel_state\",\"switch\":\"{{ value }}\"}",
"options": []string{"charger_only", "inverter_only", "on", "off"},
"availability_topic": availabilityTopic,
"device": device,
"icon": "mdi:transmission-tower-export",
},
},
haDiscoveryDefinition{
Component: "number",
ObjectID: "remote_panel_current_limit",
Config: map[string]any{
"name": "Remote Panel Current Limit",
"unique_id": fmt.Sprintf("%s_remote_panel_current_limit", nodeID),
"state_topic": haCurrentLimitStateTopic(config),
"command_topic": config.CommandTopic,
"command_template": "{\"kind\":\"panel_state\",\"current_limit\":{{ value | float(0) }}}",
"unit_of_measurement": "A",
"device_class": "current",
"mode": "box",
"min": 0,
"max": 100,
"step": 0.1,
"availability_topic": availabilityTopic,
"device": device,
"icon": "mdi:current-ac",
},
},
haDiscoveryDefinition{
Component: "switch",
ObjectID: "remote_panel_standby",
Config: map[string]any{
"name": "Remote Panel Standby",
"unique_id": fmt.Sprintf("%s_remote_panel_standby", nodeID),
"state_topic": haStandbyStateTopic(config),
"command_topic": config.CommandTopic,
"payload_on": "{\"kind\":\"standby\",\"standby\":true}",
"payload_off": "{\"kind\":\"standby\",\"standby\":false}",
"state_on": "ON",
"state_off": "OFF",
"availability_topic": availabilityTopic,
"device": device,
"icon": "mdi:power-sleep",
},
},
)
}
return sensors
}
func buildHASensor(device map[string]any, availabilityTopic, stateTopic, nodeID, objectID, name, valueTemplate, unit, deviceClass, stateClass string) haDiscoveryDefinition {
config := map[string]any{
"name": name,
"unique_id": fmt.Sprintf("%s_%s", nodeID, objectID),
"state_topic": stateTopic,
"value_template": valueTemplate,
"availability_topic": availabilityTopic,
"device": device,
}
if unit != "" {
config["unit_of_measurement"] = unit
}
if deviceClass != "" {
config["device_class"] = deviceClass
}
if stateClass != "" {
config["state_class"] = stateClass
}
return haDiscoveryDefinition{
Component: "sensor",
ObjectID: objectID,
Config: config,
}
}
func publishHAAvailability(client mqtt.Client, config Config, status string) error {
return publishString(client, haAvailabilityTopic(config), status, 1, true)
}
func publishHAControlState(client mqtt.Client, config Config, cmd writeCommand) error {
switch cmd.Kind {
case commandKindPanel:
if err := publishString(client, haPanelSwitchStateTopic(config), cmd.SwitchName, 1, true); err != nil {
return err
}
if cmd.CurrentLimitA != nil {
limit := strconv.FormatFloat(*cmd.CurrentLimitA, 'f', 1, 64)
if err := publishString(client, haCurrentLimitStateTopic(config), limit, 1, true); err != nil {
return err
}
}
case commandKindStandby:
if cmd.Standby == nil {
return nil
}
state := "OFF"
if *cmd.Standby {
state = "ON"
}
if err := publishString(client, haStandbyStateTopic(config), state, 1, true); err != nil {
return err
}
}
return nil
}
func publishJSON(client mqtt.Client, topic string, payload any, qos byte, retained bool) error {
if topic == "" {
return errors.New("topic is empty")
}
data, err := json.Marshal(payload)
if err != nil {
return fmt.Errorf("could not marshal payload: %w", err)
}
t := client.Publish(topic, qos, retained, data)
t.Wait()
if t.Error() != nil {
return t.Error()
}
return nil
}
func publishString(client mqtt.Client, topic, payload string, qos byte, retained bool) error {
if topic == "" {
return errors.New("topic is empty")
}
t := client.Publish(topic, qos, retained, payload)
t.Wait()
if t.Error() != nil {
return t.Error()
}
return nil
}
func mqttTopicRoot(topic string) string {
t := strings.Trim(strings.TrimSpace(topic), "/")
if t == "" {
return "invertergui"
}
if strings.HasSuffix(t, "/updates") {
root := strings.TrimSuffix(t, "/updates")
if root != "" {
return root
}
}
return t
}
func haAvailabilityTopic(config Config) string {
return fmt.Sprintf("%s/homeassistant/availability", mqttTopicRoot(config.Topic))
}
func haPanelSwitchStateTopic(config Config) string {
return fmt.Sprintf("%s/homeassistant/remote_panel_mode/state", mqttTopicRoot(config.Topic))
}
func haCurrentLimitStateTopic(config Config) string {
return fmt.Sprintf("%s/homeassistant/remote_panel_current_limit/state", mqttTopicRoot(config.Topic))
}
func haStandbyStateTopic(config Config) string {
return fmt.Sprintf("%s/homeassistant/remote_panel_standby/state", mqttTopicRoot(config.Topic))
}
func haDiscoveryPrefix(config Config) string {
prefix := strings.Trim(strings.TrimSpace(config.HomeAssistant.DiscoveryPrefix), "/")
if prefix == "" {
return "homeassistant"
}
return prefix
}
func haNodeID(config Config) string {
nodeID := normalizeID(config.HomeAssistant.NodeID)
if nodeID == "" {
nodeID = normalizeID(config.ClientID)
}
if nodeID == "" {
return "invertergui"
}
return nodeID
}
func haDeviceName(config Config) string {
name := strings.TrimSpace(config.HomeAssistant.DeviceName)
if name == "" {
return "Victron Inverter"
}
return name
}
func normalizeID(in string) string {
trimmed := strings.TrimSpace(strings.ToLower(in))
if trimmed == "" {
return ""
}
var b strings.Builder
lastUnderscore := false
for _, r := range trimmed {
valid := (r >= 'a' && r <= 'z') || (r >= '0' && r <= '9') || r == '-' || r == '_'
if valid {
b.WriteRune(r)
lastUnderscore = false
continue
}
if !lastUnderscore {
b.WriteRune('_')
lastUnderscore = true
}
}
return strings.Trim(b.String(), "_")
}
func copyBoolPtr(in *bool) *bool {
if in == nil {
return nil
}
value := *in
return &value
}
func getOpts(config Config) *mqtt.ClientOptions { func getOpts(config Config) *mqtt.ClientOptions {
opts := mqtt.NewClientOptions() opts := mqtt.NewClientOptions()
opts.AddBroker(config.Broker) opts.AddBroker(config.Broker)
@@ -60,14 +781,16 @@ func getOpts(config Config) *mqtt.ClientOptions {
if config.Password != "" { if config.Password != "" {
opts.SetPassword(config.Password) opts.SetPassword(config.Password)
} }
if config.HomeAssistant.Enabled {
opts.SetWill(haAvailabilityTopic(config), "offline", 1, true)
}
opts.SetKeepAlive(keepAlive) opts.SetKeepAlive(keepAlive)
opts.SetOnConnectHandler(func(mqtt.Client) { opts.SetOnConnectHandler(func(mqtt.Client) {
log.Info("Client connected to broker") log.Info("Client connected to broker")
}) })
opts.SetConnectionLostHandler(func(cli mqtt.Client, err error) { opts.SetConnectionLostHandler(func(_ mqtt.Client, err error) {
log.Errorf("Client connection to broker lost: %v", err) log.Errorf("Client connection to broker lost: %v", err)
}) })
return opts return opts
} }

View File

@@ -0,0 +1,316 @@
package mqttclient
import (
"testing"
"invertergui/mk2driver"
"github.com/stretchr/testify/assert"
)
type fakeWriter struct {
lastKind string
lastID uint16
lastValue int16
lastSwitchState mk2driver.PanelSwitchState
lastCurrentLimit *float64
lastStandby *bool
err error
}
func (f *fakeWriter) WriteRAMVar(id uint16, value int16) error {
f.lastKind = commandKindRAMVar
f.lastID = id
f.lastValue = value
return f.err
}
func (f *fakeWriter) WriteSetting(id uint16, value int16) error {
f.lastKind = commandKindSetting
f.lastID = id
f.lastValue = value
return f.err
}
func (f *fakeWriter) SetPanelState(switchState mk2driver.PanelSwitchState, currentLimitA *float64) error {
f.lastKind = commandKindPanel
f.lastSwitchState = switchState
f.lastCurrentLimit = currentLimitA
return f.err
}
func (f *fakeWriter) SetStandby(standby bool) error {
f.lastKind = commandKindStandby
f.lastStandby = &standby
return f.err
}
func Test_decodeWriteCommand(t *testing.T) {
currentLimit := 16.5
tests := []struct {
name string
payload string
check func(*testing.T, writeCommand)
wantErr string
}{
{
name: "setting",
payload: `{"request_id":"abc","kind":"setting","id":15,"value":-5}`,
check: func(t *testing.T, got writeCommand) {
assert.Equal(t, writeCommand{
RequestID: "abc",
Kind: commandKindSetting,
ID: 15,
Value: -5,
}, got)
},
},
{
name: "ram_var alias from type",
payload: `{"type":"ramvar","id":2,"value":7}`,
check: func(t *testing.T, got writeCommand) {
assert.Equal(t, writeCommand{
Kind: commandKindRAMVar,
ID: 2,
Value: 7,
}, got)
},
},
{
name: "panel state",
payload: `{"kind":"panel_state","switch":"on","current_limit":16.5}`,
check: func(t *testing.T, got writeCommand) {
assert.Equal(t, commandKindPanel, got.Kind)
assert.True(t, got.HasSwitch)
assert.Equal(t, mk2driver.PanelSwitchOn, got.SwitchState)
assert.Equal(t, "on", got.SwitchName)
if assert.NotNil(t, got.CurrentLimitA) {
assert.Equal(t, currentLimit, *got.CurrentLimitA)
}
},
},
{
name: "panel current limit only",
payload: `{"kind":"panel_state","current_limit":12.0}`,
check: func(t *testing.T, got writeCommand) {
assert.Equal(t, commandKindPanel, got.Kind)
assert.False(t, got.HasSwitch)
assert.Nil(t, got.Standby)
if assert.NotNil(t, got.CurrentLimitA) {
assert.Equal(t, 12.0, *got.CurrentLimitA)
}
},
},
{
name: "standby bool",
payload: `{"kind":"standby","standby":true}`,
check: func(t *testing.T, got writeCommand) {
assert.Equal(t, commandKindStandby, got.Kind)
if assert.NotNil(t, got.Standby) {
assert.True(t, *got.Standby)
}
},
},
{
name: "standby using value string",
payload: `{"kind":"standby","value":"OFF"}`,
check: func(t *testing.T, got writeCommand) {
assert.Equal(t, commandKindStandby, got.Kind)
if assert.NotNil(t, got.Standby) {
assert.False(t, *got.Standby)
}
},
},
{
name: "missing id",
payload: `{"kind":"setting","value":1}`,
wantErr: `missing required field "id"`,
},
{
name: "missing panel switch and current limit",
payload: `{"kind":"panel_state"}`,
wantErr: `missing required field "switch"`,
},
{
name: "invalid standby",
payload: `{"kind":"standby","value":"banana"}`,
wantErr: `field "standby" must be true/false`,
},
{
name: "invalid kind",
payload: `{"kind":"unknown","id":1,"value":1}`,
wantErr: `unsupported write command kind "unknown"`,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := decodeWriteCommand([]byte(tt.payload))
if tt.wantErr != "" {
assert.Error(t, err)
assert.ErrorContains(t, err, tt.wantErr)
return
}
assert.NoError(t, err)
tt.check(t, got)
})
}
}
func Test_executeWriteCommand(t *testing.T) {
limit := 8.0
standby := true
tests := []struct {
name string
cmd writeCommand
want string
}{
{
name: "setting",
cmd: writeCommand{
Kind: commandKindSetting,
ID: 9,
Value: 2,
},
want: commandKindSetting,
},
{
name: "ram_var",
cmd: writeCommand{
Kind: commandKindRAMVar,
ID: 3,
Value: -1,
},
want: commandKindRAMVar,
},
{
name: "panel_state",
cmd: writeCommand{
Kind: commandKindPanel,
HasSwitch: true,
SwitchState: mk2driver.PanelSwitchInverterOnly,
CurrentLimitA: &limit,
},
want: commandKindPanel,
},
{
name: "standby",
cmd: writeCommand{
Kind: commandKindStandby,
Standby: &standby,
},
want: commandKindStandby,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
writer := &fakeWriter{}
err := executeWriteCommand(writer, tt.cmd)
assert.NoError(t, err)
assert.Equal(t, tt.want, writer.lastKind)
switch tt.want {
case commandKindPanel:
assert.Equal(t, tt.cmd.SwitchState, writer.lastSwitchState)
if assert.NotNil(t, writer.lastCurrentLimit) {
assert.Equal(t, *tt.cmd.CurrentLimitA, *writer.lastCurrentLimit)
}
case commandKindStandby:
if assert.NotNil(t, writer.lastStandby) {
assert.Equal(t, *tt.cmd.Standby, *writer.lastStandby)
}
default:
assert.Equal(t, tt.cmd.ID, writer.lastID)
assert.Equal(t, tt.cmd.Value, writer.lastValue)
}
})
}
}
func Test_buildHADiscoveryDefinitions(t *testing.T) {
cfg := Config{
Topic: "invertergui/updates",
CommandTopic: "invertergui/settings/set",
HomeAssistant: HomeAssistantConfig{
Enabled: true,
DiscoveryPrefix: "homeassistant",
NodeID: "victron_main",
DeviceName: "Shed Victron",
},
}
definitions := buildHADiscoveryDefinitions(cfg)
assert.NotEmpty(t, definitions)
var panelMode *haDiscoveryDefinition
var panelCurrentLimit *haDiscoveryDefinition
var panelStandby *haDiscoveryDefinition
var batteryVoltage *haDiscoveryDefinition
for i := range definitions {
def := &definitions[i]
if def.Component == "select" && def.ObjectID == "remote_panel_mode" {
panelMode = def
}
if def.Component == "number" && def.ObjectID == "remote_panel_current_limit" {
panelCurrentLimit = def
}
if def.Component == "switch" && def.ObjectID == "remote_panel_standby" {
panelStandby = def
}
if def.Component == "sensor" && def.ObjectID == "battery_voltage" {
batteryVoltage = def
}
}
if assert.NotNil(t, panelMode) {
assert.Equal(t, cfg.CommandTopic, panelMode.Config["command_topic"])
assert.Equal(t, haPanelSwitchStateTopic(cfg), panelMode.Config["state_topic"])
}
if assert.NotNil(t, panelCurrentLimit) {
assert.Equal(t, cfg.CommandTopic, panelCurrentLimit.Config["command_topic"])
assert.Equal(t, haCurrentLimitStateTopic(cfg), panelCurrentLimit.Config["state_topic"])
}
if assert.NotNil(t, panelStandby) {
assert.Equal(t, cfg.CommandTopic, panelStandby.Config["command_topic"])
assert.Equal(t, haStandbyStateTopic(cfg), panelStandby.Config["state_topic"])
}
if assert.NotNil(t, batteryVoltage) {
assert.Equal(t, cfg.Topic, batteryVoltage.Config["state_topic"])
}
}
func Test_panelStateCacheResolvePanelCommand(t *testing.T) {
cache := &panelStateCache{}
_, err := cache.resolvePanelCommand(writeCommand{
Kind: commandKindPanel,
CurrentLimitA: float64Ptr(12),
})
assert.Error(t, err)
cache.remember(writeCommand{
Kind: commandKindPanel,
HasSwitch: true,
SwitchState: mk2driver.PanelSwitchOn,
SwitchName: "on",
})
resolved, err := cache.resolvePanelCommand(writeCommand{
Kind: commandKindPanel,
CurrentLimitA: float64Ptr(10),
})
assert.NoError(t, err)
assert.True(t, resolved.HasSwitch)
assert.Equal(t, mk2driver.PanelSwitchOn, resolved.SwitchState)
assert.Equal(t, "on", resolved.SwitchName)
}
func float64Ptr(in float64) *float64 {
return &in
}
func Test_normalizeID(t *testing.T) {
assert.Equal(t, "victron_main_01", normalizeID("Victron Main #01"))
assert.Equal(t, "inverter-gui", normalizeID(" inverter-gui "))
assert.Equal(t, "", normalizeID(" "))
}

View File

@@ -36,7 +36,7 @@ import (
"net/http" "net/http"
"time" "time"
"github.com/diebietse/invertergui/mk2driver" "invertergui/mk2driver"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
) )
@@ -63,7 +63,7 @@ func NewMunin(mk2 mk2driver.Mk2) *Munin {
return m return m
} }
func (m *Munin) ServeMuninHTTP(rw http.ResponseWriter, r *http.Request) { func (m *Munin) ServeMuninHTTP(rw http.ResponseWriter, _ *http.Request) {
muninDat := <-m.muninResponse muninDat := <-m.muninResponse
if muninDat.timesUpdated == 0 { if muninDat.timesUpdated == 0 {
log.Error("No data returned") log.Error("No data returned")
@@ -103,7 +103,7 @@ func (m *Munin) ServeMuninHTTP(rw http.ResponseWriter, r *http.Request) {
} }
} }
func (m *Munin) ServeMuninConfigHTTP(rw http.ResponseWriter, r *http.Request) { func (m *Munin) ServeMuninConfigHTTP(rw http.ResponseWriter, _ *http.Request) {
output := muninConfig output := muninConfig
_, err := rw.Write([]byte(output)) _, err := rw.Write([]byte(output))
if err != nil { if err != nil {

View File

@@ -6,10 +6,10 @@ import (
"net/http/httptest" "net/http/httptest"
"testing" "testing"
"github.com/diebietse/invertergui/mk2driver" "invertergui/mk2driver"
) )
func TestServer(t *testing.T) { func TestServer(_ *testing.T) {
mockMk2 := mk2driver.NewMk2Mock() mockMk2 := mk2driver.NewMk2Mock()
muninServer := NewMunin(mockMk2) muninServer := NewMunin(mockMk2)

View File

@@ -31,7 +31,7 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
package prometheus package prometheus
import ( import (
"github.com/diebietse/invertergui/mk2driver" "invertergui/mk2driver"
"github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus"
) )

View File

@@ -98,6 +98,117 @@
<div class="alert alert-danger" role="alert" v-if="error.has_error"> <div class="alert alert-danger" role="alert" v-if="error.has_error">
{{ error.error_message }} {{ error.error_message }}
</div> </div>
<div
class="alert"
v-if="control.message !== ''"
v-bind:class="[control.has_error ? 'alert-danger' : 'alert-success']"
>
{{ control.message }}
</div>
<div class="row">
<div class="col">
<div class="card mb-3">
<div class="card-body">
<h4 class="card-title">Remote Panel Control</h4>
<p class="text-muted mb-2">
Mode and current limit are applied together, equivalent to
<code>set_remote_panel_state</code>.
</p>
<p class="mb-1">
<strong>Current Mode:</strong>
{{ remoteModeLabel(state.remote_panel) }}
</p>
<p class="mb-1">
<strong>Current Limit:</strong>
{{ state.remote_panel.current_limit === null || state.remote_panel.current_limit === undefined ? 'Unknown' : state.remote_panel.current_limit + ' A' }}
</p>
<p class="mb-3">
<strong>Standby:</strong>
{{ remoteStandbyLabel(state.remote_panel) }}
</p>
<div class="row">
<div class="col-md-8">
<form v-on:submit.prevent="applyRemotePanelState">
<div class="form-row">
<div class="form-group col-md-6">
<label for="modeSelect">Remote Panel Mode</label>
<select
class="form-control"
id="modeSelect"
v-model="remote_form.mode"
v-bind:disabled="!state.remote_panel.writable || control.busy"
>
<option value="on">on</option>
<option value="off">off</option>
<option value="charger_only">charger_only</option>
<option value="inverter_only">inverter_only</option>
</select>
</div>
<div class="form-group col-md-6">
<label for="currentLimitInput">AC Input Current Limit (A)</label>
<input
id="currentLimitInput"
type="number"
min="0"
step="0.1"
class="form-control"
v-model="remote_form.current_limit"
placeholder="leave blank to keep current"
v-bind:disabled="!state.remote_panel.writable || control.busy"
/>
</div>
</div>
<button
type="submit"
class="btn btn-primary"
v-bind:disabled="!state.remote_panel.writable || control.busy"
>
Apply Mode + Current Limit
</button>
</form>
</div>
<div class="col-md-4">
<form v-on:submit.prevent="applyStandby">
<div class="form-group">
<div class="form-check mt-4">
<input
id="standbySwitch"
type="checkbox"
class="form-check-input"
v-model="remote_form.standby"
v-bind:disabled="!state.remote_panel.writable || control.busy"
/>
<label class="form-check-label" for="standbySwitch">
Prevent sleep while off
</label>
</div>
</div>
<button
type="submit"
class="btn btn-secondary"
v-bind:disabled="!state.remote_panel.writable || control.busy"
>
Apply Standby
</button>
</form>
</div>
</div>
<div class="mt-3 text-muted" v-if="state.remote_panel.last_updated">
Last update {{ state.remote_panel.last_updated }}
<span v-if="state.remote_panel.last_command">
({{ state.remote_panel.last_command }})
</span>
</div>
<div class="mt-2 text-danger" v-if="state.remote_panel.last_error">
{{ state.remote_panel.last_error }}
</div>
<div class="mt-2 text-warning" v-if="!state.remote_panel.writable">
Remote control is unavailable for this data source.
</div>
</div>
</div>
</div>
</div>
<div class="row"> <div class="row">
<div class="col"> <div class="col">
<hr /> <hr />

View File

@@ -3,6 +3,46 @@ const timeoutMax = 30000;
const timeoutMin = 1000; const timeoutMin = 1000;
var timeout = timeoutMin; var timeout = timeoutMin;
function defaultRemotePanelState() {
return {
writable: false,
mode: "unknown",
current_limit: null,
standby: null,
last_command: "",
last_error: "",
last_updated: ""
};
}
function defaultState() {
return {
output_current: null,
output_voltage: 0,
output_frequency: 0,
output_power: 0,
input_current: 0,
input_voltage: 0,
input_frequency: 0,
input_power: 0,
battery_current: 0,
battery_voltage: 0,
battery_charge: 0,
battery_power: 0,
led_map: {
led_mains: "dot-off",
led_absorb: "dot-off",
led_bulk: "dot-off",
led_float: "dot-off",
led_inverter: "dot-off",
led_overload: "dot-off",
led_bat_low: "dot-off",
led_over_temp: "dot-off"
},
remote_panel: defaultRemotePanelState()
};
}
function loadContent() { function loadContent() {
app = new Vue({ app = new Vue({
el: "#app", el: "#app",
@@ -11,33 +51,172 @@ function loadContent() {
has_error: false, has_error: false,
error_message: "" error_message: ""
}, },
state: { control: {
output_current: null, busy: false,
output_voltage: 0, has_error: false,
output_frequency: 0, message: ""
output_power: 0, },
input_current: 0, remote_form: {
input_voltage: 0, mode: "on",
input_frequency: 0, current_limit: "",
input_power: 0, standby: false
battery_current: 0, },
battery_voltage: 0, state: defaultState()
battery_charge: 0, },
battery_power: 0, methods: {
led_map: [ syncRemoteFormFromState: function(remoteState) {
{ led_mains: "dot-off" }, if (!remoteState) {
{ led_absorb: "dot-off" }, return;
{ led_bulk: "dot-off" }, }
{ led_float: "dot-off" }, if (remoteState.mode && remoteState.mode !== "unknown") {
{ led_inverter: "dot-off" }, this.remote_form.mode = remoteState.mode;
{ led_overload: "dot-off" }, }
{ led_bat_low: "dot-off" }, if (remoteState.current_limit === null || remoteState.current_limit === undefined) {
{ led_over_temp: "dot-off" } this.remote_form.current_limit = "";
] } else {
this.remote_form.current_limit = String(remoteState.current_limit);
}
if (remoteState.standby === null || remoteState.standby === undefined) {
this.remote_form.standby = false;
} else {
this.remote_form.standby = !!remoteState.standby;
}
},
remoteModeLabel: function(remoteState) {
var mode = (remoteState && remoteState.mode) || "unknown";
if (mode === "charger_only") {
return "Charger Only";
}
if (mode === "inverter_only") {
return "Inverter Only";
}
if (mode === "on") {
return "On";
}
if (mode === "off") {
return "Off";
}
return "Unknown";
},
remoteStandbyLabel: function(remoteState) {
if (!remoteState || remoteState.standby === null || remoteState.standby === undefined) {
return "Unknown";
}
return remoteState.standby ? "Enabled" : "Disabled";
},
refreshRemoteState: function() {
var self = this;
fetch(getAPIURI("api/remote-panel/state"))
.then(function(resp) {
if (!resp.ok) {
throw new Error("Could not load remote panel state.");
}
return resp.json();
})
.then(function(payload) {
self.state.remote_panel = payload;
self.syncRemoteFormFromState(payload);
})
.catch(function(err) {
self.control.has_error = true;
self.control.message = err.message;
});
},
applyRemotePanelState: function() {
var self = this;
if (!self.state.remote_panel.writable) {
return;
}
var body = {
mode: self.remote_form.mode
};
if (self.remote_form.current_limit !== "") {
var parsed = parseFloat(self.remote_form.current_limit);
if (isNaN(parsed)) {
self.control.has_error = true;
self.control.message = "Current limit must be numeric.";
return;
}
body.current_limit = parsed;
}
self.control.busy = true;
self.control.has_error = false;
self.control.message = "";
fetch(getAPIURI("api/remote-panel/state"), {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify(body)
})
.then(function(resp) {
if (!resp.ok) {
return resp.text().then(function(text) {
throw new Error(text || "Failed to set remote panel mode/current limit.");
});
}
return resp.json();
})
.then(function(payload) {
self.state.remote_panel = payload;
self.syncRemoteFormFromState(payload);
self.control.has_error = false;
self.control.message = "Remote panel state updated.";
})
.catch(function(err) {
self.control.has_error = true;
self.control.message = err.message;
})
.finally(function() {
self.control.busy = false;
});
},
applyStandby: function() {
var self = this;
if (!self.state.remote_panel.writable) {
return;
}
self.control.busy = true;
self.control.has_error = false;
self.control.message = "";
fetch(getAPIURI("api/remote-panel/standby"), {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
standby: !!self.remote_form.standby
})
})
.then(function(resp) {
if (!resp.ok) {
return resp.text().then(function(text) {
throw new Error(text || "Failed to set standby mode.");
});
}
return resp.json();
})
.then(function(payload) {
self.state.remote_panel = payload;
self.syncRemoteFormFromState(payload);
self.control.has_error = false;
self.control.message = "Standby mode updated.";
})
.catch(function(err) {
self.control.has_error = true;
self.control.message = err.message;
})
.finally(function() {
self.control.busy = false;
});
} }
} }
}); });
app.refreshRemoteState();
connect(); connect();
} }
@@ -61,7 +240,7 @@ function connect() {
} }
}; };
conn.onopen = function(evt) { conn.onopen = function() {
timeout = timeoutMin; timeout = timeoutMin;
app.error.has_error = false; app.error.has_error = false;
}; };
@@ -69,6 +248,9 @@ function connect() {
conn.onmessage = function(evt) { conn.onmessage = function(evt) {
var update = JSON.parse(evt.data); var update = JSON.parse(evt.data);
app.state = update; app.state = update;
if (!app.control.busy) {
app.syncRemoteFormFromState(update.remote_panel);
}
}; };
} else { } else {
app.error.has_error = true; app.error.has_error = true;
@@ -88,3 +270,11 @@ function getURI() {
new_uri += loc.pathname + "ws"; new_uri += loc.pathname + "ws";
return new_uri; return new_uri;
} }
function getAPIURI(path) {
var base = window.location.pathname;
if (base.slice(-1) !== "/") {
base += "/";
}
return base + path.replace(/^\/+/, "");
}

View File

@@ -31,13 +31,15 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
package webui package webui
import ( import (
"encoding/json"
"fmt" "fmt"
"net/http" "net/http"
"strings"
"sync" "sync"
"time" "time"
"github.com/diebietse/invertergui/mk2driver" "invertergui/mk2driver"
"github.com/diebietse/invertergui/websocket" "invertergui/websocket"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
) )
@@ -51,25 +53,53 @@ const (
BlinkGreen = "blink-green" BlinkGreen = "blink-green"
) )
const (
modeChargerOnly = "charger_only"
modeInverterOnly = "inverter_only"
modeOn = "on"
modeOff = "off"
modeUnknown = "unknown"
)
type WebGui struct { type WebGui struct {
mk2driver.Mk2 mk2driver.Mk2
writer mk2driver.SettingsWriter
stopChan chan struct{} stopChan chan struct{}
wg sync.WaitGroup wg sync.WaitGroup
hub *websocket.Hub hub *websocket.Hub
stateMu sync.RWMutex
latest *templateInput
remote remotePanelState
} }
func NewWebGui(source mk2driver.Mk2) *WebGui { func NewWebGui(source mk2driver.Mk2, writer mk2driver.SettingsWriter) *WebGui {
w := &WebGui{ w := &WebGui{
stopChan: make(chan struct{}), stopChan: make(chan struct{}),
Mk2: source, Mk2: source,
writer: writer,
hub: websocket.NewHub(), hub: websocket.NewHub(),
remote: remotePanelState{
Writable: writer != nil,
Mode: modeUnknown,
},
} }
w.wg.Add(1) w.wg.Add(1)
go w.dataPoll() go w.dataPoll()
return w return w
} }
type remotePanelState struct {
Writable bool `json:"writable"`
Mode string `json:"mode"`
CurrentLimit *float64 `json:"current_limit,omitempty"`
Standby *bool `json:"standby,omitempty"`
LastCommand string `json:"last_command,omitempty"`
LastError string `json:"last_error,omitempty"`
LastUpdated string `json:"last_updated,omitempty"`
}
type templateInput struct { type templateInput struct {
Error []error `json:"errors"` Error []error `json:"errors"`
@@ -94,12 +124,125 @@ type templateInput struct {
OutFreq string `json:"output_frequency"` OutFreq string `json:"output_frequency"`
LedMap map[string]string `json:"led_map"` LedMap map[string]string `json:"led_map"`
RemotePanel remotePanelState `json:"remote_panel"`
}
type setRemotePanelStateRequest struct {
Mode string `json:"mode"`
CurrentLimit *float64 `json:"current_limit"`
}
type setRemotePanelStandbyRequest struct {
Standby bool `json:"standby"`
} }
func (w *WebGui) ServeHub(rw http.ResponseWriter, r *http.Request) { func (w *WebGui) ServeHub(rw http.ResponseWriter, r *http.Request) {
w.hub.ServeHTTP(rw, r) w.hub.ServeHTTP(rw, r)
} }
func (w *WebGui) ServeRemotePanelState(rw http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodGet:
writeJSON(rw, http.StatusOK, w.getRemotePanelState())
case http.MethodPost:
w.handleSetRemotePanelState(rw, r)
default:
http.Error(rw, "method not allowed", http.StatusMethodNotAllowed)
}
}
func (w *WebGui) ServeRemotePanelStandby(rw http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodGet:
writeJSON(rw, http.StatusOK, w.getRemotePanelState())
case http.MethodPost:
w.handleSetRemotePanelStandby(rw, r)
default:
http.Error(rw, "method not allowed", http.StatusMethodNotAllowed)
}
}
func (w *WebGui) handleSetRemotePanelState(rw http.ResponseWriter, r *http.Request) {
if w.writer == nil {
http.Error(rw, "remote control is not supported by this data source", http.StatusNotImplemented)
return
}
req := setRemotePanelStateRequest{}
if err := decodeJSONBody(r, &req); err != nil {
http.Error(rw, err.Error(), http.StatusBadRequest)
return
}
switchState, normalizedMode, err := parsePanelMode(req.Mode)
if err != nil {
http.Error(rw, err.Error(), http.StatusBadRequest)
return
}
if err := w.writer.SetPanelState(switchState, req.CurrentLimit); err != nil {
w.updateRemotePanelState(func(state *remotePanelState) {
state.LastCommand = "set_remote_panel_state"
state.LastError = err.Error()
})
http.Error(rw, err.Error(), http.StatusBadGateway)
return
}
w.updateRemotePanelState(func(state *remotePanelState) {
state.Mode = normalizedMode
state.CurrentLimit = copyFloat64Ptr(req.CurrentLimit)
state.LastCommand = "set_remote_panel_state"
state.LastError = ""
})
writeJSON(rw, http.StatusOK, w.getRemotePanelState())
}
func (w *WebGui) handleSetRemotePanelStandby(rw http.ResponseWriter, r *http.Request) {
if w.writer == nil {
http.Error(rw, "remote control is not supported by this data source", http.StatusNotImplemented)
return
}
req := setRemotePanelStandbyRequest{}
if err := decodeJSONBody(r, &req); err != nil {
http.Error(rw, err.Error(), http.StatusBadRequest)
return
}
if err := w.writer.SetStandby(req.Standby); err != nil {
w.updateRemotePanelState(func(state *remotePanelState) {
state.LastCommand = "set_remote_panel_standby"
state.LastError = err.Error()
})
http.Error(rw, err.Error(), http.StatusBadGateway)
return
}
w.updateRemotePanelState(func(state *remotePanelState) {
state.Standby = copyBoolPtr(&req.Standby)
state.LastCommand = "set_remote_panel_standby"
state.LastError = ""
})
writeJSON(rw, http.StatusOK, w.getRemotePanelState())
}
func parsePanelMode(raw string) (mk2driver.PanelSwitchState, string, error) {
switch strings.TrimSpace(strings.ToLower(raw)) {
case modeChargerOnly:
return mk2driver.PanelSwitchChargerOnly, modeChargerOnly, nil
case modeInverterOnly:
return mk2driver.PanelSwitchInverterOnly, modeInverterOnly, nil
case modeOn:
return mk2driver.PanelSwitchOn, modeOn, nil
case modeOff:
return mk2driver.PanelSwitchOff, modeOff, nil
default:
return 0, "", fmt.Errorf("unsupported panel mode %q", raw)
}
}
func ledName(led mk2driver.Led) string { func ledName(led mk2driver.Led) string {
name, ok := mk2driver.LedNames[led] name, ok := mk2driver.LedNames[led]
if !ok { if !ok {
@@ -162,15 +305,17 @@ func (w *WebGui) Stop() {
w.wg.Wait() w.wg.Wait()
} }
// dataPoll waits for data from the w.poller channel. It will send its currently stored status
// to respChan if anything reads from it.
func (w *WebGui) dataPoll() { func (w *WebGui) dataPoll() {
for { for {
select { select {
case s := <-w.C(): case s := <-w.C():
if s.Valid { if s.Valid {
err := w.hub.Broadcast(buildTemplateInput(s)) payload := buildTemplateInput(s)
if err != nil { w.stateMu.Lock()
payload.RemotePanel = w.remote
w.latest = payload
w.stateMu.Unlock()
if err := w.hub.Broadcast(payload); err != nil {
log.Errorf("Could not send update to clients: %v", err) log.Errorf("Could not send update to clients: %v", err)
} }
} }
@@ -180,3 +325,93 @@ func (w *WebGui) dataPoll() {
} }
} }
} }
func (w *WebGui) getRemotePanelState() remotePanelState {
w.stateMu.RLock()
defer w.stateMu.RUnlock()
return copyRemotePanelState(w.remote)
}
func (w *WebGui) updateRemotePanelState(update func(state *remotePanelState)) {
w.stateMu.Lock()
update(&w.remote)
w.remote.LastUpdated = time.Now().UTC().Format(time.RFC3339)
snapshot := w.snapshotLocked()
w.stateMu.Unlock()
if snapshot != nil {
if err := w.hub.Broadcast(snapshot); err != nil {
log.Errorf("Could not send control update to clients: %v", err)
}
}
}
func (w *WebGui) snapshotLocked() *templateInput {
if w.latest == nil {
return nil
}
snapshot := cloneTemplateInput(w.latest)
snapshot.RemotePanel = copyRemotePanelState(w.remote)
return snapshot
}
func cloneTemplateInput(in *templateInput) *templateInput {
if in == nil {
return nil
}
out := *in
if in.Error != nil {
out.Error = append([]error(nil), in.Error...)
}
if in.LedMap != nil {
out.LedMap = make(map[string]string, len(in.LedMap))
for k, v := range in.LedMap {
out.LedMap[k] = v
}
}
out.RemotePanel = copyRemotePanelState(in.RemotePanel)
return &out
}
func copyRemotePanelState(in remotePanelState) remotePanelState {
in.CurrentLimit = copyFloat64Ptr(in.CurrentLimit)
in.Standby = copyBoolPtr(in.Standby)
return in
}
func copyFloat64Ptr(in *float64) *float64 {
if in == nil {
return nil
}
value := *in
return &value
}
func copyBoolPtr(in *bool) *bool {
if in == nil {
return nil
}
value := *in
return &value
}
func decodeJSONBody(r *http.Request, destination any) error {
defer r.Body.Close()
decoder := json.NewDecoder(r.Body)
decoder.DisallowUnknownFields()
if err := decoder.Decode(destination); err != nil {
return fmt.Errorf("invalid request body: %w", err)
}
return nil
}
func writeJSON(rw http.ResponseWriter, statusCode int, payload any) {
rw.Header().Set("Content-Type", "application/json")
rw.WriteHeader(statusCode)
if err := json.NewEncoder(rw).Encode(payload); err != nil {
log.Errorf("Could not encode webui API response: %v", err)
}
}

View File

@@ -36,7 +36,7 @@ import (
"testing" "testing"
"time" "time"
"github.com/diebietse/invertergui/mk2driver" "invertergui/mk2driver"
) )
type templateTest struct { type templateTest struct {
@@ -91,3 +91,53 @@ func TestTemplateInput(t *testing.T) {
} }
} }
} }
func TestParsePanelMode(t *testing.T) {
tests := []struct {
name string
input string
want mk2driver.PanelSwitchState
wantRaw string
wantErr bool
}{
{
name: "on",
input: "on",
want: mk2driver.PanelSwitchOn,
wantRaw: "on",
},
{
name: "charger_only",
input: "charger_only",
want: mk2driver.PanelSwitchChargerOnly,
wantRaw: "charger_only",
},
{
name: "invalid",
input: "banana",
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, gotRaw, err := parsePanelMode(tt.input)
if tt.wantErr {
if err == nil {
t.Fatal("expected error, got nil")
}
return
}
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if got != tt.want {
t.Fatalf("got switch %d, want %d", got, tt.want)
}
if gotRaw != tt.wantRaw {
t.Fatalf("got mode %q, want %q", gotRaw, tt.wantRaw)
}
})
}
}

View File

@@ -3,8 +3,7 @@
[![Go Reference](https://pkg.go.dev/badge/github.com/cespare/xxhash/v2.svg)](https://pkg.go.dev/github.com/cespare/xxhash/v2) [![Go Reference](https://pkg.go.dev/badge/github.com/cespare/xxhash/v2.svg)](https://pkg.go.dev/github.com/cespare/xxhash/v2)
[![Test](https://github.com/cespare/xxhash/actions/workflows/test.yml/badge.svg)](https://github.com/cespare/xxhash/actions/workflows/test.yml) [![Test](https://github.com/cespare/xxhash/actions/workflows/test.yml/badge.svg)](https://github.com/cespare/xxhash/actions/workflows/test.yml)
xxhash is a Go implementation of the 64-bit xxhash is a Go implementation of the 64-bit [xxHash] algorithm, XXH64. This is a
[xxHash](http://cyan4973.github.io/xxHash/) algorithm, XXH64. This is a
high-quality hashing algorithm that is much faster than anything in the Go high-quality hashing algorithm that is much faster than anything in the Go
standard library. standard library.
@@ -25,8 +24,11 @@ func (*Digest) WriteString(string) (int, error)
func (*Digest) Sum64() uint64 func (*Digest) Sum64() uint64
``` ```
This implementation provides a fast pure-Go implementation and an even faster The package is written with optimized pure Go and also contains even faster
assembly implementation for amd64. assembly implementations for amd64 and arm64. If desired, the `purego` build tag
opts into using the Go code even on those architectures.
[xxHash]: http://cyan4973.github.io/xxHash/
## Compatibility ## Compatibility
@@ -45,19 +47,20 @@ I recommend using the latest release of Go.
Here are some quick benchmarks comparing the pure-Go and assembly Here are some quick benchmarks comparing the pure-Go and assembly
implementations of Sum64. implementations of Sum64.
| input size | purego | asm | | input size | purego | asm |
| --- | --- | --- | | ---------- | --------- | --------- |
| 5 B | 979.66 MB/s | 1291.17 MB/s | | 4 B | 1.3 GB/s | 1.2 GB/s |
| 100 B | 7475.26 MB/s | 7973.40 MB/s | | 16 B | 2.9 GB/s | 3.5 GB/s |
| 4 KB | 17573.46 MB/s | 17602.65 MB/s | | 100 B | 6.9 GB/s | 8.1 GB/s |
| 10 MB | 17131.46 MB/s | 17142.16 MB/s | | 4 KB | 11.7 GB/s | 16.7 GB/s |
| 10 MB | 12.0 GB/s | 17.3 GB/s |
These numbers were generated on Ubuntu 18.04 with an Intel i7-8700K CPU using These numbers were generated on Ubuntu 20.04 with an Intel Xeon Platinum 8252C
the following commands under Go 1.11.2: CPU using the following commands under Go 1.19.2:
``` ```
$ go test -tags purego -benchtime 10s -bench '/xxhash,direct,bytes' benchstat <(go test -tags purego -benchtime 500ms -count 15 -bench 'Sum64$')
$ go test -benchtime 10s -bench '/xxhash,direct,bytes' benchstat <(go test -benchtime 500ms -count 15 -bench 'Sum64$')
``` ```
## Projects using this package ## Projects using this package
@@ -67,3 +70,5 @@ $ go test -benchtime 10s -bench '/xxhash,direct,bytes'
- [VictoriaMetrics](https://github.com/VictoriaMetrics/VictoriaMetrics) - [VictoriaMetrics](https://github.com/VictoriaMetrics/VictoriaMetrics)
- [FreeCache](https://github.com/coocood/freecache) - [FreeCache](https://github.com/coocood/freecache)
- [FastCache](https://github.com/VictoriaMetrics/fastcache) - [FastCache](https://github.com/VictoriaMetrics/fastcache)
- [Ristretto](https://github.com/dgraph-io/ristretto)
- [Badger](https://github.com/dgraph-io/badger)

10
vendor/github.com/cespare/xxhash/v2/testall.sh generated vendored Normal file
View File

@@ -0,0 +1,10 @@
#!/bin/bash
set -eu -o pipefail
# Small convenience script for running the tests with various combinations of
# arch/tags. This assumes we're running on amd64 and have qemu available.
go test ./...
go test -tags purego ./...
GOARCH=arm64 go test
GOARCH=arm64 go test -tags purego

View File

@@ -16,21 +16,16 @@ const (
prime5 uint64 = 2870177450012600261 prime5 uint64 = 2870177450012600261
) )
// NOTE(caleb): I'm using both consts and vars of the primes. Using consts where // Store the primes in an array as well.
// possible in the Go code is worth a small (but measurable) performance boost //
// by avoiding some MOVQs. Vars are needed for the asm and also are useful for // The consts are used when possible in Go code to avoid MOVs but we need a
// convenience in the Go code in a few places where we need to intentionally // contiguous array for the assembly code.
// avoid constant arithmetic (e.g., v1 := prime1 + prime2 fails because the var primes = [...]uint64{prime1, prime2, prime3, prime4, prime5}
// result overflows a uint64).
var (
prime1v = prime1
prime2v = prime2
prime3v = prime3
prime4v = prime4
prime5v = prime5
)
// Digest implements hash.Hash64. // Digest implements hash.Hash64.
//
// Note that a zero-valued Digest is not ready to receive writes.
// Call Reset or create a Digest using New before calling other methods.
type Digest struct { type Digest struct {
v1 uint64 v1 uint64
v2 uint64 v2 uint64
@@ -41,19 +36,31 @@ type Digest struct {
n int // how much of mem is used n int // how much of mem is used
} }
// New creates a new Digest that computes the 64-bit xxHash algorithm. // New creates a new Digest with a zero seed.
func New() *Digest { func New() *Digest {
return NewWithSeed(0)
}
// NewWithSeed creates a new Digest with the given seed.
func NewWithSeed(seed uint64) *Digest {
var d Digest var d Digest
d.Reset() d.ResetWithSeed(seed)
return &d return &d
} }
// Reset clears the Digest's state so that it can be reused. // Reset clears the Digest's state so that it can be reused.
// It uses a seed value of zero.
func (d *Digest) Reset() { func (d *Digest) Reset() {
d.v1 = prime1v + prime2 d.ResetWithSeed(0)
d.v2 = prime2 }
d.v3 = 0
d.v4 = -prime1v // ResetWithSeed clears the Digest's state so that it can be reused.
// It uses the given seed to initialize the state.
func (d *Digest) ResetWithSeed(seed uint64) {
d.v1 = seed + prime1 + prime2
d.v2 = seed + prime2
d.v3 = seed
d.v4 = seed - prime1
d.total = 0 d.total = 0
d.n = 0 d.n = 0
} }
@@ -69,21 +76,23 @@ func (d *Digest) Write(b []byte) (n int, err error) {
n = len(b) n = len(b)
d.total += uint64(n) d.total += uint64(n)
memleft := d.mem[d.n&(len(d.mem)-1):]
if d.n+n < 32 { if d.n+n < 32 {
// This new data doesn't even fill the current block. // This new data doesn't even fill the current block.
copy(d.mem[d.n:], b) copy(memleft, b)
d.n += n d.n += n
return return
} }
if d.n > 0 { if d.n > 0 {
// Finish off the partial block. // Finish off the partial block.
copy(d.mem[d.n:], b) c := copy(memleft, b)
d.v1 = round(d.v1, u64(d.mem[0:8])) d.v1 = round(d.v1, u64(d.mem[0:8]))
d.v2 = round(d.v2, u64(d.mem[8:16])) d.v2 = round(d.v2, u64(d.mem[8:16]))
d.v3 = round(d.v3, u64(d.mem[16:24])) d.v3 = round(d.v3, u64(d.mem[16:24]))
d.v4 = round(d.v4, u64(d.mem[24:32])) d.v4 = round(d.v4, u64(d.mem[24:32]))
b = b[32-d.n:] b = b[c:]
d.n = 0 d.n = 0
} }
@@ -133,21 +142,20 @@ func (d *Digest) Sum64() uint64 {
h += d.total h += d.total
i, end := 0, d.n b := d.mem[:d.n&(len(d.mem)-1)]
for ; i+8 <= end; i += 8 { for ; len(b) >= 8; b = b[8:] {
k1 := round(0, u64(d.mem[i:i+8])) k1 := round(0, u64(b[:8]))
h ^= k1 h ^= k1
h = rol27(h)*prime1 + prime4 h = rol27(h)*prime1 + prime4
} }
if i+4 <= end { if len(b) >= 4 {
h ^= uint64(u32(d.mem[i:i+4])) * prime1 h ^= uint64(u32(b[:4])) * prime1
h = rol23(h)*prime2 + prime3 h = rol23(h)*prime2 + prime3
i += 4 b = b[4:]
} }
for i < end { for ; len(b) > 0; b = b[1:] {
h ^= uint64(d.mem[i]) * prime5 h ^= uint64(b[0]) * prime5
h = rol11(h) * prime1 h = rol11(h) * prime1
i++
} }
h ^= h >> 33 h ^= h >> 33

View File

@@ -1,215 +1,209 @@
//go:build !appengine && gc && !purego
// +build !appengine // +build !appengine
// +build gc // +build gc
// +build !purego // +build !purego
#include "textflag.h" #include "textflag.h"
// Register allocation: // Registers:
// AX h #define h AX
// SI pointer to advance through b #define d AX
// DX n #define p SI // pointer to advance through b
// BX loop end #define n DX
// R8 v1, k1 #define end BX // loop end
// R9 v2 #define v1 R8
// R10 v3 #define v2 R9
// R11 v4 #define v3 R10
// R12 tmp #define v4 R11
// R13 prime1v #define x R12
// R14 prime2v #define prime1 R13
// DI prime4v #define prime2 R14
#define prime4 DI
// round reads from and advances the buffer pointer in SI. #define round(acc, x) \
// It assumes that R13 has prime1v and R14 has prime2v. IMULQ prime2, x \
#define round(r) \ ADDQ x, acc \
MOVQ (SI), R12 \ ROLQ $31, acc \
ADDQ $8, SI \ IMULQ prime1, acc
IMULQ R14, R12 \
ADDQ R12, r \
ROLQ $31, r \
IMULQ R13, r
// mergeRound applies a merge round on the two registers acc and val. // round0 performs the operation x = round(0, x).
// It assumes that R13 has prime1v, R14 has prime2v, and DI has prime4v. #define round0(x) \
#define mergeRound(acc, val) \ IMULQ prime2, x \
IMULQ R14, val \ ROLQ $31, x \
ROLQ $31, val \ IMULQ prime1, x
IMULQ R13, val \
XORQ val, acc \ // mergeRound applies a merge round on the two registers acc and x.
IMULQ R13, acc \ // It assumes that prime1, prime2, and prime4 have been loaded.
ADDQ DI, acc #define mergeRound(acc, x) \
round0(x) \
XORQ x, acc \
IMULQ prime1, acc \
ADDQ prime4, acc
// blockLoop processes as many 32-byte blocks as possible,
// updating v1, v2, v3, and v4. It assumes that there is at least one block
// to process.
#define blockLoop() \
loop: \
MOVQ +0(p), x \
round(v1, x) \
MOVQ +8(p), x \
round(v2, x) \
MOVQ +16(p), x \
round(v3, x) \
MOVQ +24(p), x \
round(v4, x) \
ADDQ $32, p \
CMPQ p, end \
JLE loop
// func Sum64(b []byte) uint64 // func Sum64(b []byte) uint64
TEXT ·Sum64(SB), NOSPLIT, $0-32 TEXT ·Sum64(SB), NOSPLIT|NOFRAME, $0-32
// Load fixed primes. // Load fixed primes.
MOVQ ·prime1v(SB), R13 MOVQ ·primes+0(SB), prime1
MOVQ ·prime2v(SB), R14 MOVQ ·primes+8(SB), prime2
MOVQ ·prime4v(SB), DI MOVQ ·primes+24(SB), prime4
// Load slice. // Load slice.
MOVQ b_base+0(FP), SI MOVQ b_base+0(FP), p
MOVQ b_len+8(FP), DX MOVQ b_len+8(FP), n
LEAQ (SI)(DX*1), BX LEAQ (p)(n*1), end
// The first loop limit will be len(b)-32. // The first loop limit will be len(b)-32.
SUBQ $32, BX SUBQ $32, end
// Check whether we have at least one block. // Check whether we have at least one block.
CMPQ DX, $32 CMPQ n, $32
JLT noBlocks JLT noBlocks
// Set up initial state (v1, v2, v3, v4). // Set up initial state (v1, v2, v3, v4).
MOVQ R13, R8 MOVQ prime1, v1
ADDQ R14, R8 ADDQ prime2, v1
MOVQ R14, R9 MOVQ prime2, v2
XORQ R10, R10 XORQ v3, v3
XORQ R11, R11 XORQ v4, v4
SUBQ R13, R11 SUBQ prime1, v4
// Loop until SI > BX. blockLoop()
blockLoop:
round(R8)
round(R9)
round(R10)
round(R11)
CMPQ SI, BX MOVQ v1, h
JLE blockLoop ROLQ $1, h
MOVQ v2, x
ROLQ $7, x
ADDQ x, h
MOVQ v3, x
ROLQ $12, x
ADDQ x, h
MOVQ v4, x
ROLQ $18, x
ADDQ x, h
MOVQ R8, AX mergeRound(h, v1)
ROLQ $1, AX mergeRound(h, v2)
MOVQ R9, R12 mergeRound(h, v3)
ROLQ $7, R12 mergeRound(h, v4)
ADDQ R12, AX
MOVQ R10, R12
ROLQ $12, R12
ADDQ R12, AX
MOVQ R11, R12
ROLQ $18, R12
ADDQ R12, AX
mergeRound(AX, R8)
mergeRound(AX, R9)
mergeRound(AX, R10)
mergeRound(AX, R11)
JMP afterBlocks JMP afterBlocks
noBlocks: noBlocks:
MOVQ ·prime5v(SB), AX MOVQ ·primes+32(SB), h
afterBlocks: afterBlocks:
ADDQ DX, AX ADDQ n, h
// Right now BX has len(b)-32, and we want to loop until SI > len(b)-8. ADDQ $24, end
ADDQ $24, BX CMPQ p, end
JG try4
CMPQ SI, BX loop8:
JG fourByte MOVQ (p), x
ADDQ $8, p
round0(x)
XORQ x, h
ROLQ $27, h
IMULQ prime1, h
ADDQ prime4, h
wordLoop: CMPQ p, end
// Calculate k1. JLE loop8
MOVQ (SI), R8
ADDQ $8, SI
IMULQ R14, R8
ROLQ $31, R8
IMULQ R13, R8
XORQ R8, AX try4:
ROLQ $27, AX ADDQ $4, end
IMULQ R13, AX CMPQ p, end
ADDQ DI, AX JG try1
CMPQ SI, BX MOVL (p), x
JLE wordLoop ADDQ $4, p
IMULQ prime1, x
XORQ x, h
fourByte: ROLQ $23, h
ADDQ $4, BX IMULQ prime2, h
CMPQ SI, BX ADDQ ·primes+16(SB), h
JG singles
MOVL (SI), R8 try1:
ADDQ $4, SI ADDQ $4, end
IMULQ R13, R8 CMPQ p, end
XORQ R8, AX
ROLQ $23, AX
IMULQ R14, AX
ADDQ ·prime3v(SB), AX
singles:
ADDQ $4, BX
CMPQ SI, BX
JGE finalize JGE finalize
singlesLoop: loop1:
MOVBQZX (SI), R12 MOVBQZX (p), x
ADDQ $1, SI ADDQ $1, p
IMULQ ·prime5v(SB), R12 IMULQ ·primes+32(SB), x
XORQ R12, AX XORQ x, h
ROLQ $11, h
IMULQ prime1, h
ROLQ $11, AX CMPQ p, end
IMULQ R13, AX JL loop1
CMPQ SI, BX
JL singlesLoop
finalize: finalize:
MOVQ AX, R12 MOVQ h, x
SHRQ $33, R12 SHRQ $33, x
XORQ R12, AX XORQ x, h
IMULQ R14, AX IMULQ prime2, h
MOVQ AX, R12 MOVQ h, x
SHRQ $29, R12 SHRQ $29, x
XORQ R12, AX XORQ x, h
IMULQ ·prime3v(SB), AX IMULQ ·primes+16(SB), h
MOVQ AX, R12 MOVQ h, x
SHRQ $32, R12 SHRQ $32, x
XORQ R12, AX XORQ x, h
MOVQ AX, ret+24(FP) MOVQ h, ret+24(FP)
RET RET
// writeBlocks uses the same registers as above except that it uses AX to store
// the d pointer.
// func writeBlocks(d *Digest, b []byte) int // func writeBlocks(d *Digest, b []byte) int
TEXT ·writeBlocks(SB), NOSPLIT, $0-40 TEXT ·writeBlocks(SB), NOSPLIT|NOFRAME, $0-40
// Load fixed primes needed for round. // Load fixed primes needed for round.
MOVQ ·prime1v(SB), R13 MOVQ ·primes+0(SB), prime1
MOVQ ·prime2v(SB), R14 MOVQ ·primes+8(SB), prime2
// Load slice. // Load slice.
MOVQ b_base+8(FP), SI MOVQ b_base+8(FP), p
MOVQ b_len+16(FP), DX MOVQ b_len+16(FP), n
LEAQ (SI)(DX*1), BX LEAQ (p)(n*1), end
SUBQ $32, BX SUBQ $32, end
// Load vN from d. // Load vN from d.
MOVQ d+0(FP), AX MOVQ s+0(FP), d
MOVQ 0(AX), R8 // v1 MOVQ 0(d), v1
MOVQ 8(AX), R9 // v2 MOVQ 8(d), v2
MOVQ 16(AX), R10 // v3 MOVQ 16(d), v3
MOVQ 24(AX), R11 // v4 MOVQ 24(d), v4
// We don't need to check the loop condition here; this function is // We don't need to check the loop condition here; this function is
// always called with at least one block of data to process. // always called with at least one block of data to process.
blockLoop: blockLoop()
round(R8)
round(R9)
round(R10)
round(R11)
CMPQ SI, BX
JLE blockLoop
// Copy vN back to d. // Copy vN back to d.
MOVQ R8, 0(AX) MOVQ v1, 0(d)
MOVQ R9, 8(AX) MOVQ v2, 8(d)
MOVQ R10, 16(AX) MOVQ v3, 16(d)
MOVQ R11, 24(AX) MOVQ v4, 24(d)
// The number of bytes written is SI minus the old base pointer. // The number of bytes written is p minus the old base pointer.
SUBQ b_base+8(FP), SI SUBQ b_base+8(FP), p
MOVQ SI, ret+32(FP) MOVQ p, ret+32(FP)
RET RET

183
vendor/github.com/cespare/xxhash/v2/xxhash_arm64.s generated vendored Normal file
View File

@@ -0,0 +1,183 @@
//go:build !appengine && gc && !purego
// +build !appengine
// +build gc
// +build !purego
#include "textflag.h"
// Registers:
#define digest R1
#define h R2 // return value
#define p R3 // input pointer
#define n R4 // input length
#define nblocks R5 // n / 32
#define prime1 R7
#define prime2 R8
#define prime3 R9
#define prime4 R10
#define prime5 R11
#define v1 R12
#define v2 R13
#define v3 R14
#define v4 R15
#define x1 R20
#define x2 R21
#define x3 R22
#define x4 R23
#define round(acc, x) \
MADD prime2, acc, x, acc \
ROR $64-31, acc \
MUL prime1, acc
// round0 performs the operation x = round(0, x).
#define round0(x) \
MUL prime2, x \
ROR $64-31, x \
MUL prime1, x
#define mergeRound(acc, x) \
round0(x) \
EOR x, acc \
MADD acc, prime4, prime1, acc
// blockLoop processes as many 32-byte blocks as possible,
// updating v1, v2, v3, and v4. It assumes that n >= 32.
#define blockLoop() \
LSR $5, n, nblocks \
PCALIGN $16 \
loop: \
LDP.P 16(p), (x1, x2) \
LDP.P 16(p), (x3, x4) \
round(v1, x1) \
round(v2, x2) \
round(v3, x3) \
round(v4, x4) \
SUB $1, nblocks \
CBNZ nblocks, loop
// func Sum64(b []byte) uint64
TEXT ·Sum64(SB), NOSPLIT|NOFRAME, $0-32
LDP b_base+0(FP), (p, n)
LDP ·primes+0(SB), (prime1, prime2)
LDP ·primes+16(SB), (prime3, prime4)
MOVD ·primes+32(SB), prime5
CMP $32, n
CSEL LT, prime5, ZR, h // if n < 32 { h = prime5 } else { h = 0 }
BLT afterLoop
ADD prime1, prime2, v1
MOVD prime2, v2
MOVD $0, v3
NEG prime1, v4
blockLoop()
ROR $64-1, v1, x1
ROR $64-7, v2, x2
ADD x1, x2
ROR $64-12, v3, x3
ROR $64-18, v4, x4
ADD x3, x4
ADD x2, x4, h
mergeRound(h, v1)
mergeRound(h, v2)
mergeRound(h, v3)
mergeRound(h, v4)
afterLoop:
ADD n, h
TBZ $4, n, try8
LDP.P 16(p), (x1, x2)
round0(x1)
// NOTE: here and below, sequencing the EOR after the ROR (using a
// rotated register) is worth a small but measurable speedup for small
// inputs.
ROR $64-27, h
EOR x1 @> 64-27, h, h
MADD h, prime4, prime1, h
round0(x2)
ROR $64-27, h
EOR x2 @> 64-27, h, h
MADD h, prime4, prime1, h
try8:
TBZ $3, n, try4
MOVD.P 8(p), x1
round0(x1)
ROR $64-27, h
EOR x1 @> 64-27, h, h
MADD h, prime4, prime1, h
try4:
TBZ $2, n, try2
MOVWU.P 4(p), x2
MUL prime1, x2
ROR $64-23, h
EOR x2 @> 64-23, h, h
MADD h, prime3, prime2, h
try2:
TBZ $1, n, try1
MOVHU.P 2(p), x3
AND $255, x3, x1
LSR $8, x3, x2
MUL prime5, x1
ROR $64-11, h
EOR x1 @> 64-11, h, h
MUL prime1, h
MUL prime5, x2
ROR $64-11, h
EOR x2 @> 64-11, h, h
MUL prime1, h
try1:
TBZ $0, n, finalize
MOVBU (p), x4
MUL prime5, x4
ROR $64-11, h
EOR x4 @> 64-11, h, h
MUL prime1, h
finalize:
EOR h >> 33, h
MUL prime2, h
EOR h >> 29, h
MUL prime3, h
EOR h >> 32, h
MOVD h, ret+24(FP)
RET
// func writeBlocks(d *Digest, b []byte) int
TEXT ·writeBlocks(SB), NOSPLIT|NOFRAME, $0-40
LDP ·primes+0(SB), (prime1, prime2)
// Load state. Assume v[1-4] are stored contiguously.
MOVD d+0(FP), digest
LDP 0(digest), (v1, v2)
LDP 16(digest), (v3, v4)
LDP b_base+8(FP), (p, n)
blockLoop()
// Store updated state.
STP (v1, v2), 0(digest)
STP (v3, v4), 16(digest)
BIC $31, n
MOVD n, ret+32(FP)
RET

View File

@@ -1,10 +1,12 @@
//go:build (amd64 || arm64) && !appengine && gc && !purego
// +build amd64 arm64
// +build !appengine // +build !appengine
// +build gc // +build gc
// +build !purego // +build !purego
package xxhash package xxhash
// Sum64 computes the 64-bit xxHash digest of b. // Sum64 computes the 64-bit xxHash digest of b with a zero seed.
// //
//go:noescape //go:noescape
func Sum64(b []byte) uint64 func Sum64(b []byte) uint64

View File

@@ -1,8 +1,9 @@
// +build !amd64 appengine !gc purego //go:build (!amd64 && !arm64) || appengine || !gc || purego
// +build !amd64,!arm64 appengine !gc purego
package xxhash package xxhash
// Sum64 computes the 64-bit xxHash digest of b. // Sum64 computes the 64-bit xxHash digest of b with a zero seed.
func Sum64(b []byte) uint64 { func Sum64(b []byte) uint64 {
// A simpler version would be // A simpler version would be
// d := New() // d := New()
@@ -14,10 +15,10 @@ func Sum64(b []byte) uint64 {
var h uint64 var h uint64
if n >= 32 { if n >= 32 {
v1 := prime1v + prime2 v1 := primes[0] + prime2
v2 := prime2 v2 := prime2
v3 := uint64(0) v3 := uint64(0)
v4 := -prime1v v4 := -primes[0]
for len(b) >= 32 { for len(b) >= 32 {
v1 = round(v1, u64(b[0:8:len(b)])) v1 = round(v1, u64(b[0:8:len(b)]))
v2 = round(v2, u64(b[8:16:len(b)])) v2 = round(v2, u64(b[8:16:len(b)]))
@@ -36,19 +37,18 @@ func Sum64(b []byte) uint64 {
h += uint64(n) h += uint64(n)
i, end := 0, len(b) for ; len(b) >= 8; b = b[8:] {
for ; i+8 <= end; i += 8 { k1 := round(0, u64(b[:8]))
k1 := round(0, u64(b[i:i+8:len(b)]))
h ^= k1 h ^= k1
h = rol27(h)*prime1 + prime4 h = rol27(h)*prime1 + prime4
} }
if i+4 <= end { if len(b) >= 4 {
h ^= uint64(u32(b[i:i+4:len(b)])) * prime1 h ^= uint64(u32(b[:4])) * prime1
h = rol23(h)*prime2 + prime3 h = rol23(h)*prime2 + prime3
i += 4 b = b[4:]
} }
for ; i < end; i++ { for ; len(b) > 0; b = b[1:] {
h ^= uint64(b[i]) * prime5 h ^= uint64(b[0]) * prime5
h = rol11(h) * prime1 h = rol11(h) * prime1
} }

View File

@@ -1,10 +1,11 @@
//go:build appengine
// +build appengine // +build appengine
// This file contains the safe implementations of otherwise unsafe-using code. // This file contains the safe implementations of otherwise unsafe-using code.
package xxhash package xxhash
// Sum64String computes the 64-bit xxHash digest of s. // Sum64String computes the 64-bit xxHash digest of s with a zero seed.
func Sum64String(s string) uint64 { func Sum64String(s string) uint64 {
return Sum64([]byte(s)) return Sum64([]byte(s))
} }

View File

@@ -1,3 +1,4 @@
//go:build !appengine
// +build !appengine // +build !appengine
// This file encapsulates usage of unsafe. // This file encapsulates usage of unsafe.
@@ -11,7 +12,7 @@ import (
// In the future it's possible that compiler optimizations will make these // In the future it's possible that compiler optimizations will make these
// XxxString functions unnecessary by realizing that calls such as // XxxString functions unnecessary by realizing that calls such as
// Sum64([]byte(s)) don't need to copy s. See https://golang.org/issue/2205. // Sum64([]byte(s)) don't need to copy s. See https://go.dev/issue/2205.
// If that happens, even if we keep these functions they can be replaced with // If that happens, even if we keep these functions they can be replaced with
// the trivial safe code. // the trivial safe code.
@@ -32,7 +33,7 @@ import (
// //
// See https://github.com/golang/go/issues/42739 for discussion. // See https://github.com/golang/go/issues/42739 for discussion.
// Sum64String computes the 64-bit xxHash digest of s. // Sum64String computes the 64-bit xxHash digest of s with a zero seed.
// It may be faster than Sum64([]byte(s)) by avoiding a copy. // It may be faster than Sum64([]byte(s)) by avoiding a copy.
func Sum64String(s string) uint64 { func Sum64String(s string) uint64 {
b := *(*[]byte)(unsafe.Pointer(&sliceHeader{s, len(s)})) b := *(*[]byte)(unsafe.Pointer(&sliceHeader{s, len(s)}))

View File

@@ -113,7 +113,9 @@ identifier; this is as per the [spec](https://docs.oasis-open.org/mqtt/mqtt/v3.1
not received, disconnecting` errors). not received, disconnecting` errors).
* When QOS1+ subscriptions have been created previously and you connect with `CleanSession` set to false it is possible * When QOS1+ subscriptions have been created previously and you connect with `CleanSession` set to false it is possible
that the broker will deliver retained messages before `Subscribe` can be called. To process these messages either that the broker will deliver retained messages before `Subscribe` can be called. To process these messages either
configure a handler with `AddRoute` or set a `DefaultPublishHandler`. configure a handler with `AddRoute` or set a `DefaultPublishHandler`. If there is no handler (or `DefaultPublishHandler`)
then inbound messages will not be acknowledged. Adding a handler (even if it's `opts.SetDefaultPublishHandler(func(mqtt.Client, mqtt.Message) {})`)
is highly recommended to avoid inadvertently hitting inflight message limits.
* Loss of network connectivity may not be detected immediately. If this is an issue then consider setting * Loss of network connectivity may not be detected immediately. If this is an issue then consider setting
`ClientOptions.KeepAlive` (sends regular messages to check the link is active). `ClientOptions.KeepAlive` (sends regular messages to check the link is active).
* Reusing a `Client` is not completely safe. After calling `Disconnect` please create a new Client (`NewClient()`) rather * Reusing a `Client` is not completely safe. After calling `Disconnect` please create a new Client (`NewClient()`) rather
@@ -193,4 +195,4 @@ Discussion of the Paho clients takes place on the [Eclipse paho-dev mailing list
General questions about the MQTT protocol are discussed in the [MQTT Google Group](https://groups.google.com/forum/?hl=en-US&fromgroups#!forum/mqtt). General questions about the MQTT protocol are discussed in the [MQTT Google Group](https://groups.google.com/forum/?hl=en-US&fromgroups#!forum/mqtt).
There is much more information available via the [MQTT community site](http://mqtt.org). There is much more information available via the [MQTT community site](http://mqtt.org).

104
vendor/github.com/eclipse/paho.mqtt.golang/backoff.go generated vendored Normal file
View File

@@ -0,0 +1,104 @@
/*
* Copyright (c) 2021 IBM Corp and others.
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v2.0
* and Eclipse Distribution License v1.0 which accompany this distribution.
*
* The Eclipse Public License is available at
* https://www.eclipse.org/legal/epl-2.0/
* and the Eclipse Distribution License is available at
* http://www.eclipse.org/org/documents/edl-v10.php.
*
* Contributors:
* Matt Brittan
* Daichi Tomaru
*/
package mqtt
import (
"sync"
"time"
)
// Controller for sleep with backoff when the client attempts reconnection
// It has statuses for each situations cause reconnection.
type backoffController struct {
sync.RWMutex
statusMap map[string]*backoffStatus
}
type backoffStatus struct {
lastSleepPeriod time.Duration
lastErrorTime time.Time
}
func newBackoffController() *backoffController {
return &backoffController{
statusMap: map[string]*backoffStatus{},
}
}
// Calculate next sleep period from the specified parameters.
// Returned values are next sleep period and whether the error situation is continual.
// If connection errors continuouslly occurs, its sleep period is exponentially increased.
// Also if there is a lot of time between last and this error, sleep period is initialized.
func (b *backoffController) getBackoffSleepTime(
situation string, initSleepPeriod time.Duration, maxSleepPeriod time.Duration, processTime time.Duration, skipFirst bool,
) (time.Duration, bool) {
// Decide first sleep time if the situation is not continual.
var firstProcess = func(status *backoffStatus, init time.Duration, skip bool) (time.Duration, bool) {
if skip {
status.lastSleepPeriod = 0
return 0, false
}
status.lastSleepPeriod = init
return init, false
}
// Prioritize maxSleep.
if initSleepPeriod > maxSleepPeriod {
initSleepPeriod = maxSleepPeriod
}
b.Lock()
defer b.Unlock()
status, exist := b.statusMap[situation]
if !exist {
b.statusMap[situation] = &backoffStatus{initSleepPeriod, time.Now()}
return firstProcess(b.statusMap[situation], initSleepPeriod, skipFirst)
}
oldTime := status.lastErrorTime
status.lastErrorTime = time.Now()
// When there is a lot of time between last and this error, sleep period is initialized.
if status.lastErrorTime.Sub(oldTime) > (processTime * 2 + status.lastSleepPeriod) {
return firstProcess(status, initSleepPeriod, skipFirst)
}
if status.lastSleepPeriod == 0 {
status.lastSleepPeriod = initSleepPeriod
return initSleepPeriod, true
}
if nextSleepPeriod := status.lastSleepPeriod * 2; nextSleepPeriod <= maxSleepPeriod {
status.lastSleepPeriod = nextSleepPeriod
} else {
status.lastSleepPeriod = maxSleepPeriod
}
return status.lastSleepPeriod, true
}
// Execute sleep the time returned from getBackoffSleepTime.
func (b *backoffController) sleepWithBackoff(
situation string, initSleepPeriod time.Duration, maxSleepPeriod time.Duration, processTime time.Duration, skipFirst bool,
) (time.Duration, bool) {
sleep, isFirst := b.getBackoffSleepTime(situation, initSleepPeriod, maxSleepPeriod, processTime, skipFirst)
if sleep != 0 {
time.Sleep(sleep)
}
return sleep, isFirst
}

View File

@@ -141,6 +141,8 @@ type client struct {
stop chan struct{} // Closed to request that workers stop stop chan struct{} // Closed to request that workers stop
workers sync.WaitGroup // used to wait for workers to complete (ping, keepalive, errwatch, resume) workers sync.WaitGroup // used to wait for workers to complete (ping, keepalive, errwatch, resume)
commsStopped chan struct{} // closed when the comms routines have stopped (kept running until after workers have closed to avoid deadlocks) commsStopped chan struct{} // closed when the comms routines have stopped (kept running until after workers have closed to avoid deadlocks)
backoff *backoffController
} }
// NewClient will create an MQTT v3.1.1 client with all of the options specified // NewClient will create an MQTT v3.1.1 client with all of the options specified
@@ -169,6 +171,7 @@ func NewClient(o *ClientOptions) Client {
c.msgRouter.setDefaultHandler(c.options.DefaultPublishHandler) c.msgRouter.setDefaultHandler(c.options.DefaultPublishHandler)
c.obound = make(chan *PacketAndToken) c.obound = make(chan *PacketAndToken)
c.oboundP = make(chan *PacketAndToken) c.oboundP = make(chan *PacketAndToken)
c.backoff = newBackoffController()
return c return c
} }
@@ -255,12 +258,15 @@ func (c *client) Connect() Token {
return return
} }
var attemptCount int
RETRYCONN: RETRYCONN:
var conn net.Conn var conn net.Conn
var rc byte var rc byte
var err error var err error
conn, rc, t.sessionPresent, err = c.attemptConnection() conn, rc, t.sessionPresent, err = c.attemptConnection(false, attemptCount)
if err != nil { if err != nil {
attemptCount++
if c.options.ConnectRetry { if c.options.ConnectRetry {
DEBUG.Println(CLI, "Connect failed, sleeping for", int(c.options.ConnectRetryInterval.Seconds()), "seconds and will then retry, error:", err.Error()) DEBUG.Println(CLI, "Connect failed, sleeping for", int(c.options.ConnectRetryInterval.Seconds()), "seconds and will then retry, error:", err.Error())
time.Sleep(c.options.ConnectRetryInterval) time.Sleep(c.options.ConnectRetryInterval)
@@ -302,28 +308,29 @@ func (c *client) Connect() Token {
func (c *client) reconnect(connectionUp connCompletedFn) { func (c *client) reconnect(connectionUp connCompletedFn) {
DEBUG.Println(CLI, "enter reconnect") DEBUG.Println(CLI, "enter reconnect")
var ( var (
sleep = 1 * time.Second initSleep = 1 * time.Second
conn net.Conn conn net.Conn
) )
// If the reason of connection lost is same as the before one, sleep timer is set before attempting connection is started.
// Sleep time is exponentially increased as the same situation continues
if slp, isContinual := c.backoff.sleepWithBackoff("connectionLost", initSleep, c.options.MaxReconnectInterval, 3*time.Second, true); isContinual {
DEBUG.Println(CLI, "Detect continual connection lost after reconnect, slept for", int(slp.Seconds()), "seconds")
}
var attemptCount int
for { for {
if nil != c.options.OnReconnecting { if nil != c.options.OnReconnecting {
c.options.OnReconnecting(c, &c.options) c.options.OnReconnecting(c, &c.options)
} }
var err error var err error
conn, _, _, err = c.attemptConnection() conn, _, _, err = c.attemptConnection(true, attemptCount)
if err == nil { if err == nil {
break break
} }
DEBUG.Println(CLI, "Reconnect failed, sleeping for", int(sleep.Seconds()), "seconds:", err) attemptCount++
time.Sleep(sleep) sleep, _ := c.backoff.sleepWithBackoff("attemptReconnection", initSleep, c.options.MaxReconnectInterval, c.options.ConnectTimeout, false)
if sleep < c.options.MaxReconnectInterval { DEBUG.Println(CLI, "Reconnect failed, slept for", int(sleep.Seconds()), "seconds:", err)
sleep *= 2
}
if sleep > c.options.MaxReconnectInterval {
sleep = c.options.MaxReconnectInterval
}
if c.status.ConnectionStatus() != reconnecting { // Disconnect may have been called if c.status.ConnectionStatus() != reconnecting { // Disconnect may have been called
if err := connectionUp(false); err != nil { // Should always return an error if err := connectionUp(false); err != nil { // Should always return an error
@@ -349,7 +356,7 @@ func (c *client) reconnect(connectionUp connCompletedFn) {
// byte - Return code (packets.Accepted indicates a successful connection). // byte - Return code (packets.Accepted indicates a successful connection).
// bool - SessionPresent flag from the connect ack (only valid if packets.Accepted) // bool - SessionPresent flag from the connect ack (only valid if packets.Accepted)
// err - Error (err != nil guarantees that conn has been set to active connection). // err - Error (err != nil guarantees that conn has been set to active connection).
func (c *client) attemptConnection() (net.Conn, byte, bool, error) { func (c *client) attemptConnection(isReconnect bool, attempt int) (net.Conn, byte, bool, error) {
protocolVersion := c.options.ProtocolVersion protocolVersion := c.options.ProtocolVersion
var ( var (
sessionPresent bool sessionPresent bool
@@ -358,6 +365,10 @@ func (c *client) attemptConnection() (net.Conn, byte, bool, error) {
rc byte rc byte
) )
if c.options.OnConnectionNotification != nil {
c.options.OnConnectionNotification(c, ConnectionNotificationConnecting{isReconnect, attempt})
}
c.optionsMu.Lock() // Protect c.options.Servers so that servers can be added in test cases c.optionsMu.Lock() // Protect c.options.Servers so that servers can be added in test cases
brokers := c.options.Servers brokers := c.options.Servers
c.optionsMu.Unlock() c.optionsMu.Unlock()
@@ -370,6 +381,9 @@ func (c *client) attemptConnection() (net.Conn, byte, bool, error) {
DEBUG.Println(CLI, "using custom onConnectAttempt handler...") DEBUG.Println(CLI, "using custom onConnectAttempt handler...")
tlsCfg = c.options.OnConnectAttempt(broker, c.options.TLSConfig) tlsCfg = c.options.OnConnectAttempt(broker, c.options.TLSConfig)
} }
if c.options.OnConnectionNotification != nil {
c.options.OnConnectionNotification(c, ConnectionNotificationBroker{broker})
}
connDeadline := time.Now().Add(c.options.ConnectTimeout) // Time by which connection must be established connDeadline := time.Now().Add(c.options.ConnectTimeout) // Time by which connection must be established
dialer := c.options.Dialer dialer := c.options.Dialer
if dialer == nil { // if dialer == nil { //
@@ -386,6 +400,9 @@ func (c *client) attemptConnection() (net.Conn, byte, bool, error) {
ERROR.Println(CLI, err.Error()) ERROR.Println(CLI, err.Error())
WARN.Println(CLI, "failed to connect to broker, trying next") WARN.Println(CLI, "failed to connect to broker, trying next")
rc = packets.ErrNetworkError rc = packets.ErrNetworkError
if c.options.OnConnectionNotification != nil {
c.options.OnConnectionNotification(c, ConnectionNotificationBrokerFailed{broker, err})
}
continue continue
} }
DEBUG.Println(CLI, "socket connected to broker") DEBUG.Println(CLI, "socket connected to broker")
@@ -425,9 +442,12 @@ func (c *client) attemptConnection() (net.Conn, byte, bool, error) {
if rc != packets.ErrNetworkError { // mqtt error if rc != packets.ErrNetworkError { // mqtt error
err = packets.ConnErrors[rc] err = packets.ConnErrors[rc]
} else { // network error (if this occurred in ConnectMQTT then err will be nil) } else { // network error (if this occurred in ConnectMQTT then err will be nil)
err = fmt.Errorf("%s : %s", packets.ConnErrors[rc], err) err = fmt.Errorf("%w : %w", packets.ConnErrors[rc], err)
} }
} }
if err != nil && c.options.OnConnectionNotification != nil {
c.options.OnConnectionNotification(c, ConnectionNotificationFailed{err})
}
return conn, rc, sessionPresent, err return conn, rc, sessionPresent, err
} }
@@ -562,6 +582,9 @@ func (c *client) internalConnLost(whyConnLost error) {
if c.options.OnConnectionLost != nil { if c.options.OnConnectionLost != nil {
go c.options.OnConnectionLost(c, whyConnLost) go c.options.OnConnectionLost(c, whyConnLost)
} }
if c.options.OnConnectionNotification != nil {
go c.options.OnConnectionNotification(c, ConnectionNotificationLost{whyConnLost})
}
DEBUG.Println(CLI, "internalConnLost complete") DEBUG.Println(CLI, "internalConnLost complete")
}() }()
} }
@@ -599,21 +622,21 @@ func (c *client) startCommsWorkers(conn net.Conn, connectionUp connCompletedFn,
c.workers.Add(1) // Done will be called when ackOut is closed c.workers.Add(1) // Done will be called when ackOut is closed
ackOut := c.msgRouter.matchAndDispatch(incomingPubChan, c.options.Order, c) ackOut := c.msgRouter.matchAndDispatch(incomingPubChan, c.options.Order, c)
// The connection is now ready for use (we spin up a few go routines below). It is possible that // The connection is now ready for use (we spin up a few go routines below).
// Disconnect has been called in the interim... // It is possible that Disconnect has been called in the interim...
// issue 675we will allow the connection to complete before the Disconnect is allowed to proceed
// as if a Disconnect event occurred immediately after connectionUp(true) completed.
if err := connectionUp(true); err != nil { if err := connectionUp(true); err != nil {
DEBUG.Println(CLI, err) ERROR.Println(CLI, err)
close(c.stop) // Tidy up anything we have already started
close(incomingPubChan)
c.workers.Wait()
c.conn.Close()
c.conn = nil
return false
} }
DEBUG.Println(CLI, "client is connected/reconnected") DEBUG.Println(CLI, "client is connected/reconnected")
if c.options.OnConnect != nil { if c.options.OnConnect != nil {
go c.options.OnConnect(c) go c.options.OnConnect(c)
} }
if c.options.OnConnectionNotification != nil {
go c.options.OnConnectionNotification(c, ConnectionNotificationConnected{})
}
// c.oboundP and c.obound need to stay active for the life of the client because, depending upon the options, // c.oboundP and c.obound need to stay active for the life of the client because, depending upon the options,
// messages may be published while the client is disconnected (they will block unless in a goroutine). However // messages may be published while the client is disconnected (they will block unless in a goroutine). However
@@ -797,9 +820,13 @@ func (c *client) Publish(topic string, qos byte, retained bool, payload interfac
if publishWaitTimeout == 0 { if publishWaitTimeout == 0 {
publishWaitTimeout = time.Second * 30 publishWaitTimeout = time.Second * 30
} }
t := time.NewTimer(publishWaitTimeout)
defer t.Stop()
select { select {
case c.obound <- &PacketAndToken{p: pub, t: token}: case c.obound <- &PacketAndToken{p: pub, t: token}:
case <-time.After(publishWaitTimeout): case <-t.C:
token.setError(errors.New("publish was broken by timeout")) token.setError(errors.New("publish was broken by timeout"))
} }
} }

View File

@@ -19,7 +19,7 @@
package mqtt package mqtt
import ( import (
"io/ioutil" "io/fs"
"os" "os"
"path" "path"
"sort" "sort"
@@ -159,15 +159,20 @@ func (store *FileStore) Reset() {
func (store *FileStore) all() []string { func (store *FileStore) all() []string {
var err error var err error
var keys []string var keys []string
var files fileInfos
if !store.opened { if !store.opened {
ERROR.Println(STR, "trying to use file store, but not open") ERROR.Println(STR, "trying to use file store, but not open")
return nil return nil
} }
files, err = ioutil.ReadDir(store.directory) entries, err := os.ReadDir(store.directory)
chkerr(err) chkerr(err)
files := make(fileInfos, 0, len(entries))
for _, entry := range entries {
info, err := entry.Info()
chkerr(err)
files = append(files, info)
}
sort.Sort(files) sort.Sort(files)
for _, f := range files { for _, f := range files {
DEBUG.Println(STR, "file in All():", f.Name()) DEBUG.Println(STR, "file in All():", f.Name())
@@ -246,7 +251,7 @@ func exists(file string) bool {
return true return true
} }
type fileInfos []os.FileInfo type fileInfos []fs.FileInfo
func (f fileInfos) Len() int { func (f fileInfos) Len() int {
return len(f) return len(f)

View File

@@ -444,24 +444,23 @@ func startComms(conn net.Conn, // Network connection (must be active)
} }
// ackFunc acknowledges a packet // ackFunc acknowledges a packet
// WARNING the function returned must not be called if the comms routine is shutting down or not running // WARNING sendAck may be called at any time (even after the connection is dead). At the time of writing ACK sent after
// (it needs outgoing comms in order to send the acknowledgement). Currently this is only called from // connection loss will be dropped (this is not ideal)
// matchAndDispatch which will be shutdown before the comms are func ackFunc(sendAck func(*PacketAndToken), persist Store, packet *packets.PublishPacket) func() {
func ackFunc(oboundP chan *PacketAndToken, persist Store, packet *packets.PublishPacket) func() {
return func() { return func() {
switch packet.Qos { switch packet.Qos {
case 2: case 2:
pr := packets.NewControlPacket(packets.Pubrec).(*packets.PubrecPacket) pr := packets.NewControlPacket(packets.Pubrec).(*packets.PubrecPacket)
pr.MessageID = packet.MessageID pr.MessageID = packet.MessageID
DEBUG.Println(NET, "putting pubrec msg on obound") DEBUG.Println(NET, "putting pubrec msg on obound")
oboundP <- &PacketAndToken{p: pr, t: nil} sendAck(&PacketAndToken{p: pr, t: nil})
DEBUG.Println(NET, "done putting pubrec msg on obound") DEBUG.Println(NET, "done putting pubrec msg on obound")
case 1: case 1:
pa := packets.NewControlPacket(packets.Puback).(*packets.PubackPacket) pa := packets.NewControlPacket(packets.Puback).(*packets.PubackPacket)
pa.MessageID = packet.MessageID pa.MessageID = packet.MessageID
DEBUG.Println(NET, "putting puback msg on obound") DEBUG.Println(NET, "putting puback msg on obound")
persistOutbound(persist, pa) persistOutbound(persist, pa) // May fail if store has been closed
oboundP <- &PacketAndToken{p: pa, t: nil} sendAck(&PacketAndToken{p: pa, t: nil})
DEBUG.Println(NET, "done putting puback msg on obound") DEBUG.Println(NET, "done putting puback msg on obound")
case 0: case 0:
// do nothing, since there is no need to send an ack packet back // do nothing, since there is no need to send an ack packet back

View File

@@ -40,22 +40,17 @@ import (
func openConnection(uri *url.URL, tlsc *tls.Config, timeout time.Duration, headers http.Header, websocketOptions *WebsocketOptions, dialer *net.Dialer) (net.Conn, error) { func openConnection(uri *url.URL, tlsc *tls.Config, timeout time.Duration, headers http.Header, websocketOptions *WebsocketOptions, dialer *net.Dialer) (net.Conn, error) {
switch uri.Scheme { switch uri.Scheme {
case "ws": case "ws":
conn, err := NewWebsocket(uri.String(), nil, timeout, headers, websocketOptions) dialURI := *uri // #623 - Gorilla Websockets does not accept URL's where uri.User != nil
dialURI.User = nil
conn, err := NewWebsocket(dialURI.String(), nil, timeout, headers, websocketOptions)
return conn, err return conn, err
case "wss": case "wss":
conn, err := NewWebsocket(uri.String(), tlsc, timeout, headers, websocketOptions) dialURI := *uri // #623 - Gorilla Websockets does not accept URL's where uri.User != nil
dialURI.User = nil
conn, err := NewWebsocket(dialURI.String(), tlsc, timeout, headers, websocketOptions)
return conn, err return conn, err
case "mqtt", "tcp": case "mqtt", "tcp":
allProxy := os.Getenv("all_proxy") proxyDialer := proxy.FromEnvironmentUsing(dialer)
if len(allProxy) == 0 {
conn, err := dialer.Dial("tcp", uri.Host)
if err != nil {
return nil, err
}
return conn, nil
}
proxyDialer := proxy.FromEnvironment()
conn, err := proxyDialer.Dial("tcp", uri.Host) conn, err := proxyDialer.Dial("tcp", uri.Host)
if err != nil { if err != nil {
return nil, err return nil, err

View File

@@ -62,93 +62,99 @@ type ConnectionAttemptHandler func(broker *url.URL, tlsCfg *tls.Config) *tls.Con
// Does not carry out any MQTT specific handshakes. // Does not carry out any MQTT specific handshakes.
type OpenConnectionFunc func(uri *url.URL, options ClientOptions) (net.Conn, error) type OpenConnectionFunc func(uri *url.URL, options ClientOptions) (net.Conn, error)
// ConnectionNotificationHandler is invoked for any type of connection event.
type ConnectionNotificationHandler func(Client, ConnectionNotification)
// ClientOptions contains configurable options for an Client. Note that these should be set using the // ClientOptions contains configurable options for an Client. Note that these should be set using the
// relevant methods (e.g. AddBroker) rather than directly. See those functions for information on usage. // relevant methods (e.g. AddBroker) rather than directly. See those functions for information on usage.
// WARNING: Create the below using NewClientOptions unless you have a compelling reason not to. It is easy // WARNING: Create the below using NewClientOptions unless you have a compelling reason not to. It is easy
// to create a configuration with difficult to trace issues (e.g. Mosquitto 2.0.12+ will reject connections // to create a configuration with difficult to trace issues (e.g. Mosquitto 2.0.12+ will reject connections
// with KeepAlive=0 by default). // with KeepAlive=0 by default).
type ClientOptions struct { type ClientOptions struct {
Servers []*url.URL Servers []*url.URL
ClientID string ClientID string
Username string Username string
Password string Password string
CredentialsProvider CredentialsProvider CredentialsProvider CredentialsProvider
CleanSession bool CleanSession bool
Order bool Order bool
WillEnabled bool WillEnabled bool
WillTopic string WillTopic string
WillPayload []byte WillPayload []byte
WillQos byte WillQos byte
WillRetained bool WillRetained bool
ProtocolVersion uint ProtocolVersion uint
protocolVersionExplicit bool protocolVersionExplicit bool
TLSConfig *tls.Config TLSConfig *tls.Config
KeepAlive int64 // Warning: Some brokers may reject connections with Keepalive = 0. KeepAlive int64 // Warning: Some brokers may reject connections with Keepalive = 0.
PingTimeout time.Duration PingTimeout time.Duration
ConnectTimeout time.Duration ConnectTimeout time.Duration
MaxReconnectInterval time.Duration MaxReconnectInterval time.Duration
AutoReconnect bool AutoReconnect bool
ConnectRetryInterval time.Duration ConnectRetryInterval time.Duration
ConnectRetry bool ConnectRetry bool
Store Store Store Store
DefaultPublishHandler MessageHandler DefaultPublishHandler MessageHandler
OnConnect OnConnectHandler OnConnect OnConnectHandler
OnConnectionLost ConnectionLostHandler OnConnectionLost ConnectionLostHandler
OnReconnecting ReconnectHandler OnReconnecting ReconnectHandler
OnConnectAttempt ConnectionAttemptHandler OnConnectAttempt ConnectionAttemptHandler
WriteTimeout time.Duration OnConnectionNotification ConnectionNotificationHandler
MessageChannelDepth uint WriteTimeout time.Duration
ResumeSubs bool MessageChannelDepth uint
HTTPHeaders http.Header ResumeSubs bool
WebsocketOptions *WebsocketOptions HTTPHeaders http.Header
MaxResumePubInFlight int // // 0 = no limit; otherwise this is the maximum simultaneous messages sent while resuming WebsocketOptions *WebsocketOptions
Dialer *net.Dialer MaxResumePubInFlight int // // 0 = no limit; otherwise this is the maximum simultaneous messages sent while resuming
CustomOpenConnectionFn OpenConnectionFunc Dialer *net.Dialer
AutoAckDisabled bool CustomOpenConnectionFn OpenConnectionFunc
AutoAckDisabled bool
} }
// NewClientOptions will create a new ClientClientOptions type with some // NewClientOptions will create a new ClientClientOptions type with some
// default values. // default values.
// Port: 1883 //
// CleanSession: True // Port: 1883
// Order: True (note: it is recommended that this be set to FALSE unless order is important) // CleanSession: True
// KeepAlive: 30 (seconds) // Order: True (note: it is recommended that this be set to FALSE unless order is important)
// ConnectTimeout: 30 (seconds) // KeepAlive: 30 (seconds)
// MaxReconnectInterval 10 (minutes) // ConnectTimeout: 30 (seconds)
// AutoReconnect: True // MaxReconnectInterval 10 (minutes)
// AutoReconnect: True
func NewClientOptions() *ClientOptions { func NewClientOptions() *ClientOptions {
o := &ClientOptions{ o := &ClientOptions{
Servers: nil, Servers: nil,
ClientID: "", ClientID: "",
Username: "", Username: "",
Password: "", Password: "",
CleanSession: true, CleanSession: true,
Order: true, Order: true,
WillEnabled: false, WillEnabled: false,
WillTopic: "", WillTopic: "",
WillPayload: nil, WillPayload: nil,
WillQos: 0, WillQos: 0,
WillRetained: false, WillRetained: false,
ProtocolVersion: 0, ProtocolVersion: 0,
protocolVersionExplicit: false, protocolVersionExplicit: false,
KeepAlive: 30, KeepAlive: 30,
PingTimeout: 10 * time.Second, PingTimeout: 10 * time.Second,
ConnectTimeout: 30 * time.Second, ConnectTimeout: 30 * time.Second,
MaxReconnectInterval: 10 * time.Minute, MaxReconnectInterval: 10 * time.Minute,
AutoReconnect: true, AutoReconnect: true,
ConnectRetryInterval: 30 * time.Second, ConnectRetryInterval: 30 * time.Second,
ConnectRetry: false, ConnectRetry: false,
Store: nil, Store: nil,
OnConnect: nil, OnConnect: nil,
OnConnectionLost: DefaultConnectionLostHandler, OnConnectionLost: DefaultConnectionLostHandler,
OnConnectAttempt: nil, OnConnectAttempt: nil,
WriteTimeout: 0, // 0 represents timeout disabled OnConnectionNotification: nil,
ResumeSubs: false, WriteTimeout: 0, // 0 represents timeout disabled
HTTPHeaders: make(map[string][]string), ResumeSubs: false,
WebsocketOptions: &WebsocketOptions{}, HTTPHeaders: make(map[string][]string),
Dialer: &net.Dialer{Timeout: 30 * time.Second}, WebsocketOptions: &WebsocketOptions{},
CustomOpenConnectionFn: nil, Dialer: &net.Dialer{Timeout: 30 * time.Second},
AutoAckDisabled: false, CustomOpenConnectionFn: nil,
AutoAckDisabled: false,
} }
return o return o
} }
@@ -355,6 +361,13 @@ func (o *ClientOptions) SetConnectionAttemptHandler(onConnectAttempt ConnectionA
return o return o
} }
// SetConnectionNotificationHandler sets the ConnectionNotificationHandler callback to receive all types of connection
// events.
func (o *ClientOptions) SetConnectionNotificationHandler(onConnectionNotification ConnectionNotificationHandler) *ClientOptions {
o.OnConnectionNotification = onConnectionNotification
return o
}
// SetWriteTimeout puts a limit on how long a mqtt publish should block until it unblocks with a // SetWriteTimeout puts a limit on how long a mqtt publish should block until it unblocks with a
// timeout error. A duration of 0 never times out. Default never times out // timeout error. A duration of 0 never times out. Default never times out
func (o *ClientOptions) SetWriteTimeout(t time.Duration) *ClientOptions { func (o *ClientOptions) SetWriteTimeout(t time.Duration) *ClientOptions {
@@ -450,6 +463,7 @@ func (o *ClientOptions) SetCustomOpenConnectionFn(customOpenConnectionFn OpenCon
} }
// SetAutoAckDisabled enables or disables the Automated Acking of Messages received by the handler. // SetAutoAckDisabled enables or disables the Automated Acking of Messages received by the handler.
//
// By default it is set to false. Setting it to true will disable the auto-ack globally. // By default it is set to false. Setting it to true will disable the auto-ack globally.
func (o *ClientOptions) SetAutoAckDisabled(autoAckDisabled bool) *ClientOptions { func (o *ClientOptions) SetAutoAckDisabled(autoAckDisabled bool) *ClientOptions {
o.AutoAckDisabled = autoAckDisabled o.AutoAckDisabled = autoAckDisabled

View File

@@ -30,6 +30,21 @@ type ClientOptionsReader struct {
options *ClientOptions options *ClientOptions
} }
// NewOptionsReader creates a ClientOptionsReader, this should only be used for mocking purposes.
//
// An example implementation:
//
// func (c *mqttClientMock) OptionsReader() mqtt.ClientOptionsReader {
// opts := mqtt.NewClientOptions()
// opts.UserName = "TestUserName"
// return mqtt.NewOptionsReader(opts)
// }
func NewOptionsReader(o *ClientOptions) ClientOptionsReader {
return ClientOptionsReader{
options: o,
}
}
// Servers returns a slice of the servers defined in the clientoptions // Servers returns a slice of the servers defined in the clientoptions
func (r *ClientOptionsReader) Servers() []*url.URL { func (r *ClientOptionsReader) Servers() []*url.URL {
s := make([]*url.URL, len(r.options.Servers)) s := make([]*url.URL, len(r.options.Servers))

View File

@@ -330,6 +330,11 @@ func decodeBytes(b io.Reader) ([]byte, error) {
} }
func encodeBytes(field []byte) []byte { func encodeBytes(field []byte) []byte {
// Attempting to encode more than 65,535 bytes would lead to an unexpected 16-bit length and extra data written
// (which would be parsed as later parts of the message). The safest option is to truncate.
if len(field) > 65535 {
field = field[0:65535]
}
fieldLength := make([]byte, 2) fieldLength := make([]byte, 2)
binary.BigEndian.PutUint16(fieldLength, uint16(len(field))) binary.BigEndian.PutUint16(fieldLength, uint16(len(field)))
return append(fieldLength, field...) return append(fieldLength, field...)

View File

@@ -32,16 +32,16 @@ import (
func keepalive(c *client, conn io.Writer) { func keepalive(c *client, conn io.Writer) {
defer c.workers.Done() defer c.workers.Done()
DEBUG.Println(PNG, "keepalive starting") DEBUG.Println(PNG, "keepalive starting")
var checkInterval int64 var checkInterval time.Duration
var pingSent time.Time var pingSent time.Time
if c.options.KeepAlive > 10 { if c.options.KeepAlive > 10 {
checkInterval = 5 checkInterval = 5 * time.Second
} else { } else {
checkInterval = c.options.KeepAlive / 2 checkInterval = time.Duration(c.options.KeepAlive) * time.Second / 4
} }
intervalTicker := time.NewTicker(time.Duration(checkInterval * int64(time.Second))) intervalTicker := time.NewTicker(checkInterval)
defer intervalTicker.Stop() defer intervalTicker.Stop()
for { for {

View File

@@ -136,60 +136,41 @@ func (r *router) setDefaultHandler(handler MessageHandler) {
// associated callback (or the defaultHandler, if one exists and no other route matched). If // associated callback (or the defaultHandler, if one exists and no other route matched). If
// anything is sent down the stop channel the function will end. // anything is sent down the stop channel the function will end.
func (r *router) matchAndDispatch(messages <-chan *packets.PublishPacket, order bool, client *client) <-chan *PacketAndToken { func (r *router) matchAndDispatch(messages <-chan *packets.PublishPacket, order bool, client *client) <-chan *PacketAndToken {
var wg sync.WaitGroup ackChan := make(chan *PacketAndToken) // Channel returned to caller; closed when goroutine terminates
ackOutChan := make(chan *PacketAndToken) // Channel returned to caller; closed when messages channel closed
var ackInChan chan *PacketAndToken // ACKs generated by ackFunc get put onto this channel
stopAckCopy := make(chan struct{}) // Closure requests stop of go routine copying ackInChan to ackOutChan // In some cases message acknowledgments may come through after shutdown (connection is down etc). Where this is the
ackCopyStopped := make(chan struct{}) // Closure indicates that it is safe to close ackOutChan // case we need to accept any such requests and then ignore them. Note that this is not a perfect solution, if we
goRoutinesDone := make(chan struct{}) // closed on wg.Done() // have reconnected, and the session is still live, then the Ack really should be sent (see Issus #726)
if order { var ackMutex sync.RWMutex
ackInChan = ackOutChan // When order = true no go routines are used so safe to use one channel and close when done sendAckChan := ackChan // This will be set to nil before ackChan is closed
} else { sendAck := func(ack *PacketAndToken) {
// When order = false ACK messages are sent in go routines so ackInChan cannot be closed until all goroutines done ackMutex.RLock()
ackInChan = make(chan *PacketAndToken) defer ackMutex.RUnlock()
go func() { // go routine to copy from ackInChan to ackOutChan until stopped if sendAckChan != nil {
for { sendAckChan <- ack
select { } else {
case a := <-ackInChan: DEBUG.Println(ROU, "matchAndDispatch received acknowledgment after processing stopped (ACK dropped).")
ackOutChan <- a }
case <-stopAckCopy:
close(ackCopyStopped) // Signal main go routine that it is safe to close ackOutChan
for {
select {
case <-ackInChan: // drain ackInChan to ensure all goRoutines can complete cleanly (ACK dropped)
DEBUG.Println(ROU, "matchAndDispatch received acknowledgment after processing stopped (ACK dropped).")
case <-goRoutinesDone:
close(ackInChan) // Nothing further should be sent (a panic is probably better than silent failure)
DEBUG.Println(ROU, "matchAndDispatch order=false copy goroutine exiting.")
return
}
}
}
}
}()
} }
go func() { // Main go routine handling inbound messages go func() { // Main go routine handling inbound messages
var handlers []MessageHandler
for message := range messages { for message := range messages {
// DEBUG.Println(ROU, "matchAndDispatch received message") // DEBUG.Println(ROU, "matchAndDispatch received message")
sent := false sent := false
r.RLock() r.RLock()
m := messageFromPublish(message, ackFunc(ackInChan, client.persist, message)) m := messageFromPublish(message, ackFunc(sendAck, client.persist, message))
var handlers []MessageHandler
for e := r.routes.Front(); e != nil; e = e.Next() { for e := r.routes.Front(); e != nil; e = e.Next() {
if e.Value.(*route).match(message.TopicName) { if e.Value.(*route).match(message.TopicName) {
if order { if order {
handlers = append(handlers, e.Value.(*route).callback) handlers = append(handlers, e.Value.(*route).callback)
} else { } else {
hd := e.Value.(*route).callback hd := e.Value.(*route).callback
wg.Add(1)
go func() { go func() {
hd(client, m) hd(client, m)
if !client.options.AutoAckDisabled { if !client.options.AutoAckDisabled {
m.Ack() m.Ack()
} }
wg.Done()
}() }()
} }
sent = true sent = true
@@ -200,13 +181,11 @@ func (r *router) matchAndDispatch(messages <-chan *packets.PublishPacket, order
if order { if order {
handlers = append(handlers, r.defaultHandler) handlers = append(handlers, r.defaultHandler)
} else { } else {
wg.Add(1)
go func() { go func() {
r.defaultHandler(client, m) r.defaultHandler(client, m)
if !client.options.AutoAckDisabled { if !client.options.AutoAckDisabled {
m.Ack() m.Ack()
} }
wg.Done()
}() }()
} }
} else { } else {
@@ -214,26 +193,22 @@ func (r *router) matchAndDispatch(messages <-chan *packets.PublishPacket, order
} }
} }
r.RUnlock() r.RUnlock()
for _, handler := range handlers { if order {
handler(client, m) for _, handler := range handlers {
if !client.options.AutoAckDisabled { handler(client, m)
m.Ack() if !client.options.AutoAckDisabled {
m.Ack()
}
} }
handlers = handlers[:0]
} }
// DEBUG.Println(ROU, "matchAndDispatch handled message") // DEBUG.Println(ROU, "matchAndDispatch handled message")
} }
if order { ackMutex.Lock()
close(ackOutChan) sendAckChan = nil
} else { // Ensure that nothing further will be written to ackOutChan before closing it ackMutex.Unlock()
close(stopAckCopy) close(ackChan) // as sendAckChan is now nil nothing further will be sent on this
<-ackCopyStopped
close(ackOutChan)
go func() {
wg.Wait() // Note: If this remains running then the user has handlers that are not returning
close(goRoutinesDone)
}()
}
DEBUG.Println(ROU, "matchAndDispatch exiting") DEBUG.Println(ROU, "matchAndDispatch exiting")
}() }()
return ackOutChan return ackChan
} }

View File

@@ -17,6 +17,7 @@
package mqtt package mqtt
import ( import (
"errors"
"sync" "sync"
"time" "time"
@@ -202,3 +203,20 @@ type UnsubscribeToken struct {
type DisconnectToken struct { type DisconnectToken struct {
baseToken baseToken
} }
// TimedOut is the error returned by WaitTimeout when the timeout expires
var TimedOut = errors.New("context canceled")
// WaitTokenTimeout is a utility function used to simplify the use of token.WaitTimeout
// token.WaitTimeout may return `false` due to time out but t.Error() still results
// in nil.
// `if t := client.X(); t.WaitTimeout(time.Second) && t.Error() != nil {` may evaluate
// to false even if the operation fails.
// It is important to note that if TimedOut is returned, then the operation may still be running
// and could eventually complete successfully.
func WaitTokenTimeout(t Token, d time.Duration) error {
if !t.WaitTimeout(d) {
return TimedOut
}
return t.Error()
}

View File

@@ -1,3 +0,0 @@
# This source code refers to The Go Authors for copyright purposes.
# The master list of authors is in the main Go distribution,
# visible at http://tip.golang.org/AUTHORS.

View File

@@ -1,3 +0,0 @@
# This source code was written by the Go contributors.
# The master list of contributors is in the main Go distribution,
# visible at http://tip.golang.org/CONTRIBUTORS.

View File

@@ -1,28 +0,0 @@
Copyright 2010 The Go Authors. All rights reserved.
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 Inc. 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.

View File

@@ -1,324 +0,0 @@
// Copyright 2019 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 proto
import (
"errors"
"fmt"
"google.golang.org/protobuf/encoding/prototext"
"google.golang.org/protobuf/encoding/protowire"
"google.golang.org/protobuf/runtime/protoimpl"
)
const (
WireVarint = 0
WireFixed32 = 5
WireFixed64 = 1
WireBytes = 2
WireStartGroup = 3
WireEndGroup = 4
)
// EncodeVarint returns the varint encoded bytes of v.
func EncodeVarint(v uint64) []byte {
return protowire.AppendVarint(nil, v)
}
// SizeVarint returns the length of the varint encoded bytes of v.
// This is equal to len(EncodeVarint(v)).
func SizeVarint(v uint64) int {
return protowire.SizeVarint(v)
}
// DecodeVarint parses a varint encoded integer from b,
// returning the integer value and the length of the varint.
// It returns (0, 0) if there is a parse error.
func DecodeVarint(b []byte) (uint64, int) {
v, n := protowire.ConsumeVarint(b)
if n < 0 {
return 0, 0
}
return v, n
}
// Buffer is a buffer for encoding and decoding the protobuf wire format.
// It may be reused between invocations to reduce memory usage.
type Buffer struct {
buf []byte
idx int
deterministic bool
}
// NewBuffer allocates a new Buffer initialized with buf,
// where the contents of buf are considered the unread portion of the buffer.
func NewBuffer(buf []byte) *Buffer {
return &Buffer{buf: buf}
}
// SetDeterministic specifies whether to use deterministic serialization.
//
// Deterministic serialization guarantees that for a given binary, equal
// messages will always be serialized to the same bytes. This implies:
//
// - Repeated serialization of a message will return the same bytes.
// - Different processes of the same binary (which may be executing on
// different machines) will serialize equal messages to the same bytes.
//
// Note that the deterministic serialization is NOT canonical across
// languages. It is not guaranteed to remain stable over time. It is unstable
// across different builds with schema changes due to unknown fields.
// Users who need canonical serialization (e.g., persistent storage in a
// canonical form, fingerprinting, etc.) should define their own
// canonicalization specification and implement their own serializer rather
// than relying on this API.
//
// If deterministic serialization is requested, map entries will be sorted
// by keys in lexographical order. This is an implementation detail and
// subject to change.
func (b *Buffer) SetDeterministic(deterministic bool) {
b.deterministic = deterministic
}
// SetBuf sets buf as the internal buffer,
// where the contents of buf are considered the unread portion of the buffer.
func (b *Buffer) SetBuf(buf []byte) {
b.buf = buf
b.idx = 0
}
// Reset clears the internal buffer of all written and unread data.
func (b *Buffer) Reset() {
b.buf = b.buf[:0]
b.idx = 0
}
// Bytes returns the internal buffer.
func (b *Buffer) Bytes() []byte {
return b.buf
}
// Unread returns the unread portion of the buffer.
func (b *Buffer) Unread() []byte {
return b.buf[b.idx:]
}
// Marshal appends the wire-format encoding of m to the buffer.
func (b *Buffer) Marshal(m Message) error {
var err error
b.buf, err = marshalAppend(b.buf, m, b.deterministic)
return err
}
// Unmarshal parses the wire-format message in the buffer and
// places the decoded results in m.
// It does not reset m before unmarshaling.
func (b *Buffer) Unmarshal(m Message) error {
err := UnmarshalMerge(b.Unread(), m)
b.idx = len(b.buf)
return err
}
type unknownFields struct{ XXX_unrecognized protoimpl.UnknownFields }
func (m *unknownFields) String() string { panic("not implemented") }
func (m *unknownFields) Reset() { panic("not implemented") }
func (m *unknownFields) ProtoMessage() { panic("not implemented") }
// DebugPrint dumps the encoded bytes of b with a header and footer including s
// to stdout. This is only intended for debugging.
func (*Buffer) DebugPrint(s string, b []byte) {
m := MessageReflect(new(unknownFields))
m.SetUnknown(b)
b, _ = prototext.MarshalOptions{AllowPartial: true, Indent: "\t"}.Marshal(m.Interface())
fmt.Printf("==== %s ====\n%s==== %s ====\n", s, b, s)
}
// EncodeVarint appends an unsigned varint encoding to the buffer.
func (b *Buffer) EncodeVarint(v uint64) error {
b.buf = protowire.AppendVarint(b.buf, v)
return nil
}
// EncodeZigzag32 appends a 32-bit zig-zag varint encoding to the buffer.
func (b *Buffer) EncodeZigzag32(v uint64) error {
return b.EncodeVarint(uint64((uint32(v) << 1) ^ uint32((int32(v) >> 31))))
}
// EncodeZigzag64 appends a 64-bit zig-zag varint encoding to the buffer.
func (b *Buffer) EncodeZigzag64(v uint64) error {
return b.EncodeVarint(uint64((uint64(v) << 1) ^ uint64((int64(v) >> 63))))
}
// EncodeFixed32 appends a 32-bit little-endian integer to the buffer.
func (b *Buffer) EncodeFixed32(v uint64) error {
b.buf = protowire.AppendFixed32(b.buf, uint32(v))
return nil
}
// EncodeFixed64 appends a 64-bit little-endian integer to the buffer.
func (b *Buffer) EncodeFixed64(v uint64) error {
b.buf = protowire.AppendFixed64(b.buf, uint64(v))
return nil
}
// EncodeRawBytes appends a length-prefixed raw bytes to the buffer.
func (b *Buffer) EncodeRawBytes(v []byte) error {
b.buf = protowire.AppendBytes(b.buf, v)
return nil
}
// EncodeStringBytes appends a length-prefixed raw bytes to the buffer.
// It does not validate whether v contains valid UTF-8.
func (b *Buffer) EncodeStringBytes(v string) error {
b.buf = protowire.AppendString(b.buf, v)
return nil
}
// EncodeMessage appends a length-prefixed encoded message to the buffer.
func (b *Buffer) EncodeMessage(m Message) error {
var err error
b.buf = protowire.AppendVarint(b.buf, uint64(Size(m)))
b.buf, err = marshalAppend(b.buf, m, b.deterministic)
return err
}
// DecodeVarint consumes an encoded unsigned varint from the buffer.
func (b *Buffer) DecodeVarint() (uint64, error) {
v, n := protowire.ConsumeVarint(b.buf[b.idx:])
if n < 0 {
return 0, protowire.ParseError(n)
}
b.idx += n
return uint64(v), nil
}
// DecodeZigzag32 consumes an encoded 32-bit zig-zag varint from the buffer.
func (b *Buffer) DecodeZigzag32() (uint64, error) {
v, err := b.DecodeVarint()
if err != nil {
return 0, err
}
return uint64((uint32(v) >> 1) ^ uint32((int32(v&1)<<31)>>31)), nil
}
// DecodeZigzag64 consumes an encoded 64-bit zig-zag varint from the buffer.
func (b *Buffer) DecodeZigzag64() (uint64, error) {
v, err := b.DecodeVarint()
if err != nil {
return 0, err
}
return uint64((uint64(v) >> 1) ^ uint64((int64(v&1)<<63)>>63)), nil
}
// DecodeFixed32 consumes a 32-bit little-endian integer from the buffer.
func (b *Buffer) DecodeFixed32() (uint64, error) {
v, n := protowire.ConsumeFixed32(b.buf[b.idx:])
if n < 0 {
return 0, protowire.ParseError(n)
}
b.idx += n
return uint64(v), nil
}
// DecodeFixed64 consumes a 64-bit little-endian integer from the buffer.
func (b *Buffer) DecodeFixed64() (uint64, error) {
v, n := protowire.ConsumeFixed64(b.buf[b.idx:])
if n < 0 {
return 0, protowire.ParseError(n)
}
b.idx += n
return uint64(v), nil
}
// DecodeRawBytes consumes a length-prefixed raw bytes from the buffer.
// If alloc is specified, it returns a copy the raw bytes
// rather than a sub-slice of the buffer.
func (b *Buffer) DecodeRawBytes(alloc bool) ([]byte, error) {
v, n := protowire.ConsumeBytes(b.buf[b.idx:])
if n < 0 {
return nil, protowire.ParseError(n)
}
b.idx += n
if alloc {
v = append([]byte(nil), v...)
}
return v, nil
}
// DecodeStringBytes consumes a length-prefixed raw bytes from the buffer.
// It does not validate whether the raw bytes contain valid UTF-8.
func (b *Buffer) DecodeStringBytes() (string, error) {
v, n := protowire.ConsumeString(b.buf[b.idx:])
if n < 0 {
return "", protowire.ParseError(n)
}
b.idx += n
return v, nil
}
// DecodeMessage consumes a length-prefixed message from the buffer.
// It does not reset m before unmarshaling.
func (b *Buffer) DecodeMessage(m Message) error {
v, err := b.DecodeRawBytes(false)
if err != nil {
return err
}
return UnmarshalMerge(v, m)
}
// DecodeGroup consumes a message group from the buffer.
// It assumes that the start group marker has already been consumed and
// consumes all bytes until (and including the end group marker).
// It does not reset m before unmarshaling.
func (b *Buffer) DecodeGroup(m Message) error {
v, n, err := consumeGroup(b.buf[b.idx:])
if err != nil {
return err
}
b.idx += n
return UnmarshalMerge(v, m)
}
// consumeGroup parses b until it finds an end group marker, returning
// the raw bytes of the message (excluding the end group marker) and the
// the total length of the message (including the end group marker).
func consumeGroup(b []byte) ([]byte, int, error) {
b0 := b
depth := 1 // assume this follows a start group marker
for {
_, wtyp, tagLen := protowire.ConsumeTag(b)
if tagLen < 0 {
return nil, 0, protowire.ParseError(tagLen)
}
b = b[tagLen:]
var valLen int
switch wtyp {
case protowire.VarintType:
_, valLen = protowire.ConsumeVarint(b)
case protowire.Fixed32Type:
_, valLen = protowire.ConsumeFixed32(b)
case protowire.Fixed64Type:
_, valLen = protowire.ConsumeFixed64(b)
case protowire.BytesType:
_, valLen = protowire.ConsumeBytes(b)
case protowire.StartGroupType:
depth++
case protowire.EndGroupType:
depth--
default:
return nil, 0, errors.New("proto: cannot parse reserved wire type")
}
if valLen < 0 {
return nil, 0, protowire.ParseError(valLen)
}
b = b[valLen:]
if depth == 0 {
return b0[:len(b0)-len(b)-tagLen], len(b0) - len(b), nil
}
}
}

View File

@@ -1,63 +0,0 @@
// Copyright 2019 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 proto
import (
"google.golang.org/protobuf/reflect/protoreflect"
)
// SetDefaults sets unpopulated scalar fields to their default values.
// Fields within a oneof are not set even if they have a default value.
// SetDefaults is recursively called upon any populated message fields.
func SetDefaults(m Message) {
if m != nil {
setDefaults(MessageReflect(m))
}
}
func setDefaults(m protoreflect.Message) {
fds := m.Descriptor().Fields()
for i := 0; i < fds.Len(); i++ {
fd := fds.Get(i)
if !m.Has(fd) {
if fd.HasDefault() && fd.ContainingOneof() == nil {
v := fd.Default()
if fd.Kind() == protoreflect.BytesKind {
v = protoreflect.ValueOf(append([]byte(nil), v.Bytes()...)) // copy the default bytes
}
m.Set(fd, v)
}
continue
}
}
m.Range(func(fd protoreflect.FieldDescriptor, v protoreflect.Value) bool {
switch {
// Handle singular message.
case fd.Cardinality() != protoreflect.Repeated:
if fd.Message() != nil {
setDefaults(m.Get(fd).Message())
}
// Handle list of messages.
case fd.IsList():
if fd.Message() != nil {
ls := m.Get(fd).List()
for i := 0; i < ls.Len(); i++ {
setDefaults(ls.Get(i).Message())
}
}
// Handle map of messages.
case fd.IsMap():
if fd.MapValue().Message() != nil {
ms := m.Get(fd).Map()
ms.Range(func(_ protoreflect.MapKey, v protoreflect.Value) bool {
setDefaults(v.Message())
return true
})
}
}
return true
})
}

View File

@@ -1,113 +0,0 @@
// Copyright 2018 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 proto
import (
"encoding/json"
"errors"
"fmt"
"strconv"
protoV2 "google.golang.org/protobuf/proto"
)
var (
// Deprecated: No longer returned.
ErrNil = errors.New("proto: Marshal called with nil")
// Deprecated: No longer returned.
ErrTooLarge = errors.New("proto: message encodes to over 2 GB")
// Deprecated: No longer returned.
ErrInternalBadWireType = errors.New("proto: internal error: bad wiretype for oneof")
)
// Deprecated: Do not use.
type Stats struct{ Emalloc, Dmalloc, Encode, Decode, Chit, Cmiss, Size uint64 }
// Deprecated: Do not use.
func GetStats() Stats { return Stats{} }
// Deprecated: Do not use.
func MarshalMessageSet(interface{}) ([]byte, error) {
return nil, errors.New("proto: not implemented")
}
// Deprecated: Do not use.
func UnmarshalMessageSet([]byte, interface{}) error {
return errors.New("proto: not implemented")
}
// Deprecated: Do not use.
func MarshalMessageSetJSON(interface{}) ([]byte, error) {
return nil, errors.New("proto: not implemented")
}
// Deprecated: Do not use.
func UnmarshalMessageSetJSON([]byte, interface{}) error {
return errors.New("proto: not implemented")
}
// Deprecated: Do not use.
func RegisterMessageSetType(Message, int32, string) {}
// Deprecated: Do not use.
func EnumName(m map[int32]string, v int32) string {
s, ok := m[v]
if ok {
return s
}
return strconv.Itoa(int(v))
}
// Deprecated: Do not use.
func UnmarshalJSONEnum(m map[string]int32, data []byte, enumName string) (int32, error) {
if data[0] == '"' {
// New style: enums are strings.
var repr string
if err := json.Unmarshal(data, &repr); err != nil {
return -1, err
}
val, ok := m[repr]
if !ok {
return 0, fmt.Errorf("unrecognized enum %s value %q", enumName, repr)
}
return val, nil
}
// Old style: enums are ints.
var val int32
if err := json.Unmarshal(data, &val); err != nil {
return 0, fmt.Errorf("cannot unmarshal %#q into enum %s", data, enumName)
}
return val, nil
}
// Deprecated: Do not use; this type existed for intenal-use only.
type InternalMessageInfo struct{}
// Deprecated: Do not use; this method existed for intenal-use only.
func (*InternalMessageInfo) DiscardUnknown(m Message) {
DiscardUnknown(m)
}
// Deprecated: Do not use; this method existed for intenal-use only.
func (*InternalMessageInfo) Marshal(b []byte, m Message, deterministic bool) ([]byte, error) {
return protoV2.MarshalOptions{Deterministic: deterministic}.MarshalAppend(b, MessageV2(m))
}
// Deprecated: Do not use; this method existed for intenal-use only.
func (*InternalMessageInfo) Merge(dst, src Message) {
protoV2.Merge(MessageV2(dst), MessageV2(src))
}
// Deprecated: Do not use; this method existed for intenal-use only.
func (*InternalMessageInfo) Size(m Message) int {
return protoV2.Size(MessageV2(m))
}
// Deprecated: Do not use; this method existed for intenal-use only.
func (*InternalMessageInfo) Unmarshal(m Message, b []byte) error {
return protoV2.UnmarshalOptions{Merge: true}.Unmarshal(b, MessageV2(m))
}

View File

@@ -1,58 +0,0 @@
// Copyright 2019 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 proto
import (
"google.golang.org/protobuf/reflect/protoreflect"
)
// DiscardUnknown recursively discards all unknown fields from this message
// and all embedded messages.
//
// When unmarshaling a message with unrecognized fields, the tags and values
// of such fields are preserved in the Message. This allows a later call to
// marshal to be able to produce a message that continues to have those
// unrecognized fields. To avoid this, DiscardUnknown is used to
// explicitly clear the unknown fields after unmarshaling.
func DiscardUnknown(m Message) {
if m != nil {
discardUnknown(MessageReflect(m))
}
}
func discardUnknown(m protoreflect.Message) {
m.Range(func(fd protoreflect.FieldDescriptor, val protoreflect.Value) bool {
switch {
// Handle singular message.
case fd.Cardinality() != protoreflect.Repeated:
if fd.Message() != nil {
discardUnknown(m.Get(fd).Message())
}
// Handle list of messages.
case fd.IsList():
if fd.Message() != nil {
ls := m.Get(fd).List()
for i := 0; i < ls.Len(); i++ {
discardUnknown(ls.Get(i).Message())
}
}
// Handle map of messages.
case fd.IsMap():
if fd.MapValue().Message() != nil {
ms := m.Get(fd).Map()
ms.Range(func(_ protoreflect.MapKey, v protoreflect.Value) bool {
discardUnknown(v.Message())
return true
})
}
}
return true
})
// Discard unknown fields.
if len(m.GetUnknown()) > 0 {
m.SetUnknown(nil)
}
}

View File

@@ -1,356 +0,0 @@
// Copyright 2010 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 proto
import (
"errors"
"fmt"
"reflect"
"google.golang.org/protobuf/encoding/protowire"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/reflect/protoreflect"
"google.golang.org/protobuf/reflect/protoregistry"
"google.golang.org/protobuf/runtime/protoiface"
"google.golang.org/protobuf/runtime/protoimpl"
)
type (
// ExtensionDesc represents an extension descriptor and
// is used to interact with an extension field in a message.
//
// Variables of this type are generated in code by protoc-gen-go.
ExtensionDesc = protoimpl.ExtensionInfo
// ExtensionRange represents a range of message extensions.
// Used in code generated by protoc-gen-go.
ExtensionRange = protoiface.ExtensionRangeV1
// Deprecated: Do not use; this is an internal type.
Extension = protoimpl.ExtensionFieldV1
// Deprecated: Do not use; this is an internal type.
XXX_InternalExtensions = protoimpl.ExtensionFields
)
// ErrMissingExtension reports whether the extension was not present.
var ErrMissingExtension = errors.New("proto: missing extension")
var errNotExtendable = errors.New("proto: not an extendable proto.Message")
// HasExtension reports whether the extension field is present in m
// either as an explicitly populated field or as an unknown field.
func HasExtension(m Message, xt *ExtensionDesc) (has bool) {
mr := MessageReflect(m)
if mr == nil || !mr.IsValid() {
return false
}
// Check whether any populated known field matches the field number.
xtd := xt.TypeDescriptor()
if isValidExtension(mr.Descriptor(), xtd) {
has = mr.Has(xtd)
} else {
mr.Range(func(fd protoreflect.FieldDescriptor, _ protoreflect.Value) bool {
has = int32(fd.Number()) == xt.Field
return !has
})
}
// Check whether any unknown field matches the field number.
for b := mr.GetUnknown(); !has && len(b) > 0; {
num, _, n := protowire.ConsumeField(b)
has = int32(num) == xt.Field
b = b[n:]
}
return has
}
// ClearExtension removes the extension field from m
// either as an explicitly populated field or as an unknown field.
func ClearExtension(m Message, xt *ExtensionDesc) {
mr := MessageReflect(m)
if mr == nil || !mr.IsValid() {
return
}
xtd := xt.TypeDescriptor()
if isValidExtension(mr.Descriptor(), xtd) {
mr.Clear(xtd)
} else {
mr.Range(func(fd protoreflect.FieldDescriptor, _ protoreflect.Value) bool {
if int32(fd.Number()) == xt.Field {
mr.Clear(fd)
return false
}
return true
})
}
clearUnknown(mr, fieldNum(xt.Field))
}
// ClearAllExtensions clears all extensions from m.
// This includes populated fields and unknown fields in the extension range.
func ClearAllExtensions(m Message) {
mr := MessageReflect(m)
if mr == nil || !mr.IsValid() {
return
}
mr.Range(func(fd protoreflect.FieldDescriptor, _ protoreflect.Value) bool {
if fd.IsExtension() {
mr.Clear(fd)
}
return true
})
clearUnknown(mr, mr.Descriptor().ExtensionRanges())
}
// GetExtension retrieves a proto2 extended field from m.
//
// If the descriptor is type complete (i.e., ExtensionDesc.ExtensionType is non-nil),
// then GetExtension parses the encoded field and returns a Go value of the specified type.
// If the field is not present, then the default value is returned (if one is specified),
// otherwise ErrMissingExtension is reported.
//
// If the descriptor is type incomplete (i.e., ExtensionDesc.ExtensionType is nil),
// then GetExtension returns the raw encoded bytes for the extension field.
func GetExtension(m Message, xt *ExtensionDesc) (interface{}, error) {
mr := MessageReflect(m)
if mr == nil || !mr.IsValid() || mr.Descriptor().ExtensionRanges().Len() == 0 {
return nil, errNotExtendable
}
// Retrieve the unknown fields for this extension field.
var bo protoreflect.RawFields
for bi := mr.GetUnknown(); len(bi) > 0; {
num, _, n := protowire.ConsumeField(bi)
if int32(num) == xt.Field {
bo = append(bo, bi[:n]...)
}
bi = bi[n:]
}
// For type incomplete descriptors, only retrieve the unknown fields.
if xt.ExtensionType == nil {
return []byte(bo), nil
}
// If the extension field only exists as unknown fields, unmarshal it.
// This is rarely done since proto.Unmarshal eagerly unmarshals extensions.
xtd := xt.TypeDescriptor()
if !isValidExtension(mr.Descriptor(), xtd) {
return nil, fmt.Errorf("proto: bad extended type; %T does not extend %T", xt.ExtendedType, m)
}
if !mr.Has(xtd) && len(bo) > 0 {
m2 := mr.New()
if err := (proto.UnmarshalOptions{
Resolver: extensionResolver{xt},
}.Unmarshal(bo, m2.Interface())); err != nil {
return nil, err
}
if m2.Has(xtd) {
mr.Set(xtd, m2.Get(xtd))
clearUnknown(mr, fieldNum(xt.Field))
}
}
// Check whether the message has the extension field set or a default.
var pv protoreflect.Value
switch {
case mr.Has(xtd):
pv = mr.Get(xtd)
case xtd.HasDefault():
pv = xtd.Default()
default:
return nil, ErrMissingExtension
}
v := xt.InterfaceOf(pv)
rv := reflect.ValueOf(v)
if isScalarKind(rv.Kind()) {
rv2 := reflect.New(rv.Type())
rv2.Elem().Set(rv)
v = rv2.Interface()
}
return v, nil
}
// extensionResolver is a custom extension resolver that stores a single
// extension type that takes precedence over the global registry.
type extensionResolver struct{ xt protoreflect.ExtensionType }
func (r extensionResolver) FindExtensionByName(field protoreflect.FullName) (protoreflect.ExtensionType, error) {
if xtd := r.xt.TypeDescriptor(); xtd.FullName() == field {
return r.xt, nil
}
return protoregistry.GlobalTypes.FindExtensionByName(field)
}
func (r extensionResolver) FindExtensionByNumber(message protoreflect.FullName, field protoreflect.FieldNumber) (protoreflect.ExtensionType, error) {
if xtd := r.xt.TypeDescriptor(); xtd.ContainingMessage().FullName() == message && xtd.Number() == field {
return r.xt, nil
}
return protoregistry.GlobalTypes.FindExtensionByNumber(message, field)
}
// GetExtensions returns a list of the extensions values present in m,
// corresponding with the provided list of extension descriptors, xts.
// If an extension is missing in m, the corresponding value is nil.
func GetExtensions(m Message, xts []*ExtensionDesc) ([]interface{}, error) {
mr := MessageReflect(m)
if mr == nil || !mr.IsValid() {
return nil, errNotExtendable
}
vs := make([]interface{}, len(xts))
for i, xt := range xts {
v, err := GetExtension(m, xt)
if err != nil {
if err == ErrMissingExtension {
continue
}
return vs, err
}
vs[i] = v
}
return vs, nil
}
// SetExtension sets an extension field in m to the provided value.
func SetExtension(m Message, xt *ExtensionDesc, v interface{}) error {
mr := MessageReflect(m)
if mr == nil || !mr.IsValid() || mr.Descriptor().ExtensionRanges().Len() == 0 {
return errNotExtendable
}
rv := reflect.ValueOf(v)
if reflect.TypeOf(v) != reflect.TypeOf(xt.ExtensionType) {
return fmt.Errorf("proto: bad extension value type. got: %T, want: %T", v, xt.ExtensionType)
}
if rv.Kind() == reflect.Ptr {
if rv.IsNil() {
return fmt.Errorf("proto: SetExtension called with nil value of type %T", v)
}
if isScalarKind(rv.Elem().Kind()) {
v = rv.Elem().Interface()
}
}
xtd := xt.TypeDescriptor()
if !isValidExtension(mr.Descriptor(), xtd) {
return fmt.Errorf("proto: bad extended type; %T does not extend %T", xt.ExtendedType, m)
}
mr.Set(xtd, xt.ValueOf(v))
clearUnknown(mr, fieldNum(xt.Field))
return nil
}
// SetRawExtension inserts b into the unknown fields of m.
//
// Deprecated: Use Message.ProtoReflect.SetUnknown instead.
func SetRawExtension(m Message, fnum int32, b []byte) {
mr := MessageReflect(m)
if mr == nil || !mr.IsValid() {
return
}
// Verify that the raw field is valid.
for b0 := b; len(b0) > 0; {
num, _, n := protowire.ConsumeField(b0)
if int32(num) != fnum {
panic(fmt.Sprintf("mismatching field number: got %d, want %d", num, fnum))
}
b0 = b0[n:]
}
ClearExtension(m, &ExtensionDesc{Field: fnum})
mr.SetUnknown(append(mr.GetUnknown(), b...))
}
// ExtensionDescs returns a list of extension descriptors found in m,
// containing descriptors for both populated extension fields in m and
// also unknown fields of m that are in the extension range.
// For the later case, an type incomplete descriptor is provided where only
// the ExtensionDesc.Field field is populated.
// The order of the extension descriptors is undefined.
func ExtensionDescs(m Message) ([]*ExtensionDesc, error) {
mr := MessageReflect(m)
if mr == nil || !mr.IsValid() || mr.Descriptor().ExtensionRanges().Len() == 0 {
return nil, errNotExtendable
}
// Collect a set of known extension descriptors.
extDescs := make(map[protoreflect.FieldNumber]*ExtensionDesc)
mr.Range(func(fd protoreflect.FieldDescriptor, v protoreflect.Value) bool {
if fd.IsExtension() {
xt := fd.(protoreflect.ExtensionTypeDescriptor)
if xd, ok := xt.Type().(*ExtensionDesc); ok {
extDescs[fd.Number()] = xd
}
}
return true
})
// Collect a set of unknown extension descriptors.
extRanges := mr.Descriptor().ExtensionRanges()
for b := mr.GetUnknown(); len(b) > 0; {
num, _, n := protowire.ConsumeField(b)
if extRanges.Has(num) && extDescs[num] == nil {
extDescs[num] = nil
}
b = b[n:]
}
// Transpose the set of descriptors into a list.
var xts []*ExtensionDesc
for num, xt := range extDescs {
if xt == nil {
xt = &ExtensionDesc{Field: int32(num)}
}
xts = append(xts, xt)
}
return xts, nil
}
// isValidExtension reports whether xtd is a valid extension descriptor for md.
func isValidExtension(md protoreflect.MessageDescriptor, xtd protoreflect.ExtensionTypeDescriptor) bool {
return xtd.ContainingMessage() == md && md.ExtensionRanges().Has(xtd.Number())
}
// isScalarKind reports whether k is a protobuf scalar kind (except bytes).
// This function exists for historical reasons since the representation of
// scalars differs between v1 and v2, where v1 uses *T and v2 uses T.
func isScalarKind(k reflect.Kind) bool {
switch k {
case reflect.Bool, reflect.Int32, reflect.Int64, reflect.Uint32, reflect.Uint64, reflect.Float32, reflect.Float64, reflect.String:
return true
default:
return false
}
}
// clearUnknown removes unknown fields from m where remover.Has reports true.
func clearUnknown(m protoreflect.Message, remover interface {
Has(protoreflect.FieldNumber) bool
}) {
var bo protoreflect.RawFields
for bi := m.GetUnknown(); len(bi) > 0; {
num, _, n := protowire.ConsumeField(bi)
if !remover.Has(num) {
bo = append(bo, bi[:n]...)
}
bi = bi[n:]
}
if bi := m.GetUnknown(); len(bi) != len(bo) {
m.SetUnknown(bo)
}
}
type fieldNum protoreflect.FieldNumber
func (n1 fieldNum) Has(n2 protoreflect.FieldNumber) bool {
return protoreflect.FieldNumber(n1) == n2
}

View File

@@ -1,306 +0,0 @@
// Copyright 2010 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 proto
import (
"fmt"
"reflect"
"strconv"
"strings"
"sync"
"google.golang.org/protobuf/reflect/protoreflect"
"google.golang.org/protobuf/runtime/protoimpl"
)
// StructProperties represents protocol buffer type information for a
// generated protobuf message in the open-struct API.
//
// Deprecated: Do not use.
type StructProperties struct {
// Prop are the properties for each field.
//
// Fields belonging to a oneof are stored in OneofTypes instead, with a
// single Properties representing the parent oneof held here.
//
// The order of Prop matches the order of fields in the Go struct.
// Struct fields that are not related to protobufs have a "XXX_" prefix
// in the Properties.Name and must be ignored by the user.
Prop []*Properties
// OneofTypes contains information about the oneof fields in this message.
// It is keyed by the protobuf field name.
OneofTypes map[string]*OneofProperties
}
// Properties represents the type information for a protobuf message field.
//
// Deprecated: Do not use.
type Properties struct {
// Name is a placeholder name with little meaningful semantic value.
// If the name has an "XXX_" prefix, the entire Properties must be ignored.
Name string
// OrigName is the protobuf field name or oneof name.
OrigName string
// JSONName is the JSON name for the protobuf field.
JSONName string
// Enum is a placeholder name for enums.
// For historical reasons, this is neither the Go name for the enum,
// nor the protobuf name for the enum.
Enum string // Deprecated: Do not use.
// Weak contains the full name of the weakly referenced message.
Weak string
// Wire is a string representation of the wire type.
Wire string
// WireType is the protobuf wire type for the field.
WireType int
// Tag is the protobuf field number.
Tag int
// Required reports whether this is a required field.
Required bool
// Optional reports whether this is a optional field.
Optional bool
// Repeated reports whether this is a repeated field.
Repeated bool
// Packed reports whether this is a packed repeated field of scalars.
Packed bool
// Proto3 reports whether this field operates under the proto3 syntax.
Proto3 bool
// Oneof reports whether this field belongs within a oneof.
Oneof bool
// Default is the default value in string form.
Default string
// HasDefault reports whether the field has a default value.
HasDefault bool
// MapKeyProp is the properties for the key field for a map field.
MapKeyProp *Properties
// MapValProp is the properties for the value field for a map field.
MapValProp *Properties
}
// OneofProperties represents the type information for a protobuf oneof.
//
// Deprecated: Do not use.
type OneofProperties struct {
// Type is a pointer to the generated wrapper type for the field value.
// This is nil for messages that are not in the open-struct API.
Type reflect.Type
// Field is the index into StructProperties.Prop for the containing oneof.
Field int
// Prop is the properties for the field.
Prop *Properties
}
// String formats the properties in the protobuf struct field tag style.
func (p *Properties) String() string {
s := p.Wire
s += "," + strconv.Itoa(p.Tag)
if p.Required {
s += ",req"
}
if p.Optional {
s += ",opt"
}
if p.Repeated {
s += ",rep"
}
if p.Packed {
s += ",packed"
}
s += ",name=" + p.OrigName
if p.JSONName != "" {
s += ",json=" + p.JSONName
}
if len(p.Enum) > 0 {
s += ",enum=" + p.Enum
}
if len(p.Weak) > 0 {
s += ",weak=" + p.Weak
}
if p.Proto3 {
s += ",proto3"
}
if p.Oneof {
s += ",oneof"
}
if p.HasDefault {
s += ",def=" + p.Default
}
return s
}
// Parse populates p by parsing a string in the protobuf struct field tag style.
func (p *Properties) Parse(tag string) {
// For example: "bytes,49,opt,name=foo,def=hello!"
for len(tag) > 0 {
i := strings.IndexByte(tag, ',')
if i < 0 {
i = len(tag)
}
switch s := tag[:i]; {
case strings.HasPrefix(s, "name="):
p.OrigName = s[len("name="):]
case strings.HasPrefix(s, "json="):
p.JSONName = s[len("json="):]
case strings.HasPrefix(s, "enum="):
p.Enum = s[len("enum="):]
case strings.HasPrefix(s, "weak="):
p.Weak = s[len("weak="):]
case strings.Trim(s, "0123456789") == "":
n, _ := strconv.ParseUint(s, 10, 32)
p.Tag = int(n)
case s == "opt":
p.Optional = true
case s == "req":
p.Required = true
case s == "rep":
p.Repeated = true
case s == "varint" || s == "zigzag32" || s == "zigzag64":
p.Wire = s
p.WireType = WireVarint
case s == "fixed32":
p.Wire = s
p.WireType = WireFixed32
case s == "fixed64":
p.Wire = s
p.WireType = WireFixed64
case s == "bytes":
p.Wire = s
p.WireType = WireBytes
case s == "group":
p.Wire = s
p.WireType = WireStartGroup
case s == "packed":
p.Packed = true
case s == "proto3":
p.Proto3 = true
case s == "oneof":
p.Oneof = true
case strings.HasPrefix(s, "def="):
// The default tag is special in that everything afterwards is the
// default regardless of the presence of commas.
p.HasDefault = true
p.Default, i = tag[len("def="):], len(tag)
}
tag = strings.TrimPrefix(tag[i:], ",")
}
}
// Init populates the properties from a protocol buffer struct tag.
//
// Deprecated: Do not use.
func (p *Properties) Init(typ reflect.Type, name, tag string, f *reflect.StructField) {
p.Name = name
p.OrigName = name
if tag == "" {
return
}
p.Parse(tag)
if typ != nil && typ.Kind() == reflect.Map {
p.MapKeyProp = new(Properties)
p.MapKeyProp.Init(nil, "Key", f.Tag.Get("protobuf_key"), nil)
p.MapValProp = new(Properties)
p.MapValProp.Init(nil, "Value", f.Tag.Get("protobuf_val"), nil)
}
}
var propertiesCache sync.Map // map[reflect.Type]*StructProperties
// GetProperties returns the list of properties for the type represented by t,
// which must be a generated protocol buffer message in the open-struct API,
// where protobuf message fields are represented by exported Go struct fields.
//
// Deprecated: Use protobuf reflection instead.
func GetProperties(t reflect.Type) *StructProperties {
if p, ok := propertiesCache.Load(t); ok {
return p.(*StructProperties)
}
p, _ := propertiesCache.LoadOrStore(t, newProperties(t))
return p.(*StructProperties)
}
func newProperties(t reflect.Type) *StructProperties {
if t.Kind() != reflect.Struct {
panic(fmt.Sprintf("%v is not a generated message in the open-struct API", t))
}
var hasOneof bool
prop := new(StructProperties)
// Construct a list of properties for each field in the struct.
for i := 0; i < t.NumField(); i++ {
p := new(Properties)
f := t.Field(i)
tagField := f.Tag.Get("protobuf")
p.Init(f.Type, f.Name, tagField, &f)
tagOneof := f.Tag.Get("protobuf_oneof")
if tagOneof != "" {
hasOneof = true
p.OrigName = tagOneof
}
// Rename unrelated struct fields with the "XXX_" prefix since so much
// user code simply checks for this to exclude special fields.
if tagField == "" && tagOneof == "" && !strings.HasPrefix(p.Name, "XXX_") {
p.Name = "XXX_" + p.Name
p.OrigName = "XXX_" + p.OrigName
} else if p.Weak != "" {
p.Name = p.OrigName // avoid possible "XXX_" prefix on weak field
}
prop.Prop = append(prop.Prop, p)
}
// Construct a mapping of oneof field names to properties.
if hasOneof {
var oneofWrappers []interface{}
if fn, ok := reflect.PtrTo(t).MethodByName("XXX_OneofFuncs"); ok {
oneofWrappers = fn.Func.Call([]reflect.Value{reflect.Zero(fn.Type.In(0))})[3].Interface().([]interface{})
}
if fn, ok := reflect.PtrTo(t).MethodByName("XXX_OneofWrappers"); ok {
oneofWrappers = fn.Func.Call([]reflect.Value{reflect.Zero(fn.Type.In(0))})[0].Interface().([]interface{})
}
if m, ok := reflect.Zero(reflect.PtrTo(t)).Interface().(protoreflect.ProtoMessage); ok {
if m, ok := m.ProtoReflect().(interface{ ProtoMessageInfo() *protoimpl.MessageInfo }); ok {
oneofWrappers = m.ProtoMessageInfo().OneofWrappers
}
}
prop.OneofTypes = make(map[string]*OneofProperties)
for _, wrapper := range oneofWrappers {
p := &OneofProperties{
Type: reflect.ValueOf(wrapper).Type(), // *T
Prop: new(Properties),
}
f := p.Type.Elem().Field(0)
p.Prop.Name = f.Name
p.Prop.Parse(f.Tag.Get("protobuf"))
// Determine the struct field that contains this oneof.
// Each wrapper is assignable to exactly one parent field.
var foundOneof bool
for i := 0; i < t.NumField() && !foundOneof; i++ {
if p.Type.AssignableTo(t.Field(i).Type) {
p.Field = i
foundOneof = true
}
}
if !foundOneof {
panic(fmt.Sprintf("%v is not a generated message in the open-struct API", t))
}
prop.OneofTypes[p.Prop.OrigName] = p
}
}
return prop
}
func (sp *StructProperties) Len() int { return len(sp.Prop) }
func (sp *StructProperties) Less(i, j int) bool { return false }
func (sp *StructProperties) Swap(i, j int) { return }

View File

@@ -1,167 +0,0 @@
// Copyright 2019 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 proto provides functionality for handling protocol buffer messages.
// In particular, it provides marshaling and unmarshaling between a protobuf
// message and the binary wire format.
//
// See https://developers.google.com/protocol-buffers/docs/gotutorial for
// more information.
//
// Deprecated: Use the "google.golang.org/protobuf/proto" package instead.
package proto
import (
protoV2 "google.golang.org/protobuf/proto"
"google.golang.org/protobuf/reflect/protoreflect"
"google.golang.org/protobuf/runtime/protoiface"
"google.golang.org/protobuf/runtime/protoimpl"
)
const (
ProtoPackageIsVersion1 = true
ProtoPackageIsVersion2 = true
ProtoPackageIsVersion3 = true
ProtoPackageIsVersion4 = true
)
// GeneratedEnum is any enum type generated by protoc-gen-go
// which is a named int32 kind.
// This type exists for documentation purposes.
type GeneratedEnum interface{}
// GeneratedMessage is any message type generated by protoc-gen-go
// which is a pointer to a named struct kind.
// This type exists for documentation purposes.
type GeneratedMessage interface{}
// Message is a protocol buffer message.
//
// This is the v1 version of the message interface and is marginally better
// than an empty interface as it lacks any method to programatically interact
// with the contents of the message.
//
// A v2 message is declared in "google.golang.org/protobuf/proto".Message and
// exposes protobuf reflection as a first-class feature of the interface.
//
// To convert a v1 message to a v2 message, use the MessageV2 function.
// To convert a v2 message to a v1 message, use the MessageV1 function.
type Message = protoiface.MessageV1
// MessageV1 converts either a v1 or v2 message to a v1 message.
// It returns nil if m is nil.
func MessageV1(m GeneratedMessage) protoiface.MessageV1 {
return protoimpl.X.ProtoMessageV1Of(m)
}
// MessageV2 converts either a v1 or v2 message to a v2 message.
// It returns nil if m is nil.
func MessageV2(m GeneratedMessage) protoV2.Message {
return protoimpl.X.ProtoMessageV2Of(m)
}
// MessageReflect returns a reflective view for a message.
// It returns nil if m is nil.
func MessageReflect(m Message) protoreflect.Message {
return protoimpl.X.MessageOf(m)
}
// Marshaler is implemented by messages that can marshal themselves.
// This interface is used by the following functions: Size, Marshal,
// Buffer.Marshal, and Buffer.EncodeMessage.
//
// Deprecated: Do not implement.
type Marshaler interface {
// Marshal formats the encoded bytes of the message.
// It should be deterministic and emit valid protobuf wire data.
// The caller takes ownership of the returned buffer.
Marshal() ([]byte, error)
}
// Unmarshaler is implemented by messages that can unmarshal themselves.
// This interface is used by the following functions: Unmarshal, UnmarshalMerge,
// Buffer.Unmarshal, Buffer.DecodeMessage, and Buffer.DecodeGroup.
//
// Deprecated: Do not implement.
type Unmarshaler interface {
// Unmarshal parses the encoded bytes of the protobuf wire input.
// The provided buffer is only valid for during method call.
// It should not reset the receiver message.
Unmarshal([]byte) error
}
// Merger is implemented by messages that can merge themselves.
// This interface is used by the following functions: Clone and Merge.
//
// Deprecated: Do not implement.
type Merger interface {
// Merge merges the contents of src into the receiver message.
// It clones all data structures in src such that it aliases no mutable
// memory referenced by src.
Merge(src Message)
}
// RequiredNotSetError is an error type returned when
// marshaling or unmarshaling a message with missing required fields.
type RequiredNotSetError struct {
err error
}
func (e *RequiredNotSetError) Error() string {
if e.err != nil {
return e.err.Error()
}
return "proto: required field not set"
}
func (e *RequiredNotSetError) RequiredNotSet() bool {
return true
}
func checkRequiredNotSet(m protoV2.Message) error {
if err := protoV2.CheckInitialized(m); err != nil {
return &RequiredNotSetError{err: err}
}
return nil
}
// Clone returns a deep copy of src.
func Clone(src Message) Message {
return MessageV1(protoV2.Clone(MessageV2(src)))
}
// Merge merges src into dst, which must be messages of the same type.
//
// Populated scalar fields in src are copied to dst, while populated
// singular messages in src are merged into dst by recursively calling Merge.
// The elements of every list field in src is appended to the corresponded
// list fields in dst. The entries of every map field in src is copied into
// the corresponding map field in dst, possibly replacing existing entries.
// The unknown fields of src are appended to the unknown fields of dst.
func Merge(dst, src Message) {
protoV2.Merge(MessageV2(dst), MessageV2(src))
}
// Equal reports whether two messages are equal.
// If two messages marshal to the same bytes under deterministic serialization,
// then Equal is guaranteed to report true.
//
// Two messages are equal if they are the same protobuf message type,
// have the same set of populated known and extension field values,
// and the same set of unknown fields values.
//
// Scalar values are compared with the equivalent of the == operator in Go,
// except bytes values which are compared using bytes.Equal and
// floating point values which specially treat NaNs as equal.
// Message values are compared by recursively calling Equal.
// Lists are equal if each element value is also equal.
// Maps are equal if they have the same set of keys, where the pair of values
// for each key is also equal.
func Equal(x, y Message) bool {
return protoV2.Equal(MessageV2(x), MessageV2(y))
}
func isMessageSet(md protoreflect.MessageDescriptor) bool {
ms, ok := md.(interface{ IsMessageSet() bool })
return ok && ms.IsMessageSet()
}

View File

@@ -1,317 +0,0 @@
// Copyright 2019 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 proto
import (
"bytes"
"compress/gzip"
"fmt"
"io/ioutil"
"reflect"
"strings"
"sync"
"google.golang.org/protobuf/reflect/protodesc"
"google.golang.org/protobuf/reflect/protoreflect"
"google.golang.org/protobuf/reflect/protoregistry"
"google.golang.org/protobuf/runtime/protoimpl"
)
// filePath is the path to the proto source file.
type filePath = string // e.g., "google/protobuf/descriptor.proto"
// fileDescGZIP is the compressed contents of the encoded FileDescriptorProto.
type fileDescGZIP = []byte
var fileCache sync.Map // map[filePath]fileDescGZIP
// RegisterFile is called from generated code to register the compressed
// FileDescriptorProto with the file path for a proto source file.
//
// Deprecated: Use protoregistry.GlobalFiles.RegisterFile instead.
func RegisterFile(s filePath, d fileDescGZIP) {
// Decompress the descriptor.
zr, err := gzip.NewReader(bytes.NewReader(d))
if err != nil {
panic(fmt.Sprintf("proto: invalid compressed file descriptor: %v", err))
}
b, err := ioutil.ReadAll(zr)
if err != nil {
panic(fmt.Sprintf("proto: invalid compressed file descriptor: %v", err))
}
// Construct a protoreflect.FileDescriptor from the raw descriptor.
// Note that DescBuilder.Build automatically registers the constructed
// file descriptor with the v2 registry.
protoimpl.DescBuilder{RawDescriptor: b}.Build()
// Locally cache the raw descriptor form for the file.
fileCache.Store(s, d)
}
// FileDescriptor returns the compressed FileDescriptorProto given the file path
// for a proto source file. It returns nil if not found.
//
// Deprecated: Use protoregistry.GlobalFiles.FindFileByPath instead.
func FileDescriptor(s filePath) fileDescGZIP {
if v, ok := fileCache.Load(s); ok {
return v.(fileDescGZIP)
}
// Find the descriptor in the v2 registry.
var b []byte
if fd, _ := protoregistry.GlobalFiles.FindFileByPath(s); fd != nil {
b, _ = Marshal(protodesc.ToFileDescriptorProto(fd))
}
// Locally cache the raw descriptor form for the file.
if len(b) > 0 {
v, _ := fileCache.LoadOrStore(s, protoimpl.X.CompressGZIP(b))
return v.(fileDescGZIP)
}
return nil
}
// enumName is the name of an enum. For historical reasons, the enum name is
// neither the full Go name nor the full protobuf name of the enum.
// The name is the dot-separated combination of just the proto package that the
// enum is declared within followed by the Go type name of the generated enum.
type enumName = string // e.g., "my.proto.package.GoMessage_GoEnum"
// enumsByName maps enum values by name to their numeric counterpart.
type enumsByName = map[string]int32
// enumsByNumber maps enum values by number to their name counterpart.
type enumsByNumber = map[int32]string
var enumCache sync.Map // map[enumName]enumsByName
var numFilesCache sync.Map // map[protoreflect.FullName]int
// RegisterEnum is called from the generated code to register the mapping of
// enum value names to enum numbers for the enum identified by s.
//
// Deprecated: Use protoregistry.GlobalTypes.RegisterEnum instead.
func RegisterEnum(s enumName, _ enumsByNumber, m enumsByName) {
if _, ok := enumCache.Load(s); ok {
panic("proto: duplicate enum registered: " + s)
}
enumCache.Store(s, m)
// This does not forward registration to the v2 registry since this API
// lacks sufficient information to construct a complete v2 enum descriptor.
}
// EnumValueMap returns the mapping from enum value names to enum numbers for
// the enum of the given name. It returns nil if not found.
//
// Deprecated: Use protoregistry.GlobalTypes.FindEnumByName instead.
func EnumValueMap(s enumName) enumsByName {
if v, ok := enumCache.Load(s); ok {
return v.(enumsByName)
}
// Check whether the cache is stale. If the number of files in the current
// package differs, then it means that some enums may have been recently
// registered upstream that we do not know about.
var protoPkg protoreflect.FullName
if i := strings.LastIndexByte(s, '.'); i >= 0 {
protoPkg = protoreflect.FullName(s[:i])
}
v, _ := numFilesCache.Load(protoPkg)
numFiles, _ := v.(int)
if protoregistry.GlobalFiles.NumFilesByPackage(protoPkg) == numFiles {
return nil // cache is up-to-date; was not found earlier
}
// Update the enum cache for all enums declared in the given proto package.
numFiles = 0
protoregistry.GlobalFiles.RangeFilesByPackage(protoPkg, func(fd protoreflect.FileDescriptor) bool {
walkEnums(fd, func(ed protoreflect.EnumDescriptor) {
name := protoimpl.X.LegacyEnumName(ed)
if _, ok := enumCache.Load(name); !ok {
m := make(enumsByName)
evs := ed.Values()
for i := evs.Len() - 1; i >= 0; i-- {
ev := evs.Get(i)
m[string(ev.Name())] = int32(ev.Number())
}
enumCache.LoadOrStore(name, m)
}
})
numFiles++
return true
})
numFilesCache.Store(protoPkg, numFiles)
// Check cache again for enum map.
if v, ok := enumCache.Load(s); ok {
return v.(enumsByName)
}
return nil
}
// walkEnums recursively walks all enums declared in d.
func walkEnums(d interface {
Enums() protoreflect.EnumDescriptors
Messages() protoreflect.MessageDescriptors
}, f func(protoreflect.EnumDescriptor)) {
eds := d.Enums()
for i := eds.Len() - 1; i >= 0; i-- {
f(eds.Get(i))
}
mds := d.Messages()
for i := mds.Len() - 1; i >= 0; i-- {
walkEnums(mds.Get(i), f)
}
}
// messageName is the full name of protobuf message.
type messageName = string
var messageTypeCache sync.Map // map[messageName]reflect.Type
// RegisterType is called from generated code to register the message Go type
// for a message of the given name.
//
// Deprecated: Use protoregistry.GlobalTypes.RegisterMessage instead.
func RegisterType(m Message, s messageName) {
mt := protoimpl.X.LegacyMessageTypeOf(m, protoreflect.FullName(s))
if err := protoregistry.GlobalTypes.RegisterMessage(mt); err != nil {
panic(err)
}
messageTypeCache.Store(s, reflect.TypeOf(m))
}
// RegisterMapType is called from generated code to register the Go map type
// for a protobuf message representing a map entry.
//
// Deprecated: Do not use.
func RegisterMapType(m interface{}, s messageName) {
t := reflect.TypeOf(m)
if t.Kind() != reflect.Map {
panic(fmt.Sprintf("invalid map kind: %v", t))
}
if _, ok := messageTypeCache.Load(s); ok {
panic(fmt.Errorf("proto: duplicate proto message registered: %s", s))
}
messageTypeCache.Store(s, t)
}
// MessageType returns the message type for a named message.
// It returns nil if not found.
//
// Deprecated: Use protoregistry.GlobalTypes.FindMessageByName instead.
func MessageType(s messageName) reflect.Type {
if v, ok := messageTypeCache.Load(s); ok {
return v.(reflect.Type)
}
// Derive the message type from the v2 registry.
var t reflect.Type
if mt, _ := protoregistry.GlobalTypes.FindMessageByName(protoreflect.FullName(s)); mt != nil {
t = messageGoType(mt)
}
// If we could not get a concrete type, it is possible that it is a
// pseudo-message for a map entry.
if t == nil {
d, _ := protoregistry.GlobalFiles.FindDescriptorByName(protoreflect.FullName(s))
if md, _ := d.(protoreflect.MessageDescriptor); md != nil && md.IsMapEntry() {
kt := goTypeForField(md.Fields().ByNumber(1))
vt := goTypeForField(md.Fields().ByNumber(2))
t = reflect.MapOf(kt, vt)
}
}
// Locally cache the message type for the given name.
if t != nil {
v, _ := messageTypeCache.LoadOrStore(s, t)
return v.(reflect.Type)
}
return nil
}
func goTypeForField(fd protoreflect.FieldDescriptor) reflect.Type {
switch k := fd.Kind(); k {
case protoreflect.EnumKind:
if et, _ := protoregistry.GlobalTypes.FindEnumByName(fd.Enum().FullName()); et != nil {
return enumGoType(et)
}
return reflect.TypeOf(protoreflect.EnumNumber(0))
case protoreflect.MessageKind, protoreflect.GroupKind:
if mt, _ := protoregistry.GlobalTypes.FindMessageByName(fd.Message().FullName()); mt != nil {
return messageGoType(mt)
}
return reflect.TypeOf((*protoreflect.Message)(nil)).Elem()
default:
return reflect.TypeOf(fd.Default().Interface())
}
}
func enumGoType(et protoreflect.EnumType) reflect.Type {
return reflect.TypeOf(et.New(0))
}
func messageGoType(mt protoreflect.MessageType) reflect.Type {
return reflect.TypeOf(MessageV1(mt.Zero().Interface()))
}
// MessageName returns the full protobuf name for the given message type.
//
// Deprecated: Use protoreflect.MessageDescriptor.FullName instead.
func MessageName(m Message) messageName {
if m == nil {
return ""
}
if m, ok := m.(interface{ XXX_MessageName() messageName }); ok {
return m.XXX_MessageName()
}
return messageName(protoimpl.X.MessageDescriptorOf(m).FullName())
}
// RegisterExtension is called from the generated code to register
// the extension descriptor.
//
// Deprecated: Use protoregistry.GlobalTypes.RegisterExtension instead.
func RegisterExtension(d *ExtensionDesc) {
if err := protoregistry.GlobalTypes.RegisterExtension(d); err != nil {
panic(err)
}
}
type extensionsByNumber = map[int32]*ExtensionDesc
var extensionCache sync.Map // map[messageName]extensionsByNumber
// RegisteredExtensions returns a map of the registered extensions for the
// provided protobuf message, indexed by the extension field number.
//
// Deprecated: Use protoregistry.GlobalTypes.RangeExtensionsByMessage instead.
func RegisteredExtensions(m Message) extensionsByNumber {
// Check whether the cache is stale. If the number of extensions for
// the given message differs, then it means that some extensions were
// recently registered upstream that we do not know about.
s := MessageName(m)
v, _ := extensionCache.Load(s)
xs, _ := v.(extensionsByNumber)
if protoregistry.GlobalTypes.NumExtensionsByMessage(protoreflect.FullName(s)) == len(xs) {
return xs // cache is up-to-date
}
// Cache is stale, re-compute the extensions map.
xs = make(extensionsByNumber)
protoregistry.GlobalTypes.RangeExtensionsByMessage(protoreflect.FullName(s), func(xt protoreflect.ExtensionType) bool {
if xd, ok := xt.(*ExtensionDesc); ok {
xs[int32(xt.TypeDescriptor().Number())] = xd
} else {
// TODO: This implies that the protoreflect.ExtensionType is a
// custom type not generated by protoc-gen-go. We could try and
// convert the type to an ExtensionDesc.
}
return true
})
extensionCache.Store(s, xs)
return xs
}

View File

@@ -1,801 +0,0 @@
// Copyright 2010 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 proto
import (
"encoding"
"errors"
"fmt"
"reflect"
"strconv"
"strings"
"unicode/utf8"
"google.golang.org/protobuf/encoding/prototext"
protoV2 "google.golang.org/protobuf/proto"
"google.golang.org/protobuf/reflect/protoreflect"
"google.golang.org/protobuf/reflect/protoregistry"
)
const wrapTextUnmarshalV2 = false
// ParseError is returned by UnmarshalText.
type ParseError struct {
Message string
// Deprecated: Do not use.
Line, Offset int
}
func (e *ParseError) Error() string {
if wrapTextUnmarshalV2 {
return e.Message
}
if e.Line == 1 {
return fmt.Sprintf("line 1.%d: %v", e.Offset, e.Message)
}
return fmt.Sprintf("line %d: %v", e.Line, e.Message)
}
// UnmarshalText parses a proto text formatted string into m.
func UnmarshalText(s string, m Message) error {
if u, ok := m.(encoding.TextUnmarshaler); ok {
return u.UnmarshalText([]byte(s))
}
m.Reset()
mi := MessageV2(m)
if wrapTextUnmarshalV2 {
err := prototext.UnmarshalOptions{
AllowPartial: true,
}.Unmarshal([]byte(s), mi)
if err != nil {
return &ParseError{Message: err.Error()}
}
return checkRequiredNotSet(mi)
} else {
if err := newTextParser(s).unmarshalMessage(mi.ProtoReflect(), ""); err != nil {
return err
}
return checkRequiredNotSet(mi)
}
}
type textParser struct {
s string // remaining input
done bool // whether the parsing is finished (success or error)
backed bool // whether back() was called
offset, line int
cur token
}
type token struct {
value string
err *ParseError
line int // line number
offset int // byte number from start of input, not start of line
unquoted string // the unquoted version of value, if it was a quoted string
}
func newTextParser(s string) *textParser {
p := new(textParser)
p.s = s
p.line = 1
p.cur.line = 1
return p
}
func (p *textParser) unmarshalMessage(m protoreflect.Message, terminator string) (err error) {
md := m.Descriptor()
fds := md.Fields()
// A struct is a sequence of "name: value", terminated by one of
// '>' or '}', or the end of the input. A name may also be
// "[extension]" or "[type/url]".
//
// The whole struct can also be an expanded Any message, like:
// [type/url] < ... struct contents ... >
seen := make(map[protoreflect.FieldNumber]bool)
for {
tok := p.next()
if tok.err != nil {
return tok.err
}
if tok.value == terminator {
break
}
if tok.value == "[" {
if err := p.unmarshalExtensionOrAny(m, seen); err != nil {
return err
}
continue
}
// This is a normal, non-extension field.
name := protoreflect.Name(tok.value)
fd := fds.ByName(name)
switch {
case fd == nil:
gd := fds.ByName(protoreflect.Name(strings.ToLower(string(name))))
if gd != nil && gd.Kind() == protoreflect.GroupKind && gd.Message().Name() == name {
fd = gd
}
case fd.Kind() == protoreflect.GroupKind && fd.Message().Name() != name:
fd = nil
case fd.IsWeak() && fd.Message().IsPlaceholder():
fd = nil
}
if fd == nil {
typeName := string(md.FullName())
if m, ok := m.Interface().(Message); ok {
t := reflect.TypeOf(m)
if t.Kind() == reflect.Ptr {
typeName = t.Elem().String()
}
}
return p.errorf("unknown field name %q in %v", name, typeName)
}
if od := fd.ContainingOneof(); od != nil && m.WhichOneof(od) != nil {
return p.errorf("field '%s' would overwrite already parsed oneof '%s'", name, od.Name())
}
if fd.Cardinality() != protoreflect.Repeated && seen[fd.Number()] {
return p.errorf("non-repeated field %q was repeated", fd.Name())
}
seen[fd.Number()] = true
// Consume any colon.
if err := p.checkForColon(fd); err != nil {
return err
}
// Parse into the field.
v := m.Get(fd)
if !m.Has(fd) && (fd.IsList() || fd.IsMap() || fd.Message() != nil) {
v = m.Mutable(fd)
}
if v, err = p.unmarshalValue(v, fd); err != nil {
return err
}
m.Set(fd, v)
if err := p.consumeOptionalSeparator(); err != nil {
return err
}
}
return nil
}
func (p *textParser) unmarshalExtensionOrAny(m protoreflect.Message, seen map[protoreflect.FieldNumber]bool) error {
name, err := p.consumeExtensionOrAnyName()
if err != nil {
return err
}
// If it contains a slash, it's an Any type URL.
if slashIdx := strings.LastIndex(name, "/"); slashIdx >= 0 {
tok := p.next()
if tok.err != nil {
return tok.err
}
// consume an optional colon
if tok.value == ":" {
tok = p.next()
if tok.err != nil {
return tok.err
}
}
var terminator string
switch tok.value {
case "<":
terminator = ">"
case "{":
terminator = "}"
default:
return p.errorf("expected '{' or '<', found %q", tok.value)
}
mt, err := protoregistry.GlobalTypes.FindMessageByURL(name)
if err != nil {
return p.errorf("unrecognized message %q in google.protobuf.Any", name[slashIdx+len("/"):])
}
m2 := mt.New()
if err := p.unmarshalMessage(m2, terminator); err != nil {
return err
}
b, err := protoV2.Marshal(m2.Interface())
if err != nil {
return p.errorf("failed to marshal message of type %q: %v", name[slashIdx+len("/"):], err)
}
urlFD := m.Descriptor().Fields().ByName("type_url")
valFD := m.Descriptor().Fields().ByName("value")
if seen[urlFD.Number()] {
return p.errorf("Any message unpacked multiple times, or %q already set", urlFD.Name())
}
if seen[valFD.Number()] {
return p.errorf("Any message unpacked multiple times, or %q already set", valFD.Name())
}
m.Set(urlFD, protoreflect.ValueOfString(name))
m.Set(valFD, protoreflect.ValueOfBytes(b))
seen[urlFD.Number()] = true
seen[valFD.Number()] = true
return nil
}
xname := protoreflect.FullName(name)
xt, _ := protoregistry.GlobalTypes.FindExtensionByName(xname)
if xt == nil && isMessageSet(m.Descriptor()) {
xt, _ = protoregistry.GlobalTypes.FindExtensionByName(xname.Append("message_set_extension"))
}
if xt == nil {
return p.errorf("unrecognized extension %q", name)
}
fd := xt.TypeDescriptor()
if fd.ContainingMessage().FullName() != m.Descriptor().FullName() {
return p.errorf("extension field %q does not extend message %q", name, m.Descriptor().FullName())
}
if err := p.checkForColon(fd); err != nil {
return err
}
v := m.Get(fd)
if !m.Has(fd) && (fd.IsList() || fd.IsMap() || fd.Message() != nil) {
v = m.Mutable(fd)
}
v, err = p.unmarshalValue(v, fd)
if err != nil {
return err
}
m.Set(fd, v)
return p.consumeOptionalSeparator()
}
func (p *textParser) unmarshalValue(v protoreflect.Value, fd protoreflect.FieldDescriptor) (protoreflect.Value, error) {
tok := p.next()
if tok.err != nil {
return v, tok.err
}
if tok.value == "" {
return v, p.errorf("unexpected EOF")
}
switch {
case fd.IsList():
lv := v.List()
var err error
if tok.value == "[" {
// Repeated field with list notation, like [1,2,3].
for {
vv := lv.NewElement()
vv, err = p.unmarshalSingularValue(vv, fd)
if err != nil {
return v, err
}
lv.Append(vv)
tok := p.next()
if tok.err != nil {
return v, tok.err
}
if tok.value == "]" {
break
}
if tok.value != "," {
return v, p.errorf("Expected ']' or ',' found %q", tok.value)
}
}
return v, nil
}
// One value of the repeated field.
p.back()
vv := lv.NewElement()
vv, err = p.unmarshalSingularValue(vv, fd)
if err != nil {
return v, err
}
lv.Append(vv)
return v, nil
case fd.IsMap():
// The map entry should be this sequence of tokens:
// < key : KEY value : VALUE >
// However, implementations may omit key or value, and technically
// we should support them in any order.
var terminator string
switch tok.value {
case "<":
terminator = ">"
case "{":
terminator = "}"
default:
return v, p.errorf("expected '{' or '<', found %q", tok.value)
}
keyFD := fd.MapKey()
valFD := fd.MapValue()
mv := v.Map()
kv := keyFD.Default()
vv := mv.NewValue()
for {
tok := p.next()
if tok.err != nil {
return v, tok.err
}
if tok.value == terminator {
break
}
var err error
switch tok.value {
case "key":
if err := p.consumeToken(":"); err != nil {
return v, err
}
if kv, err = p.unmarshalSingularValue(kv, keyFD); err != nil {
return v, err
}
if err := p.consumeOptionalSeparator(); err != nil {
return v, err
}
case "value":
if err := p.checkForColon(valFD); err != nil {
return v, err
}
if vv, err = p.unmarshalSingularValue(vv, valFD); err != nil {
return v, err
}
if err := p.consumeOptionalSeparator(); err != nil {
return v, err
}
default:
p.back()
return v, p.errorf(`expected "key", "value", or %q, found %q`, terminator, tok.value)
}
}
mv.Set(kv.MapKey(), vv)
return v, nil
default:
p.back()
return p.unmarshalSingularValue(v, fd)
}
}
func (p *textParser) unmarshalSingularValue(v protoreflect.Value, fd protoreflect.FieldDescriptor) (protoreflect.Value, error) {
tok := p.next()
if tok.err != nil {
return v, tok.err
}
if tok.value == "" {
return v, p.errorf("unexpected EOF")
}
switch fd.Kind() {
case protoreflect.BoolKind:
switch tok.value {
case "true", "1", "t", "True":
return protoreflect.ValueOfBool(true), nil
case "false", "0", "f", "False":
return protoreflect.ValueOfBool(false), nil
}
case protoreflect.Int32Kind, protoreflect.Sint32Kind, protoreflect.Sfixed32Kind:
if x, err := strconv.ParseInt(tok.value, 0, 32); err == nil {
return protoreflect.ValueOfInt32(int32(x)), nil
}
// The C++ parser accepts large positive hex numbers that uses
// two's complement arithmetic to represent negative numbers.
// This feature is here for backwards compatibility with C++.
if strings.HasPrefix(tok.value, "0x") {
if x, err := strconv.ParseUint(tok.value, 0, 32); err == nil {
return protoreflect.ValueOfInt32(int32(-(int64(^x) + 1))), nil
}
}
case protoreflect.Int64Kind, protoreflect.Sint64Kind, protoreflect.Sfixed64Kind:
if x, err := strconv.ParseInt(tok.value, 0, 64); err == nil {
return protoreflect.ValueOfInt64(int64(x)), nil
}
// The C++ parser accepts large positive hex numbers that uses
// two's complement arithmetic to represent negative numbers.
// This feature is here for backwards compatibility with C++.
if strings.HasPrefix(tok.value, "0x") {
if x, err := strconv.ParseUint(tok.value, 0, 64); err == nil {
return protoreflect.ValueOfInt64(int64(-(int64(^x) + 1))), nil
}
}
case protoreflect.Uint32Kind, protoreflect.Fixed32Kind:
if x, err := strconv.ParseUint(tok.value, 0, 32); err == nil {
return protoreflect.ValueOfUint32(uint32(x)), nil
}
case protoreflect.Uint64Kind, protoreflect.Fixed64Kind:
if x, err := strconv.ParseUint(tok.value, 0, 64); err == nil {
return protoreflect.ValueOfUint64(uint64(x)), nil
}
case protoreflect.FloatKind:
// Ignore 'f' for compatibility with output generated by C++,
// but don't remove 'f' when the value is "-inf" or "inf".
v := tok.value
if strings.HasSuffix(v, "f") && v != "-inf" && v != "inf" {
v = v[:len(v)-len("f")]
}
if x, err := strconv.ParseFloat(v, 32); err == nil {
return protoreflect.ValueOfFloat32(float32(x)), nil
}
case protoreflect.DoubleKind:
// Ignore 'f' for compatibility with output generated by C++,
// but don't remove 'f' when the value is "-inf" or "inf".
v := tok.value
if strings.HasSuffix(v, "f") && v != "-inf" && v != "inf" {
v = v[:len(v)-len("f")]
}
if x, err := strconv.ParseFloat(v, 64); err == nil {
return protoreflect.ValueOfFloat64(float64(x)), nil
}
case protoreflect.StringKind:
if isQuote(tok.value[0]) {
return protoreflect.ValueOfString(tok.unquoted), nil
}
case protoreflect.BytesKind:
if isQuote(tok.value[0]) {
return protoreflect.ValueOfBytes([]byte(tok.unquoted)), nil
}
case protoreflect.EnumKind:
if x, err := strconv.ParseInt(tok.value, 0, 32); err == nil {
return protoreflect.ValueOfEnum(protoreflect.EnumNumber(x)), nil
}
vd := fd.Enum().Values().ByName(protoreflect.Name(tok.value))
if vd != nil {
return protoreflect.ValueOfEnum(vd.Number()), nil
}
case protoreflect.MessageKind, protoreflect.GroupKind:
var terminator string
switch tok.value {
case "{":
terminator = "}"
case "<":
terminator = ">"
default:
return v, p.errorf("expected '{' or '<', found %q", tok.value)
}
err := p.unmarshalMessage(v.Message(), terminator)
return v, err
default:
panic(fmt.Sprintf("invalid kind %v", fd.Kind()))
}
return v, p.errorf("invalid %v: %v", fd.Kind(), tok.value)
}
// Consume a ':' from the input stream (if the next token is a colon),
// returning an error if a colon is needed but not present.
func (p *textParser) checkForColon(fd protoreflect.FieldDescriptor) *ParseError {
tok := p.next()
if tok.err != nil {
return tok.err
}
if tok.value != ":" {
if fd.Message() == nil {
return p.errorf("expected ':', found %q", tok.value)
}
p.back()
}
return nil
}
// consumeExtensionOrAnyName consumes an extension name or an Any type URL and
// the following ']'. It returns the name or URL consumed.
func (p *textParser) consumeExtensionOrAnyName() (string, error) {
tok := p.next()
if tok.err != nil {
return "", tok.err
}
// If extension name or type url is quoted, it's a single token.
if len(tok.value) > 2 && isQuote(tok.value[0]) && tok.value[len(tok.value)-1] == tok.value[0] {
name, err := unquoteC(tok.value[1:len(tok.value)-1], rune(tok.value[0]))
if err != nil {
return "", err
}
return name, p.consumeToken("]")
}
// Consume everything up to "]"
var parts []string
for tok.value != "]" {
parts = append(parts, tok.value)
tok = p.next()
if tok.err != nil {
return "", p.errorf("unrecognized type_url or extension name: %s", tok.err)
}
if p.done && tok.value != "]" {
return "", p.errorf("unclosed type_url or extension name")
}
}
return strings.Join(parts, ""), nil
}
// consumeOptionalSeparator consumes an optional semicolon or comma.
// It is used in unmarshalMessage to provide backward compatibility.
func (p *textParser) consumeOptionalSeparator() error {
tok := p.next()
if tok.err != nil {
return tok.err
}
if tok.value != ";" && tok.value != "," {
p.back()
}
return nil
}
func (p *textParser) errorf(format string, a ...interface{}) *ParseError {
pe := &ParseError{fmt.Sprintf(format, a...), p.cur.line, p.cur.offset}
p.cur.err = pe
p.done = true
return pe
}
func (p *textParser) skipWhitespace() {
i := 0
for i < len(p.s) && (isWhitespace(p.s[i]) || p.s[i] == '#') {
if p.s[i] == '#' {
// comment; skip to end of line or input
for i < len(p.s) && p.s[i] != '\n' {
i++
}
if i == len(p.s) {
break
}
}
if p.s[i] == '\n' {
p.line++
}
i++
}
p.offset += i
p.s = p.s[i:len(p.s)]
if len(p.s) == 0 {
p.done = true
}
}
func (p *textParser) advance() {
// Skip whitespace
p.skipWhitespace()
if p.done {
return
}
// Start of non-whitespace
p.cur.err = nil
p.cur.offset, p.cur.line = p.offset, p.line
p.cur.unquoted = ""
switch p.s[0] {
case '<', '>', '{', '}', ':', '[', ']', ';', ',', '/':
// Single symbol
p.cur.value, p.s = p.s[0:1], p.s[1:len(p.s)]
case '"', '\'':
// Quoted string
i := 1
for i < len(p.s) && p.s[i] != p.s[0] && p.s[i] != '\n' {
if p.s[i] == '\\' && i+1 < len(p.s) {
// skip escaped char
i++
}
i++
}
if i >= len(p.s) || p.s[i] != p.s[0] {
p.errorf("unmatched quote")
return
}
unq, err := unquoteC(p.s[1:i], rune(p.s[0]))
if err != nil {
p.errorf("invalid quoted string %s: %v", p.s[0:i+1], err)
return
}
p.cur.value, p.s = p.s[0:i+1], p.s[i+1:len(p.s)]
p.cur.unquoted = unq
default:
i := 0
for i < len(p.s) && isIdentOrNumberChar(p.s[i]) {
i++
}
if i == 0 {
p.errorf("unexpected byte %#x", p.s[0])
return
}
p.cur.value, p.s = p.s[0:i], p.s[i:len(p.s)]
}
p.offset += len(p.cur.value)
}
// Back off the parser by one token. Can only be done between calls to next().
// It makes the next advance() a no-op.
func (p *textParser) back() { p.backed = true }
// Advances the parser and returns the new current token.
func (p *textParser) next() *token {
if p.backed || p.done {
p.backed = false
return &p.cur
}
p.advance()
if p.done {
p.cur.value = ""
} else if len(p.cur.value) > 0 && isQuote(p.cur.value[0]) {
// Look for multiple quoted strings separated by whitespace,
// and concatenate them.
cat := p.cur
for {
p.skipWhitespace()
if p.done || !isQuote(p.s[0]) {
break
}
p.advance()
if p.cur.err != nil {
return &p.cur
}
cat.value += " " + p.cur.value
cat.unquoted += p.cur.unquoted
}
p.done = false // parser may have seen EOF, but we want to return cat
p.cur = cat
}
return &p.cur
}
func (p *textParser) consumeToken(s string) error {
tok := p.next()
if tok.err != nil {
return tok.err
}
if tok.value != s {
p.back()
return p.errorf("expected %q, found %q", s, tok.value)
}
return nil
}
var errBadUTF8 = errors.New("proto: bad UTF-8")
func unquoteC(s string, quote rune) (string, error) {
// This is based on C++'s tokenizer.cc.
// Despite its name, this is *not* parsing C syntax.
// For instance, "\0" is an invalid quoted string.
// Avoid allocation in trivial cases.
simple := true
for _, r := range s {
if r == '\\' || r == quote {
simple = false
break
}
}
if simple {
return s, nil
}
buf := make([]byte, 0, 3*len(s)/2)
for len(s) > 0 {
r, n := utf8.DecodeRuneInString(s)
if r == utf8.RuneError && n == 1 {
return "", errBadUTF8
}
s = s[n:]
if r != '\\' {
if r < utf8.RuneSelf {
buf = append(buf, byte(r))
} else {
buf = append(buf, string(r)...)
}
continue
}
ch, tail, err := unescape(s)
if err != nil {
return "", err
}
buf = append(buf, ch...)
s = tail
}
return string(buf), nil
}
func unescape(s string) (ch string, tail string, err error) {
r, n := utf8.DecodeRuneInString(s)
if r == utf8.RuneError && n == 1 {
return "", "", errBadUTF8
}
s = s[n:]
switch r {
case 'a':
return "\a", s, nil
case 'b':
return "\b", s, nil
case 'f':
return "\f", s, nil
case 'n':
return "\n", s, nil
case 'r':
return "\r", s, nil
case 't':
return "\t", s, nil
case 'v':
return "\v", s, nil
case '?':
return "?", s, nil // trigraph workaround
case '\'', '"', '\\':
return string(r), s, nil
case '0', '1', '2', '3', '4', '5', '6', '7':
if len(s) < 2 {
return "", "", fmt.Errorf(`\%c requires 2 following digits`, r)
}
ss := string(r) + s[:2]
s = s[2:]
i, err := strconv.ParseUint(ss, 8, 8)
if err != nil {
return "", "", fmt.Errorf(`\%s contains non-octal digits`, ss)
}
return string([]byte{byte(i)}), s, nil
case 'x', 'X', 'u', 'U':
var n int
switch r {
case 'x', 'X':
n = 2
case 'u':
n = 4
case 'U':
n = 8
}
if len(s) < n {
return "", "", fmt.Errorf(`\%c requires %d following digits`, r, n)
}
ss := s[:n]
s = s[n:]
i, err := strconv.ParseUint(ss, 16, 64)
if err != nil {
return "", "", fmt.Errorf(`\%c%s contains non-hexadecimal digits`, r, ss)
}
if r == 'x' || r == 'X' {
return string([]byte{byte(i)}), s, nil
}
if i > utf8.MaxRune {
return "", "", fmt.Errorf(`\%c%s is not a valid Unicode code point`, r, ss)
}
return string(rune(i)), s, nil
}
return "", "", fmt.Errorf(`unknown escape \%c`, r)
}
func isIdentOrNumberChar(c byte) bool {
switch {
case 'A' <= c && c <= 'Z', 'a' <= c && c <= 'z':
return true
case '0' <= c && c <= '9':
return true
}
switch c {
case '-', '+', '.', '_':
return true
}
return false
}
func isWhitespace(c byte) bool {
switch c {
case ' ', '\t', '\n', '\r':
return true
}
return false
}
func isQuote(c byte) bool {
switch c {
case '"', '\'':
return true
}
return false
}

View File

@@ -1,560 +0,0 @@
// Copyright 2010 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 proto
import (
"bytes"
"encoding"
"fmt"
"io"
"math"
"sort"
"strings"
"google.golang.org/protobuf/encoding/prototext"
"google.golang.org/protobuf/encoding/protowire"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/reflect/protoreflect"
"google.golang.org/protobuf/reflect/protoregistry"
)
const wrapTextMarshalV2 = false
// TextMarshaler is a configurable text format marshaler.
type TextMarshaler struct {
Compact bool // use compact text format (one line)
ExpandAny bool // expand google.protobuf.Any messages of known types
}
// Marshal writes the proto text format of m to w.
func (tm *TextMarshaler) Marshal(w io.Writer, m Message) error {
b, err := tm.marshal(m)
if len(b) > 0 {
if _, err := w.Write(b); err != nil {
return err
}
}
return err
}
// Text returns a proto text formatted string of m.
func (tm *TextMarshaler) Text(m Message) string {
b, _ := tm.marshal(m)
return string(b)
}
func (tm *TextMarshaler) marshal(m Message) ([]byte, error) {
mr := MessageReflect(m)
if mr == nil || !mr.IsValid() {
return []byte("<nil>"), nil
}
if wrapTextMarshalV2 {
if m, ok := m.(encoding.TextMarshaler); ok {
return m.MarshalText()
}
opts := prototext.MarshalOptions{
AllowPartial: true,
EmitUnknown: true,
}
if !tm.Compact {
opts.Indent = " "
}
if !tm.ExpandAny {
opts.Resolver = (*protoregistry.Types)(nil)
}
return opts.Marshal(mr.Interface())
} else {
w := &textWriter{
compact: tm.Compact,
expandAny: tm.ExpandAny,
complete: true,
}
if m, ok := m.(encoding.TextMarshaler); ok {
b, err := m.MarshalText()
if err != nil {
return nil, err
}
w.Write(b)
return w.buf, nil
}
err := w.writeMessage(mr)
return w.buf, err
}
}
var (
defaultTextMarshaler = TextMarshaler{}
compactTextMarshaler = TextMarshaler{Compact: true}
)
// MarshalText writes the proto text format of m to w.
func MarshalText(w io.Writer, m Message) error { return defaultTextMarshaler.Marshal(w, m) }
// MarshalTextString returns a proto text formatted string of m.
func MarshalTextString(m Message) string { return defaultTextMarshaler.Text(m) }
// CompactText writes the compact proto text format of m to w.
func CompactText(w io.Writer, m Message) error { return compactTextMarshaler.Marshal(w, m) }
// CompactTextString returns a compact proto text formatted string of m.
func CompactTextString(m Message) string { return compactTextMarshaler.Text(m) }
var (
newline = []byte("\n")
endBraceNewline = []byte("}\n")
posInf = []byte("inf")
negInf = []byte("-inf")
nan = []byte("nan")
)
// textWriter is an io.Writer that tracks its indentation level.
type textWriter struct {
compact bool // same as TextMarshaler.Compact
expandAny bool // same as TextMarshaler.ExpandAny
complete bool // whether the current position is a complete line
indent int // indentation level; never negative
buf []byte
}
func (w *textWriter) Write(p []byte) (n int, _ error) {
newlines := bytes.Count(p, newline)
if newlines == 0 {
if !w.compact && w.complete {
w.writeIndent()
}
w.buf = append(w.buf, p...)
w.complete = false
return len(p), nil
}
frags := bytes.SplitN(p, newline, newlines+1)
if w.compact {
for i, frag := range frags {
if i > 0 {
w.buf = append(w.buf, ' ')
n++
}
w.buf = append(w.buf, frag...)
n += len(frag)
}
return n, nil
}
for i, frag := range frags {
if w.complete {
w.writeIndent()
}
w.buf = append(w.buf, frag...)
n += len(frag)
if i+1 < len(frags) {
w.buf = append(w.buf, '\n')
n++
}
}
w.complete = len(frags[len(frags)-1]) == 0
return n, nil
}
func (w *textWriter) WriteByte(c byte) error {
if w.compact && c == '\n' {
c = ' '
}
if !w.compact && w.complete {
w.writeIndent()
}
w.buf = append(w.buf, c)
w.complete = c == '\n'
return nil
}
func (w *textWriter) writeName(fd protoreflect.FieldDescriptor) {
if !w.compact && w.complete {
w.writeIndent()
}
w.complete = false
if fd.Kind() != protoreflect.GroupKind {
w.buf = append(w.buf, fd.Name()...)
w.WriteByte(':')
} else {
// Use message type name for group field name.
w.buf = append(w.buf, fd.Message().Name()...)
}
if !w.compact {
w.WriteByte(' ')
}
}
func requiresQuotes(u string) bool {
// When type URL contains any characters except [0-9A-Za-z./\-]*, it must be quoted.
for _, ch := range u {
switch {
case ch == '.' || ch == '/' || ch == '_':
continue
case '0' <= ch && ch <= '9':
continue
case 'A' <= ch && ch <= 'Z':
continue
case 'a' <= ch && ch <= 'z':
continue
default:
return true
}
}
return false
}
// writeProto3Any writes an expanded google.protobuf.Any message.
//
// It returns (false, nil) if sv value can't be unmarshaled (e.g. because
// required messages are not linked in).
//
// It returns (true, error) when sv was written in expanded format or an error
// was encountered.
func (w *textWriter) writeProto3Any(m protoreflect.Message) (bool, error) {
md := m.Descriptor()
fdURL := md.Fields().ByName("type_url")
fdVal := md.Fields().ByName("value")
url := m.Get(fdURL).String()
mt, err := protoregistry.GlobalTypes.FindMessageByURL(url)
if err != nil {
return false, nil
}
b := m.Get(fdVal).Bytes()
m2 := mt.New()
if err := proto.Unmarshal(b, m2.Interface()); err != nil {
return false, nil
}
w.Write([]byte("["))
if requiresQuotes(url) {
w.writeQuotedString(url)
} else {
w.Write([]byte(url))
}
if w.compact {
w.Write([]byte("]:<"))
} else {
w.Write([]byte("]: <\n"))
w.indent++
}
if err := w.writeMessage(m2); err != nil {
return true, err
}
if w.compact {
w.Write([]byte("> "))
} else {
w.indent--
w.Write([]byte(">\n"))
}
return true, nil
}
func (w *textWriter) writeMessage(m protoreflect.Message) error {
md := m.Descriptor()
if w.expandAny && md.FullName() == "google.protobuf.Any" {
if canExpand, err := w.writeProto3Any(m); canExpand {
return err
}
}
fds := md.Fields()
for i := 0; i < fds.Len(); {
fd := fds.Get(i)
if od := fd.ContainingOneof(); od != nil {
fd = m.WhichOneof(od)
i += od.Fields().Len()
} else {
i++
}
if fd == nil || !m.Has(fd) {
continue
}
switch {
case fd.IsList():
lv := m.Get(fd).List()
for j := 0; j < lv.Len(); j++ {
w.writeName(fd)
v := lv.Get(j)
if err := w.writeSingularValue(v, fd); err != nil {
return err
}
w.WriteByte('\n')
}
case fd.IsMap():
kfd := fd.MapKey()
vfd := fd.MapValue()
mv := m.Get(fd).Map()
type entry struct{ key, val protoreflect.Value }
var entries []entry
mv.Range(func(k protoreflect.MapKey, v protoreflect.Value) bool {
entries = append(entries, entry{k.Value(), v})
return true
})
sort.Slice(entries, func(i, j int) bool {
switch kfd.Kind() {
case protoreflect.BoolKind:
return !entries[i].key.Bool() && entries[j].key.Bool()
case protoreflect.Int32Kind, protoreflect.Sint32Kind, protoreflect.Sfixed32Kind, protoreflect.Int64Kind, protoreflect.Sint64Kind, protoreflect.Sfixed64Kind:
return entries[i].key.Int() < entries[j].key.Int()
case protoreflect.Uint32Kind, protoreflect.Fixed32Kind, protoreflect.Uint64Kind, protoreflect.Fixed64Kind:
return entries[i].key.Uint() < entries[j].key.Uint()
case protoreflect.StringKind:
return entries[i].key.String() < entries[j].key.String()
default:
panic("invalid kind")
}
})
for _, entry := range entries {
w.writeName(fd)
w.WriteByte('<')
if !w.compact {
w.WriteByte('\n')
}
w.indent++
w.writeName(kfd)
if err := w.writeSingularValue(entry.key, kfd); err != nil {
return err
}
w.WriteByte('\n')
w.writeName(vfd)
if err := w.writeSingularValue(entry.val, vfd); err != nil {
return err
}
w.WriteByte('\n')
w.indent--
w.WriteByte('>')
w.WriteByte('\n')
}
default:
w.writeName(fd)
if err := w.writeSingularValue(m.Get(fd), fd); err != nil {
return err
}
w.WriteByte('\n')
}
}
if b := m.GetUnknown(); len(b) > 0 {
w.writeUnknownFields(b)
}
return w.writeExtensions(m)
}
func (w *textWriter) writeSingularValue(v protoreflect.Value, fd protoreflect.FieldDescriptor) error {
switch fd.Kind() {
case protoreflect.FloatKind, protoreflect.DoubleKind:
switch vf := v.Float(); {
case math.IsInf(vf, +1):
w.Write(posInf)
case math.IsInf(vf, -1):
w.Write(negInf)
case math.IsNaN(vf):
w.Write(nan)
default:
fmt.Fprint(w, v.Interface())
}
case protoreflect.StringKind:
// NOTE: This does not validate UTF-8 for historical reasons.
w.writeQuotedString(string(v.String()))
case protoreflect.BytesKind:
w.writeQuotedString(string(v.Bytes()))
case protoreflect.MessageKind, protoreflect.GroupKind:
var bra, ket byte = '<', '>'
if fd.Kind() == protoreflect.GroupKind {
bra, ket = '{', '}'
}
w.WriteByte(bra)
if !w.compact {
w.WriteByte('\n')
}
w.indent++
m := v.Message()
if m2, ok := m.Interface().(encoding.TextMarshaler); ok {
b, err := m2.MarshalText()
if err != nil {
return err
}
w.Write(b)
} else {
w.writeMessage(m)
}
w.indent--
w.WriteByte(ket)
case protoreflect.EnumKind:
if ev := fd.Enum().Values().ByNumber(v.Enum()); ev != nil {
fmt.Fprint(w, ev.Name())
} else {
fmt.Fprint(w, v.Enum())
}
default:
fmt.Fprint(w, v.Interface())
}
return nil
}
// writeQuotedString writes a quoted string in the protocol buffer text format.
func (w *textWriter) writeQuotedString(s string) {
w.WriteByte('"')
for i := 0; i < len(s); i++ {
switch c := s[i]; c {
case '\n':
w.buf = append(w.buf, `\n`...)
case '\r':
w.buf = append(w.buf, `\r`...)
case '\t':
w.buf = append(w.buf, `\t`...)
case '"':
w.buf = append(w.buf, `\"`...)
case '\\':
w.buf = append(w.buf, `\\`...)
default:
if isPrint := c >= 0x20 && c < 0x7f; isPrint {
w.buf = append(w.buf, c)
} else {
w.buf = append(w.buf, fmt.Sprintf(`\%03o`, c)...)
}
}
}
w.WriteByte('"')
}
func (w *textWriter) writeUnknownFields(b []byte) {
if !w.compact {
fmt.Fprintf(w, "/* %d unknown bytes */\n", len(b))
}
for len(b) > 0 {
num, wtyp, n := protowire.ConsumeTag(b)
if n < 0 {
return
}
b = b[n:]
if wtyp == protowire.EndGroupType {
w.indent--
w.Write(endBraceNewline)
continue
}
fmt.Fprint(w, num)
if wtyp != protowire.StartGroupType {
w.WriteByte(':')
}
if !w.compact || wtyp == protowire.StartGroupType {
w.WriteByte(' ')
}
switch wtyp {
case protowire.VarintType:
v, n := protowire.ConsumeVarint(b)
if n < 0 {
return
}
b = b[n:]
fmt.Fprint(w, v)
case protowire.Fixed32Type:
v, n := protowire.ConsumeFixed32(b)
if n < 0 {
return
}
b = b[n:]
fmt.Fprint(w, v)
case protowire.Fixed64Type:
v, n := protowire.ConsumeFixed64(b)
if n < 0 {
return
}
b = b[n:]
fmt.Fprint(w, v)
case protowire.BytesType:
v, n := protowire.ConsumeBytes(b)
if n < 0 {
return
}
b = b[n:]
fmt.Fprintf(w, "%q", v)
case protowire.StartGroupType:
w.WriteByte('{')
w.indent++
default:
fmt.Fprintf(w, "/* unknown wire type %d */", wtyp)
}
w.WriteByte('\n')
}
}
// writeExtensions writes all the extensions in m.
func (w *textWriter) writeExtensions(m protoreflect.Message) error {
md := m.Descriptor()
if md.ExtensionRanges().Len() == 0 {
return nil
}
type ext struct {
desc protoreflect.FieldDescriptor
val protoreflect.Value
}
var exts []ext
m.Range(func(fd protoreflect.FieldDescriptor, v protoreflect.Value) bool {
if fd.IsExtension() {
exts = append(exts, ext{fd, v})
}
return true
})
sort.Slice(exts, func(i, j int) bool {
return exts[i].desc.Number() < exts[j].desc.Number()
})
for _, ext := range exts {
// For message set, use the name of the message as the extension name.
name := string(ext.desc.FullName())
if isMessageSet(ext.desc.ContainingMessage()) {
name = strings.TrimSuffix(name, ".message_set_extension")
}
if !ext.desc.IsList() {
if err := w.writeSingularExtension(name, ext.val, ext.desc); err != nil {
return err
}
} else {
lv := ext.val.List()
for i := 0; i < lv.Len(); i++ {
if err := w.writeSingularExtension(name, lv.Get(i), ext.desc); err != nil {
return err
}
}
}
}
return nil
}
func (w *textWriter) writeSingularExtension(name string, v protoreflect.Value, fd protoreflect.FieldDescriptor) error {
fmt.Fprintf(w, "[%s]:", name)
if !w.compact {
w.WriteByte(' ')
}
if err := w.writeSingularValue(v, fd); err != nil {
return err
}
w.WriteByte('\n')
return nil
}
func (w *textWriter) writeIndent() {
if !w.complete {
return
}
for i := 0; i < w.indent*2; i++ {
w.buf = append(w.buf, ' ')
}
w.complete = false
}

View File

@@ -1,78 +0,0 @@
// Copyright 2019 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 proto
import (
protoV2 "google.golang.org/protobuf/proto"
"google.golang.org/protobuf/runtime/protoiface"
)
// Size returns the size in bytes of the wire-format encoding of m.
func Size(m Message) int {
if m == nil {
return 0
}
mi := MessageV2(m)
return protoV2.Size(mi)
}
// Marshal returns the wire-format encoding of m.
func Marshal(m Message) ([]byte, error) {
b, err := marshalAppend(nil, m, false)
if b == nil {
b = zeroBytes
}
return b, err
}
var zeroBytes = make([]byte, 0, 0)
func marshalAppend(buf []byte, m Message, deterministic bool) ([]byte, error) {
if m == nil {
return nil, ErrNil
}
mi := MessageV2(m)
nbuf, err := protoV2.MarshalOptions{
Deterministic: deterministic,
AllowPartial: true,
}.MarshalAppend(buf, mi)
if err != nil {
return buf, err
}
if len(buf) == len(nbuf) {
if !mi.ProtoReflect().IsValid() {
return buf, ErrNil
}
}
return nbuf, checkRequiredNotSet(mi)
}
// Unmarshal parses a wire-format message in b and places the decoded results in m.
//
// Unmarshal resets m before starting to unmarshal, so any existing data in m is always
// removed. Use UnmarshalMerge to preserve and append to existing data.
func Unmarshal(b []byte, m Message) error {
m.Reset()
return UnmarshalMerge(b, m)
}
// UnmarshalMerge parses a wire-format message in b and places the decoded results in m.
func UnmarshalMerge(b []byte, m Message) error {
mi := MessageV2(m)
out, err := protoV2.UnmarshalOptions{
AllowPartial: true,
Merge: true,
}.UnmarshalState(protoiface.UnmarshalInput{
Buf: b,
Message: mi.ProtoReflect(),
})
if err != nil {
return err
}
if out.Flags&protoiface.UnmarshalInitialized > 0 {
return nil
}
return checkRequiredNotSet(mi)
}

View File

@@ -1,34 +0,0 @@
// Copyright 2019 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 proto
// Bool stores v in a new bool value and returns a pointer to it.
func Bool(v bool) *bool { return &v }
// Int stores v in a new int32 value and returns a pointer to it.
//
// Deprecated: Use Int32 instead.
func Int(v int) *int32 { return Int32(int32(v)) }
// Int32 stores v in a new int32 value and returns a pointer to it.
func Int32(v int32) *int32 { return &v }
// Int64 stores v in a new int64 value and returns a pointer to it.
func Int64(v int64) *int64 { return &v }
// Uint32 stores v in a new uint32 value and returns a pointer to it.
func Uint32(v uint32) *uint32 { return &v }
// Uint64 stores v in a new uint64 value and returns a pointer to it.
func Uint64(v uint64) *uint64 { return &v }
// Float32 stores v in a new float32 value and returns a pointer to it.
func Float32(v float32) *float32 { return &v }
// Float64 stores v in a new float64 value and returns a pointer to it.
func Float64(v float64) *float64 { return &v }
// String stores v in a new string value and returns a pointer to it.
func String(v string) *string { return &v }

View File

@@ -1,64 +0,0 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// source: github.com/golang/protobuf/ptypes/timestamp/timestamp.proto
package timestamp
import (
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
timestamppb "google.golang.org/protobuf/types/known/timestamppb"
reflect "reflect"
)
// Symbols defined in public import of google/protobuf/timestamp.proto.
type Timestamp = timestamppb.Timestamp
var File_github_com_golang_protobuf_ptypes_timestamp_timestamp_proto protoreflect.FileDescriptor
var file_github_com_golang_protobuf_ptypes_timestamp_timestamp_proto_rawDesc = []byte{
0x0a, 0x3b, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x6f, 0x6c,
0x61, 0x6e, 0x67, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x70, 0x74, 0x79,
0x70, 0x65, 0x73, 0x2f, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2f, 0x74, 0x69,
0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x1f, 0x67,
0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x74,
0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x42, 0x37,
0x5a, 0x35, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x6f, 0x6c,
0x61, 0x6e, 0x67, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x70, 0x74, 0x79,
0x70, 0x65, 0x73, 0x2f, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x3b, 0x74, 0x69,
0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x50, 0x00, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f,
0x33,
}
var file_github_com_golang_protobuf_ptypes_timestamp_timestamp_proto_goTypes = []interface{}{}
var file_github_com_golang_protobuf_ptypes_timestamp_timestamp_proto_depIdxs = []int32{
0, // [0:0] is the sub-list for method output_type
0, // [0:0] is the sub-list for method input_type
0, // [0:0] is the sub-list for extension type_name
0, // [0:0] is the sub-list for extension extendee
0, // [0:0] is the sub-list for field type_name
}
func init() { file_github_com_golang_protobuf_ptypes_timestamp_timestamp_proto_init() }
func file_github_com_golang_protobuf_ptypes_timestamp_timestamp_proto_init() {
if File_github_com_golang_protobuf_ptypes_timestamp_timestamp_proto != nil {
return
}
type x struct{}
out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: file_github_com_golang_protobuf_ptypes_timestamp_timestamp_proto_rawDesc,
NumEnums: 0,
NumMessages: 0,
NumExtensions: 0,
NumServices: 0,
},
GoTypes: file_github_com_golang_protobuf_ptypes_timestamp_timestamp_proto_goTypes,
DependencyIndexes: file_github_com_golang_protobuf_ptypes_timestamp_timestamp_proto_depIdxs,
}.Build()
File_github_com_golang_protobuf_ptypes_timestamp_timestamp_proto = out.File
file_github_com_golang_protobuf_ptypes_timestamp_timestamp_proto_rawDesc = nil
file_github_com_golang_protobuf_ptypes_timestamp_timestamp_proto_goTypes = nil
file_github_com_golang_protobuf_ptypes_timestamp_timestamp_proto_depIdxs = nil
}

View File

@@ -1,9 +0,0 @@
# This is the official list of Gorilla WebSocket authors for copyright
# purposes.
#
# Please keep the list sorted.
Gary Burd <gary@beagledreams.com>
Google LLC (https://opensource.google.com/)
Joachim Bauch <mail@joachim-bauch.de>

View File

@@ -7,12 +7,6 @@ Gorilla WebSocket is a [Go](http://golang.org/) implementation of the
[WebSocket](http://www.rfc-editor.org/rfc/rfc6455.txt) protocol. [WebSocket](http://www.rfc-editor.org/rfc/rfc6455.txt) protocol.
---
⚠️ **[The Gorilla WebSocket Package is looking for a new maintainer](https://github.com/gorilla/websocket/issues/370)**
---
### Documentation ### Documentation
* [API Reference](https://pkg.go.dev/github.com/gorilla/websocket?tab=doc) * [API Reference](https://pkg.go.dev/github.com/gorilla/websocket?tab=doc)

View File

@@ -9,6 +9,7 @@ import (
"context" "context"
"crypto/tls" "crypto/tls"
"errors" "errors"
"fmt"
"io" "io"
"io/ioutil" "io/ioutil"
"net" "net"
@@ -318,14 +319,14 @@ func (d *Dialer) DialContext(ctx context.Context, urlStr string, requestHeader h
} }
netConn, err := netDial("tcp", hostPort) netConn, err := netDial("tcp", hostPort)
if err != nil {
return nil, nil, err
}
if trace != nil && trace.GotConn != nil { if trace != nil && trace.GotConn != nil {
trace.GotConn(httptrace.GotConnInfo{ trace.GotConn(httptrace.GotConnInfo{
Conn: netConn, Conn: netConn,
}) })
} }
if err != nil {
return nil, nil, err
}
defer func() { defer func() {
if netConn != nil { if netConn != nil {
@@ -370,6 +371,17 @@ func (d *Dialer) DialContext(ctx context.Context, urlStr string, requestHeader h
resp, err := http.ReadResponse(conn.br, req) resp, err := http.ReadResponse(conn.br, req)
if err != nil { if err != nil {
if d.TLSClientConfig != nil {
for _, proto := range d.TLSClientConfig.NextProtos {
if proto != "http/1.1" {
return nil, nil, fmt.Errorf(
"websocket: protocol %q was given but is not supported;"+
"sharing tls.Config with net/http Transport can cause this error: %w",
proto, err,
)
}
}
}
return nil, nil, err return nil, nil, err
} }

View File

@@ -1189,8 +1189,16 @@ func (c *Conn) SetPongHandler(h func(appData string) error) {
c.handlePong = h c.handlePong = h
} }
// NetConn returns the underlying connection that is wrapped by c.
// Note that writing to or reading from this connection directly will corrupt the
// WebSocket connection.
func (c *Conn) NetConn() net.Conn {
return c.conn
}
// UnderlyingConn returns the internal net.Conn. This can be used to further // UnderlyingConn returns the internal net.Conn. This can be used to further
// modifications to connection specific flags. // modifications to connection specific flags.
// Deprecated: Use the NetConn method.
func (c *Conn) UnderlyingConn() net.Conn { func (c *Conn) UnderlyingConn() net.Conn {
return c.conn return c.conn
} }

View File

@@ -154,8 +154,8 @@ func (u *Upgrader) Upgrade(w http.ResponseWriter, r *http.Request, responseHeade
} }
challengeKey := r.Header.Get("Sec-Websocket-Key") challengeKey := r.Header.Get("Sec-Websocket-Key")
if challengeKey == "" { if !isValidChallengeKey(challengeKey) {
return u.returnError(w, r, http.StatusBadRequest, "websocket: not a websocket handshake: 'Sec-WebSocket-Key' header is missing or blank") return u.returnError(w, r, http.StatusBadRequest, "websocket: not a websocket handshake: 'Sec-WebSocket-Key' header must be Base64 encoded value of 16-byte in length")
} }
subprotocol := u.selectSubprotocol(r, responseHeader) subprotocol := u.selectSubprotocol(r, responseHeader)

View File

@@ -1,21 +0,0 @@
//go:build !go1.17
// +build !go1.17
package websocket
import (
"context"
"crypto/tls"
)
func doHandshake(ctx context.Context, tlsConn *tls.Conn, cfg *tls.Config) error {
if err := tlsConn.Handshake(); err != nil {
return err
}
if !cfg.InsecureSkipVerify {
if err := tlsConn.VerifyHostname(cfg.ServerName); err != nil {
return err
}
}
return nil
}

View File

@@ -281,3 +281,18 @@ headers:
} }
return result return result
} }
// isValidChallengeKey checks if the argument meets RFC6455 specification.
func isValidChallengeKey(s string) bool {
// From RFC6455:
//
// A |Sec-WebSocket-Key| header field with a base64-encoded (see
// Section 4 of [RFC4648]) value that, when decoded, is 16 bytes in
// length.
if s == "" {
return false
}
decoded, err := base64.StdEncoding.DecodeString(s)
return err == nil && len(decoded) == 16
}

View File

@@ -1,473 +0,0 @@
// Code generated by golang.org/x/tools/cmd/bundle. DO NOT EDIT.
//go:generate bundle -o x_net_proxy.go golang.org/x/net/proxy
// Package proxy provides support for a variety of protocols to proxy network
// data.
//
package websocket
import (
"errors"
"io"
"net"
"net/url"
"os"
"strconv"
"strings"
"sync"
)
type proxy_direct struct{}
// Direct is a direct proxy: one that makes network connections directly.
var proxy_Direct = proxy_direct{}
func (proxy_direct) Dial(network, addr string) (net.Conn, error) {
return net.Dial(network, addr)
}
// A PerHost directs connections to a default Dialer unless the host name
// requested matches one of a number of exceptions.
type proxy_PerHost struct {
def, bypass proxy_Dialer
bypassNetworks []*net.IPNet
bypassIPs []net.IP
bypassZones []string
bypassHosts []string
}
// NewPerHost returns a PerHost Dialer that directs connections to either
// defaultDialer or bypass, depending on whether the connection matches one of
// the configured rules.
func proxy_NewPerHost(defaultDialer, bypass proxy_Dialer) *proxy_PerHost {
return &proxy_PerHost{
def: defaultDialer,
bypass: bypass,
}
}
// Dial connects to the address addr on the given network through either
// defaultDialer or bypass.
func (p *proxy_PerHost) Dial(network, addr string) (c net.Conn, err error) {
host, _, err := net.SplitHostPort(addr)
if err != nil {
return nil, err
}
return p.dialerForRequest(host).Dial(network, addr)
}
func (p *proxy_PerHost) dialerForRequest(host string) proxy_Dialer {
if ip := net.ParseIP(host); ip != nil {
for _, net := range p.bypassNetworks {
if net.Contains(ip) {
return p.bypass
}
}
for _, bypassIP := range p.bypassIPs {
if bypassIP.Equal(ip) {
return p.bypass
}
}
return p.def
}
for _, zone := range p.bypassZones {
if strings.HasSuffix(host, zone) {
return p.bypass
}
if host == zone[1:] {
// For a zone ".example.com", we match "example.com"
// too.
return p.bypass
}
}
for _, bypassHost := range p.bypassHosts {
if bypassHost == host {
return p.bypass
}
}
return p.def
}
// AddFromString parses a string that contains comma-separated values
// specifying hosts that should use the bypass proxy. Each value is either an
// IP address, a CIDR range, a zone (*.example.com) or a host name
// (localhost). A best effort is made to parse the string and errors are
// ignored.
func (p *proxy_PerHost) AddFromString(s string) {
hosts := strings.Split(s, ",")
for _, host := range hosts {
host = strings.TrimSpace(host)
if len(host) == 0 {
continue
}
if strings.Contains(host, "/") {
// We assume that it's a CIDR address like 127.0.0.0/8
if _, net, err := net.ParseCIDR(host); err == nil {
p.AddNetwork(net)
}
continue
}
if ip := net.ParseIP(host); ip != nil {
p.AddIP(ip)
continue
}
if strings.HasPrefix(host, "*.") {
p.AddZone(host[1:])
continue
}
p.AddHost(host)
}
}
// AddIP specifies an IP address that will use the bypass proxy. Note that
// this will only take effect if a literal IP address is dialed. A connection
// to a named host will never match an IP.
func (p *proxy_PerHost) AddIP(ip net.IP) {
p.bypassIPs = append(p.bypassIPs, ip)
}
// AddNetwork specifies an IP range that will use the bypass proxy. Note that
// this will only take effect if a literal IP address is dialed. A connection
// to a named host will never match.
func (p *proxy_PerHost) AddNetwork(net *net.IPNet) {
p.bypassNetworks = append(p.bypassNetworks, net)
}
// AddZone specifies a DNS suffix that will use the bypass proxy. A zone of
// "example.com" matches "example.com" and all of its subdomains.
func (p *proxy_PerHost) AddZone(zone string) {
if strings.HasSuffix(zone, ".") {
zone = zone[:len(zone)-1]
}
if !strings.HasPrefix(zone, ".") {
zone = "." + zone
}
p.bypassZones = append(p.bypassZones, zone)
}
// AddHost specifies a host name that will use the bypass proxy.
func (p *proxy_PerHost) AddHost(host string) {
if strings.HasSuffix(host, ".") {
host = host[:len(host)-1]
}
p.bypassHosts = append(p.bypassHosts, host)
}
// A Dialer is a means to establish a connection.
type proxy_Dialer interface {
// Dial connects to the given address via the proxy.
Dial(network, addr string) (c net.Conn, err error)
}
// Auth contains authentication parameters that specific Dialers may require.
type proxy_Auth struct {
User, Password string
}
// FromEnvironment returns the dialer specified by the proxy related variables in
// the environment.
func proxy_FromEnvironment() proxy_Dialer {
allProxy := proxy_allProxyEnv.Get()
if len(allProxy) == 0 {
return proxy_Direct
}
proxyURL, err := url.Parse(allProxy)
if err != nil {
return proxy_Direct
}
proxy, err := proxy_FromURL(proxyURL, proxy_Direct)
if err != nil {
return proxy_Direct
}
noProxy := proxy_noProxyEnv.Get()
if len(noProxy) == 0 {
return proxy
}
perHost := proxy_NewPerHost(proxy, proxy_Direct)
perHost.AddFromString(noProxy)
return perHost
}
// proxySchemes is a map from URL schemes to a function that creates a Dialer
// from a URL with such a scheme.
var proxy_proxySchemes map[string]func(*url.URL, proxy_Dialer) (proxy_Dialer, error)
// RegisterDialerType takes a URL scheme and a function to generate Dialers from
// a URL with that scheme and a forwarding Dialer. Registered schemes are used
// by FromURL.
func proxy_RegisterDialerType(scheme string, f func(*url.URL, proxy_Dialer) (proxy_Dialer, error)) {
if proxy_proxySchemes == nil {
proxy_proxySchemes = make(map[string]func(*url.URL, proxy_Dialer) (proxy_Dialer, error))
}
proxy_proxySchemes[scheme] = f
}
// FromURL returns a Dialer given a URL specification and an underlying
// Dialer for it to make network requests.
func proxy_FromURL(u *url.URL, forward proxy_Dialer) (proxy_Dialer, error) {
var auth *proxy_Auth
if u.User != nil {
auth = new(proxy_Auth)
auth.User = u.User.Username()
if p, ok := u.User.Password(); ok {
auth.Password = p
}
}
switch u.Scheme {
case "socks5":
return proxy_SOCKS5("tcp", u.Host, auth, forward)
}
// If the scheme doesn't match any of the built-in schemes, see if it
// was registered by another package.
if proxy_proxySchemes != nil {
if f, ok := proxy_proxySchemes[u.Scheme]; ok {
return f(u, forward)
}
}
return nil, errors.New("proxy: unknown scheme: " + u.Scheme)
}
var (
proxy_allProxyEnv = &proxy_envOnce{
names: []string{"ALL_PROXY", "all_proxy"},
}
proxy_noProxyEnv = &proxy_envOnce{
names: []string{"NO_PROXY", "no_proxy"},
}
)
// envOnce looks up an environment variable (optionally by multiple
// names) once. It mitigates expensive lookups on some platforms
// (e.g. Windows).
// (Borrowed from net/http/transport.go)
type proxy_envOnce struct {
names []string
once sync.Once
val string
}
func (e *proxy_envOnce) Get() string {
e.once.Do(e.init)
return e.val
}
func (e *proxy_envOnce) init() {
for _, n := range e.names {
e.val = os.Getenv(n)
if e.val != "" {
return
}
}
}
// SOCKS5 returns a Dialer that makes SOCKSv5 connections to the given address
// with an optional username and password. See RFC 1928 and RFC 1929.
func proxy_SOCKS5(network, addr string, auth *proxy_Auth, forward proxy_Dialer) (proxy_Dialer, error) {
s := &proxy_socks5{
network: network,
addr: addr,
forward: forward,
}
if auth != nil {
s.user = auth.User
s.password = auth.Password
}
return s, nil
}
type proxy_socks5 struct {
user, password string
network, addr string
forward proxy_Dialer
}
const proxy_socks5Version = 5
const (
proxy_socks5AuthNone = 0
proxy_socks5AuthPassword = 2
)
const proxy_socks5Connect = 1
const (
proxy_socks5IP4 = 1
proxy_socks5Domain = 3
proxy_socks5IP6 = 4
)
var proxy_socks5Errors = []string{
"",
"general failure",
"connection forbidden",
"network unreachable",
"host unreachable",
"connection refused",
"TTL expired",
"command not supported",
"address type not supported",
}
// Dial connects to the address addr on the given network via the SOCKS5 proxy.
func (s *proxy_socks5) Dial(network, addr string) (net.Conn, error) {
switch network {
case "tcp", "tcp6", "tcp4":
default:
return nil, errors.New("proxy: no support for SOCKS5 proxy connections of type " + network)
}
conn, err := s.forward.Dial(s.network, s.addr)
if err != nil {
return nil, err
}
if err := s.connect(conn, addr); err != nil {
conn.Close()
return nil, err
}
return conn, nil
}
// connect takes an existing connection to a socks5 proxy server,
// and commands the server to extend that connection to target,
// which must be a canonical address with a host and port.
func (s *proxy_socks5) connect(conn net.Conn, target string) error {
host, portStr, err := net.SplitHostPort(target)
if err != nil {
return err
}
port, err := strconv.Atoi(portStr)
if err != nil {
return errors.New("proxy: failed to parse port number: " + portStr)
}
if port < 1 || port > 0xffff {
return errors.New("proxy: port number out of range: " + portStr)
}
// the size here is just an estimate
buf := make([]byte, 0, 6+len(host))
buf = append(buf, proxy_socks5Version)
if len(s.user) > 0 && len(s.user) < 256 && len(s.password) < 256 {
buf = append(buf, 2 /* num auth methods */, proxy_socks5AuthNone, proxy_socks5AuthPassword)
} else {
buf = append(buf, 1 /* num auth methods */, proxy_socks5AuthNone)
}
if _, err := conn.Write(buf); err != nil {
return errors.New("proxy: failed to write greeting to SOCKS5 proxy at " + s.addr + ": " + err.Error())
}
if _, err := io.ReadFull(conn, buf[:2]); err != nil {
return errors.New("proxy: failed to read greeting from SOCKS5 proxy at " + s.addr + ": " + err.Error())
}
if buf[0] != 5 {
return errors.New("proxy: SOCKS5 proxy at " + s.addr + " has unexpected version " + strconv.Itoa(int(buf[0])))
}
if buf[1] == 0xff {
return errors.New("proxy: SOCKS5 proxy at " + s.addr + " requires authentication")
}
// See RFC 1929
if buf[1] == proxy_socks5AuthPassword {
buf = buf[:0]
buf = append(buf, 1 /* password protocol version */)
buf = append(buf, uint8(len(s.user)))
buf = append(buf, s.user...)
buf = append(buf, uint8(len(s.password)))
buf = append(buf, s.password...)
if _, err := conn.Write(buf); err != nil {
return errors.New("proxy: failed to write authentication request to SOCKS5 proxy at " + s.addr + ": " + err.Error())
}
if _, err := io.ReadFull(conn, buf[:2]); err != nil {
return errors.New("proxy: failed to read authentication reply from SOCKS5 proxy at " + s.addr + ": " + err.Error())
}
if buf[1] != 0 {
return errors.New("proxy: SOCKS5 proxy at " + s.addr + " rejected username/password")
}
}
buf = buf[:0]
buf = append(buf, proxy_socks5Version, proxy_socks5Connect, 0 /* reserved */)
if ip := net.ParseIP(host); ip != nil {
if ip4 := ip.To4(); ip4 != nil {
buf = append(buf, proxy_socks5IP4)
ip = ip4
} else {
buf = append(buf, proxy_socks5IP6)
}
buf = append(buf, ip...)
} else {
if len(host) > 255 {
return errors.New("proxy: destination host name too long: " + host)
}
buf = append(buf, proxy_socks5Domain)
buf = append(buf, byte(len(host)))
buf = append(buf, host...)
}
buf = append(buf, byte(port>>8), byte(port))
if _, err := conn.Write(buf); err != nil {
return errors.New("proxy: failed to write connect request to SOCKS5 proxy at " + s.addr + ": " + err.Error())
}
if _, err := io.ReadFull(conn, buf[:4]); err != nil {
return errors.New("proxy: failed to read connect reply from SOCKS5 proxy at " + s.addr + ": " + err.Error())
}
failure := "unknown error"
if int(buf[1]) < len(proxy_socks5Errors) {
failure = proxy_socks5Errors[buf[1]]
}
if len(failure) > 0 {
return errors.New("proxy: SOCKS5 proxy at " + s.addr + " failed to connect: " + failure)
}
bytesToDiscard := 0
switch buf[3] {
case proxy_socks5IP4:
bytesToDiscard = net.IPv4len
case proxy_socks5IP6:
bytesToDiscard = net.IPv6len
case proxy_socks5Domain:
_, err := io.ReadFull(conn, buf[:1])
if err != nil {
return errors.New("proxy: failed to read domain length from SOCKS5 proxy at " + s.addr + ": " + err.Error())
}
bytesToDiscard = int(buf[0])
default:
return errors.New("proxy: got unknown address type " + strconv.Itoa(int(buf[3])) + " from SOCKS5 proxy at " + s.addr)
}
if cap(buf) < bytesToDiscard {
buf = make([]byte, bytesToDiscard)
} else {
buf = buf[:bytesToDiscard]
}
if _, err := io.ReadFull(conn, buf); err != nil {
return errors.New("proxy: failed to read address from SOCKS5 proxy at " + s.addr + ": " + err.Error())
}
// Also need to discard the port number
if _, err := io.ReadFull(conn, buf[:2]); err != nil {
return errors.New("proxy: failed to read port from SOCKS5 proxy at " + s.addr + ": " + err.Error())
}
return nil
}

View File

@@ -1,39 +0,0 @@
language: go
os:
- linux
- osx
go:
- 1.16.x
install:
# go-flags
- go build -v ./...
# linting
- go get -v golang.org/x/lint/golint
# code coverage
- go get golang.org/x/tools/cmd/cover
- go get github.com/onsi/ginkgo/ginkgo
- go get github.com/modocache/gover
- if [ "$TRAVIS_SECURE_ENV_VARS" = "true" ]; then go get github.com/mattn/goveralls; fi
script:
# go-flags
- $(exit $(gofmt -l . | wc -l))
- go test -v ./...
# linting
- go tool vet -all=true -v=true . || true
- $(go env GOPATH | awk 'BEGIN{FS=":"} {print $1}')/bin/golint ./...
# code coverage
- $(go env GOPATH | awk 'BEGIN{FS=":"} {print $1}')/bin/ginkgo -r -cover
- $(go env GOPATH | awk 'BEGIN{FS=":"} {print $1}')/bin/gover
- if [ "$TRAVIS_SECURE_ENV_VARS" = "true" ]; then $(go env GOPATH | awk 'BEGIN{FS=":"} {print $1}')/bin/goveralls -coverprofile=gover.coverprofile -service=travis-ci -repotoken $COVERALLS_TOKEN; fi
env:
# coveralls.io
secure: "RCYbiB4P0RjQRIoUx/vG/AjP3mmYCbzOmr86DCww1Z88yNcy3hYr3Cq8rpPtYU5v0g7wTpu4adaKIcqRE9xknYGbqj3YWZiCoBP1/n4Z+9sHW3Dsd9D/GRGeHUus0laJUGARjWoCTvoEtOgTdGQDoX7mH+pUUY0FBltNYUdOiiU="

View File

@@ -1,7 +1,7 @@
go-flags: a go library for parsing command line arguments go-flags: a go library for parsing command line arguments
========================================================= =========================================================
[![GoDoc](https://godoc.org/github.com/jessevdk/go-flags?status.png)](https://godoc.org/github.com/jessevdk/go-flags) [![Build Status](https://travis-ci.org/jessevdk/go-flags.svg?branch=master)](https://travis-ci.org/jessevdk/go-flags) [![Coverage Status](https://img.shields.io/coveralls/jessevdk/go-flags.svg)](https://coveralls.io/r/jessevdk/go-flags?branch=master) [![GoDoc](https://godoc.org/github.com/jessevdk/go-flags?status.png)](https://godoc.org/github.com/jessevdk/go-flags)
This library provides similar functionality to the builtin flag library of This library provides similar functionality to the builtin flag library of
go, but provides much more functionality and nicer formatting. From the go, but provides much more functionality and nicer formatting. From the
@@ -78,6 +78,9 @@ var opts struct {
// Example of a map // Example of a map
IntMap map[string]int `long:"intmap" description:"A map from string to int"` IntMap map[string]int `long:"intmap" description:"A map from string to int"`
// Example of env variable
Thresholds []int `long:"thresholds" default:"1" default:"2" env:"THRESHOLD_VALUES" env-delim:","`
} }
// Callback which will invoke callto:<argument> to call a number. // Callback which will invoke callto:<argument> to call a number.

View File

@@ -30,6 +30,12 @@ type Command struct {
// Whether positional arguments are required // Whether positional arguments are required
ArgsRequired bool ArgsRequired bool
// Whether to pass all arguments after the first non option as remaining
// command line arguments. This is equivalent to strict POSIX processing.
// This is command-local version of PassAfterNonOption Parser flag. It
// cannot be turned off when PassAfterNonOption Parser flag is set.
PassAfterNonOption bool
commands []*Command commands []*Command
hasBuiltinHelpGroup bool hasBuiltinHelpGroup bool
args []*Arg args []*Arg
@@ -244,6 +250,7 @@ func (c *Command) scanSubcommandHandler(parentg *Group) scanHandler {
longDescription := mtag.Get("long-description") longDescription := mtag.Get("long-description")
subcommandsOptional := mtag.Get("subcommands-optional") subcommandsOptional := mtag.Get("subcommands-optional")
aliases := mtag.GetMany("alias") aliases := mtag.GetMany("alias")
passAfterNonOption := mtag.Get("pass-after-non-option")
subc, err := c.AddCommand(subcommand, shortDescription, longDescription, ptrval.Interface()) subc, err := c.AddCommand(subcommand, shortDescription, longDescription, ptrval.Interface())
@@ -261,6 +268,10 @@ func (c *Command) scanSubcommandHandler(parentg *Group) scanHandler {
subc.Aliases = aliases subc.Aliases = aliases
} }
if len(passAfterNonOption) > 0 {
subc.PassAfterNonOption = true
}
return true, nil return true, nil
} }

View File

@@ -53,7 +53,7 @@ func getBase(options multiTag, base int) (int, error) {
func convertMarshal(val reflect.Value) (bool, string, error) { func convertMarshal(val reflect.Value) (bool, string, error) {
// Check first for the Marshaler interface // Check first for the Marshaler interface
if val.Type().NumMethod() > 0 && val.CanInterface() { if val.IsValid() && val.Type().NumMethod() > 0 && val.CanInterface() {
if marshaler, ok := val.Interface().(Marshaler); ok { if marshaler, ok := val.Interface().(Marshaler); ok {
ret, err := marshaler.MarshalFlag() ret, err := marshaler.MarshalFlag()
return true, ret, err return true, ret, err
@@ -68,6 +68,10 @@ func convertToString(val reflect.Value, options multiTag) (string, error) {
return ret, err return ret, err
} }
if !val.IsValid() {
return "", nil
}
tp := val.Type() tp := val.Type()
// Support for time.Duration // Support for time.Duration
@@ -220,7 +224,7 @@ func convert(val string, retval reflect.Value, options multiTag) error {
retval.SetBool(b) retval.SetBool(b)
} }
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
base, err := getBase(options, 10) base, err := getBase(options, 0)
if err != nil { if err != nil {
return err return err
@@ -234,7 +238,7 @@ func convert(val string, retval reflect.Value, options multiTag) error {
retval.SetInt(parsed) retval.SetInt(parsed)
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
base, err := getBase(options, 10) base, err := getBase(options, 0)
if err != nil { if err != nil {
return err return err
@@ -267,7 +271,12 @@ func convert(val string, retval reflect.Value, options multiTag) error {
retval.Set(reflect.Append(retval, elemval)) retval.Set(reflect.Append(retval, elemval))
case reflect.Map: case reflect.Map:
parts := strings.SplitN(val, ":", 2) keyValueDelimiter := options.Get("key-value-delimiter")
if keyValueDelimiter == "" {
keyValueDelimiter = ":"
}
parts := strings.SplitN(val, keyValueDelimiter, 2)
key := parts[0] key := parts[0]
var value string var value string

View File

@@ -8,46 +8,45 @@ The flags package is similar in functionality to the go built-in flag package
but provides more options and uses reflection to provide a convenient and but provides more options and uses reflection to provide a convenient and
succinct way of specifying command line options. succinct way of specifying command line options.
# Supported features
Supported features
The following features are supported in go-flags: The following features are supported in go-flags:
Options with short names (-v) Options with short names (-v)
Options with long names (--verbose) Options with long names (--verbose)
Options with and without arguments (bool v.s. other type) Options with and without arguments (bool v.s. other type)
Options with optional arguments and default values Options with optional arguments and default values
Option default values from ENVIRONMENT_VARIABLES, including slice and map values Option default values from ENVIRONMENT_VARIABLES, including slice and map values
Multiple option groups each containing a set of options Multiple option groups each containing a set of options
Generate and print well-formatted help message Generate and print well-formatted help message
Passing remaining command line arguments after -- (optional) Passing remaining command line arguments after -- (optional)
Ignoring unknown command line options (optional) Ignoring unknown command line options (optional)
Supports -I/usr/include -I=/usr/include -I /usr/include option argument specification Supports -I/usr/include -I=/usr/include -I /usr/include option argument specification
Supports multiple short options -aux Supports multiple short options -aux
Supports all primitive go types (string, int{8..64}, uint{8..64}, float) Supports all primitive go types (string, int{8..64}, uint{8..64}, float)
Supports same option multiple times (can store in slice or last option counts) Supports same option multiple times (can store in slice or last option counts)
Supports maps Supports maps
Supports function callbacks Supports function callbacks
Supports namespaces for (nested) option groups Supports namespaces for (nested) option groups
Additional features specific to Windows: Additional features specific to Windows:
Options with short names (/v)
Options with long names (/verbose)
Windows-style options with arguments use a colon as the delimiter
Modify generated help message with Windows-style / options
Windows style options can be disabled at build time using the "forceposix"
build tag
Options with short names (/v)
Options with long names (/verbose)
Windows-style options with arguments use a colon as the delimiter
Modify generated help message with Windows-style / options
Windows style options can be disabled at build time using the "forceposix"
build tag
Basic usage # Basic usage
The flags package uses structs, reflection and struct field tags The flags package uses structs, reflection and struct field tags
to allow users to specify command line options. This results in very simple to allow users to specify command line options. This results in very simple
and concise specification of your application options. For example: and concise specification of your application options. For example:
type Options struct { type Options struct {
Verbose []bool `short:"v" long:"verbose" description:"Show verbose debug information"` Verbose []bool `short:"v" long:"verbose" description:"Show verbose debug information"`
} }
This specifies one option with a short name -v and a long name --verbose. This specifies one option with a short name -v and a long name --verbose.
When either -v or --verbose is found on the command line, a 'true' value When either -v or --verbose is found on the command line, a 'true' value
@@ -60,9 +59,9 @@ whenever the option is encountered, a value is appended to the slice.
Map options from string to primitive type are also supported. On the command Map options from string to primitive type are also supported. On the command
line, you specify the value for such an option as key:value. For example line, you specify the value for such an option as key:value. For example
type Options struct { type Options struct {
AuthorInfo string[string] `short:"a"` AuthorInfo string[string] `short:"a"`
} }
Then, the AuthorInfo map can be filled with something like Then, the AuthorInfo map can be filled with something like
-a name:Jesse -a "surname:van den Kieboom". -a name:Jesse -a "surname:van den Kieboom".
@@ -71,96 +70,94 @@ Finally, for full control over the conversion between command line argument
values and options, user defined types can choose to implement the Marshaler values and options, user defined types can choose to implement the Marshaler
and Unmarshaler interfaces. and Unmarshaler interfaces.
# Available field tags
Available field tags
The following is a list of tags for struct fields supported by go-flags: The following is a list of tags for struct fields supported by go-flags:
short: the short name of the option (single character) short: the short name of the option (single character)
long: the long name of the option long: the long name of the option
required: if non empty, makes the option required to appear on the command required: if non empty, makes the option required to appear on the command
line. If a required option is not present, the parser will line. If a required option is not present, the parser will
return ErrRequired (optional) return ErrRequired (optional)
description: the description of the option (optional) description: the description of the option (optional)
long-description: the long description of the option. Currently only long-description: the long description of the option. Currently only
displayed in generated man pages (optional) displayed in generated man pages (optional)
no-flag: if non-empty, this field is ignored as an option (optional) no-flag: if non-empty, this field is ignored as an option (optional)
optional: if non-empty, makes the argument of the option optional. When an optional: if non-empty, makes the argument of the option optional. When an
argument is optional it can only be specified using argument is optional it can only be specified using
--option=argument (optional) --option=argument (optional)
optional-value: the value of an optional option when the option occurs optional-value: the value of an optional option when the option occurs
without an argument. This tag can be specified multiple without an argument. This tag can be specified multiple
times in the case of maps or slices (optional) times in the case of maps or slices (optional)
default: the default value of an option. This tag can be specified default: the default value of an option. This tag can be specified
multiple times in the case of slices or maps (optional) multiple times in the case of slices or maps (optional)
default-mask: when specified, this value will be displayed in the help default-mask: when specified, this value will be displayed in the help
instead of the actual default value. This is useful instead of the actual default value. This is useful
mostly for hiding otherwise sensitive information from mostly for hiding otherwise sensitive information from
showing up in the help. If default-mask takes the special showing up in the help. If default-mask takes the special
value "-", then no default value will be shown at all value "-", then no default value will be shown at all
(optional) (optional)
env: the default value of the option is overridden from the env: the default value of the option is overridden from the
specified environment variable, if one has been defined. specified environment variable, if one has been defined.
(optional) (optional)
env-delim: the 'env' default value from environment is split into env-delim: the 'env' default value from environment is split into
multiple values with the given delimiter string, use with multiple values with the given delimiter string, use with
slices and maps (optional) slices and maps (optional)
value-name: the name of the argument value (to be shown in the help) value-name: the name of the argument value (to be shown in the help)
(optional) (optional)
choice: limits the values for an option to a set of values. choice: limits the values for an option to a set of values.
Repeat this tag once for each allowable value. Repeat this tag once for each allowable value.
e.g. `long:"animal" choice:"cat" choice:"dog"` e.g. `long:"animal" choice:"cat" choice:"dog"`
hidden: if non-empty, the option is not visible in the help or man page. hidden: if non-empty, the option is not visible in the help or man page.
base: a base (radix) used to convert strings to integer values, the base: a base (radix) used to convert strings to integer values, the
default base is 10 (i.e. decimal) (optional) default base is 10 (i.e. decimal) (optional)
ini-name: the explicit ini option name (optional) ini-name: the explicit ini option name (optional)
no-ini: if non-empty this field is ignored as an ini option no-ini: if non-empty this field is ignored as an ini option
(optional) (optional)
group: when specified on a struct field, makes the struct group: when specified on a struct field, makes the struct
field a separate group with the given name (optional) field a separate group with the given name (optional)
namespace: when specified on a group struct field, the namespace namespace: when specified on a group struct field, the namespace
gets prepended to every option's long name and gets prepended to every option's long name and
subgroup's namespace of this group, separated by subgroup's namespace of this group, separated by
the parser's namespace delimiter (optional) the parser's namespace delimiter (optional)
env-namespace: when specified on a group struct field, the env-namespace env-namespace: when specified on a group struct field, the env-namespace
gets prepended to every option's env key and gets prepended to every option's env key and
subgroup's env-namespace of this group, separated by subgroup's env-namespace of this group, separated by
the parser's env-namespace delimiter (optional) the parser's env-namespace delimiter (optional)
command: when specified on a struct field, makes the struct command: when specified on a struct field, makes the struct
field a (sub)command with the given name (optional) field a (sub)command with the given name (optional)
subcommands-optional: when specified on a command struct field, makes subcommands-optional: when specified on a command struct field, makes
any subcommands of that command optional (optional) any subcommands of that command optional (optional)
alias: when specified on a command struct field, adds the alias: when specified on a command struct field, adds the
specified name as an alias for the command. Can be specified name as an alias for the command. Can be
be specified multiple times to add more than one be specified multiple times to add more than one
alias (optional) alias (optional)
positional-args: when specified on a field with a struct type, positional-args: when specified on a field with a struct type,
uses the fields of that struct to parse remaining uses the fields of that struct to parse remaining
positional command line arguments into (in order positional command line arguments into (in order
of the fields). If a field has a slice type, of the fields). If a field has a slice type,
then all remaining arguments will be added to it. then all remaining arguments will be added to it.
Positional arguments are optional by default, Positional arguments are optional by default,
unless the "required" tag is specified together unless the "required" tag is specified together
with the "positional-args" tag. The "required" tag with the "positional-args" tag. The "required" tag
can also be set on the individual rest argument can also be set on the individual rest argument
fields, to require only the first N positional fields, to require only the first N positional
arguments. If the "required" tag is set on the arguments. If the "required" tag is set on the
rest arguments slice, then its value determines rest arguments slice, then its value determines
the minimum amount of rest arguments that needs to the minimum amount of rest arguments that needs to
be provided (e.g. `required:"2"`) (optional) be provided (e.g. `required:"2"`) (optional)
positional-arg-name: used on a field in a positional argument struct; name positional-arg-name: used on a field in a positional argument struct; name
of the positional argument placeholder to be shown in of the positional argument placeholder to be shown in
the help (optional) the help (optional)
Either the `short:` tag or the `long:` must be specified to make the field eligible as an Either the `short:` tag or the `long:` must be specified to make the field eligible as an
option. option.
# Option groups
Option groups
Option groups are a simple way to semantically separate your options. All Option groups are a simple way to semantically separate your options. All
options in a particular group are shown together in the help under the name options in a particular group are shown together in the help under the name
@@ -169,14 +166,12 @@ precisely and emphasize the options affiliation to their group.
There are currently three ways to specify option groups. There are currently three ways to specify option groups.
1. Use NewNamedParser specifying the various option groups. 1. Use NewNamedParser specifying the various option groups.
2. Use AddGroup to add a group to an existing parser. 2. Use AddGroup to add a group to an existing parser.
3. Add a struct field to the top-level options annotated with the 3. Add a struct field to the top-level options annotated with the
group:"group-name" tag. group:"group-name" tag.
# Commands
Commands
The flags package also has basic support for commands. Commands are often The flags package also has basic support for commands. Commands are often
used in monolithic applications that support various commands or actions. used in monolithic applications that support various commands or actions.
@@ -186,9 +181,9 @@ application.
There are currently two ways to specify a command. There are currently two ways to specify a command.
1. Use AddCommand on an existing parser. 1. Use AddCommand on an existing parser.
2. Add a struct field to your options struct annotated with the 2. Add a struct field to your options struct annotated with the
command:"command-name" tag. command:"command-name" tag.
The most common, idiomatic way to implement commands is to define a global The most common, idiomatic way to implement commands is to define a global
parser instance and implement each command in a separate file. These parser instance and implement each command in a separate file. These
@@ -204,15 +199,14 @@ command has been specified on the command line, in addition to the options
of all the parent commands. I.e. considering a -v flag on the parser and an of all the parent commands. I.e. considering a -v flag on the parser and an
add command, the following are equivalent: add command, the following are equivalent:
./app -v add ./app -v add
./app add -v ./app add -v
However, if the -v flag is defined on the add command, then the first of However, if the -v flag is defined on the add command, then the first of
the two examples above would fail since the -v flag is not defined before the two examples above would fail since the -v flag is not defined before
the add command. the add command.
# Completion
Completion
go-flags has builtin support to provide bash completion of flags, commands go-flags has builtin support to provide bash completion of flags, commands
and argument values. To use completion, the binary which uses go-flags and argument values. To use completion, the binary which uses go-flags
@@ -226,7 +220,7 @@ by replacing the argument parsing routine with the completion routine which
outputs completions for the passed arguments. The basic invocation to outputs completions for the passed arguments. The basic invocation to
complete a set of arguments is therefore: complete a set of arguments is therefore:
GO_FLAGS_COMPLETION=1 ./completion-example arg1 arg2 arg3 GO_FLAGS_COMPLETION=1 ./completion-example arg1 arg2 arg3
where `completion-example` is the binary, `arg1` and `arg2` are where `completion-example` is the binary, `arg1` and `arg2` are
the current arguments, and `arg3` (the last argument) is the argument the current arguments, and `arg3` (the last argument) is the argument
@@ -237,20 +231,20 @@ are more than 1 completion items.
To use this with bash completion, a simple file can be written which To use this with bash completion, a simple file can be written which
calls the binary which supports go-flags completion: calls the binary which supports go-flags completion:
_completion_example() { _completion_example() {
# All arguments except the first one # All arguments except the first one
args=("${COMP_WORDS[@]:1:$COMP_CWORD}") args=("${COMP_WORDS[@]:1:$COMP_CWORD}")
# Only split on newlines # Only split on newlines
local IFS=$'\n' local IFS=$'\n'
# Call completion (note that the first element of COMP_WORDS is # Call completion (note that the first element of COMP_WORDS is
# the executable itself) # the executable itself)
COMPREPLY=($(GO_FLAGS_COMPLETION=1 ${COMP_WORDS[0]} "${args[@]}")) COMPREPLY=($(GO_FLAGS_COMPLETION=1 ${COMP_WORDS[0]} "${args[@]}"))
return 0 return 0
} }
complete -F _completion_example completion-example complete -F _completion_example completion-example
Completion requires the parser option PassDoubleDash and is therefore enforced if the environment variable GO_FLAGS_COMPLETION is set. Completion requires the parser option PassDoubleDash and is therefore enforced if the environment variable GO_FLAGS_COMPLETION is set.

View File

@@ -72,15 +72,15 @@ func (p *Parser) getAlignmentInfo() alignmentInfo {
var prevcmd *Command var prevcmd *Command
p.eachActiveGroup(func(c *Command, grp *Group) { p.eachActiveGroup(func(c *Command, grp *Group) {
if !grp.showInHelp() {
return
}
if c != prevcmd { if c != prevcmd {
for _, arg := range c.args { for _, arg := range c.args {
ret.updateLen(arg.Name, c != p.Command) ret.updateLen(arg.Name, c != p.Command)
} }
prevcmd = c
}
if !grp.showInHelp() {
return
} }
for _, info := range grp.options { for _, info := range grp.options {
if !info.showInHelp() { if !info.showInHelp() {
continue continue
@@ -334,7 +334,11 @@ func (p *Parser) WriteHelp(writer io.Writer) {
} }
if !allcmd.ArgsRequired { if !allcmd.ArgsRequired {
fmt.Fprintf(wr, "[%s]", name) if arg.Required > 0 {
fmt.Fprintf(wr, "%s", name)
} else {
fmt.Fprintf(wr, "[%s]", name)
}
} else { } else {
fmt.Fprintf(wr, "%s", name) fmt.Fprintf(wr, "%s", name)
} }

View File

@@ -113,18 +113,18 @@ func (i *IniParser) ParseFile(filename string) error {
// //
// The format of the ini file is as follows: // The format of the ini file is as follows:
// //
// [Option group name] // [Option group name]
// option = value // option = value
// //
// Each section in the ini file represents an option group or command in the // Each section in the ini file represents an option group or command in the
// flags parser. The default flags parser option group (i.e. when using // flags parser. The default flags parser option group (i.e. when using
// flags.Parse) is named 'Application Options'. The ini option name is matched // flags.Parse) is named 'Application Options'. The ini option name is matched
// in the following order: // in the following order:
// //
// 1. Compared to the ini-name tag on the option struct field (if present) // 1. Compared to the ini-name tag on the option struct field (if present)
// 2. Compared to the struct field name // 2. Compared to the struct field name
// 3. Compared to the option long name (if present) // 3. Compared to the option long name (if present)
// 4. Compared to the option short name (if present) // 4. Compared to the option short name (if present)
// //
// Sections for nested groups and commands can be addressed using a dot `.' // Sections for nested groups and commands can be addressed using a dot `.'
// namespacing notation (i.e [subcommand.Options]). Group section names are // namespacing notation (i.e [subcommand.Options]). Group section names are
@@ -584,7 +584,7 @@ func (i *IniParser) parse(ini *ini) error {
if i.ParseAsDefaults { if i.ParseAsDefaults {
err = opt.setDefault(pval) err = opt.setDefault(pval)
} else { } else {
err = opt.set(pval) err = opt.Set(pval)
} }
if err != nil { if err != nil {

View File

@@ -239,7 +239,7 @@ func (option *Option) IsSetDefault() bool {
// Set the value of an option to the specified value. An error will be returned // Set the value of an option to the specified value. An error will be returned
// if the specified value could not be converted to the corresponding option // if the specified value could not be converted to the corresponding option
// value type. // value type.
func (option *Option) set(value *string) error { func (option *Option) Set(value *string) error {
kind := option.value.Type().Kind() kind := option.value.Type().Kind()
if (kind == reflect.Map || kind == reflect.Slice) && option.clearReferenceBeforeSet { if (kind == reflect.Map || kind == reflect.Slice) && option.clearReferenceBeforeSet {
@@ -287,7 +287,7 @@ func (option *Option) setDefault(value *string) error {
return nil return nil
} }
if err := option.set(value); err != nil { if err := option.Set(value); err != nil {
return err return err
} }

View File

@@ -1,3 +1,4 @@
//go:build !windows || forceposix
// +build !windows forceposix // +build !windows forceposix
package flags package flags

View File

@@ -1,3 +1,4 @@
//go:build !forceposix
// +build !forceposix // +build !forceposix
package flags package flags

View File

@@ -113,6 +113,10 @@ const (
// POSIX processing. // POSIX processing.
PassAfterNonOption PassAfterNonOption
// AllowBoolValues allows a user to assign true/false to a boolean value
// rather than raising an error stating it cannot have an argument.
AllowBoolValues
// Default is a convenient default set of options which should cover // Default is a convenient default set of options which should cover
// most of the uses of the flags package. // most of the uses of the flags package.
Default = HelpFlag | PrintErrors | PassDoubleDash Default = HelpFlag | PrintErrors | PassDoubleDash
@@ -252,7 +256,7 @@ func (p *Parser) ParseArgs(args []string) ([]string, error) {
} }
if !argumentIsOption(arg) { if !argumentIsOption(arg) {
if (p.Options&PassAfterNonOption) != None && s.lookup.commands[arg] == nil { if ((p.Options&PassAfterNonOption) != None || s.command.PassAfterNonOption) && s.lookup.commands[arg] == nil {
// If PassAfterNonOption is set then all remaining arguments // If PassAfterNonOption is set then all remaining arguments
// are considered positional // are considered positional
if err = s.addArgs(s.arg); err != nil { if err = s.addArgs(s.arg); err != nil {
@@ -521,11 +525,10 @@ func (p *parseState) estimateCommand() error {
func (p *Parser) parseOption(s *parseState, name string, option *Option, canarg bool, argument *string) (err error) { func (p *Parser) parseOption(s *parseState, name string, option *Option, canarg bool, argument *string) (err error) {
if !option.canArgument() { if !option.canArgument() {
if argument != nil { if argument != nil && (p.Options&AllowBoolValues) == None {
return newErrorf(ErrNoArgumentForBool, "bool flag `%s' cannot have an argument", option) return newErrorf(ErrNoArgumentForBool, "bool flag `%s' cannot have an argument", option)
} }
err = option.Set(argument)
err = option.set(nil)
} else if argument != nil || (canarg && !s.eof()) { } else if argument != nil || (canarg && !s.eof()) {
var arg string var arg string
@@ -546,13 +549,13 @@ func (p *Parser) parseOption(s *parseState, name string, option *Option, canarg
} }
if err == nil { if err == nil {
err = option.set(&arg) err = option.Set(&arg)
} }
} else if option.OptionalArgument { } else if option.OptionalArgument {
option.empty() option.empty()
for _, v := range option.OptionalValue { for _, v := range option.OptionalValue {
err = option.set(&v) err = option.Set(&v)
if err != nil { if err != nil {
break break

View File

@@ -1,4 +1,5 @@
// +build !windows,!plan9,!appengine,!wasm //go:build !windows && !plan9 && !appengine && !wasm && !aix
// +build !windows,!plan9,!appengine,!wasm,!aix
package flags package flags

View File

@@ -1,4 +1,5 @@
// +build plan9 appengine wasm //go:build plan9 || appengine || wasm || aix
// +build plan9 appengine wasm aix
package flags package flags

View File

@@ -1,3 +1,4 @@
//go:build windows
// +build windows // +build windows
package flags package flags

View File

@@ -1,201 +0,0 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor 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, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "{}"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright {yyyy} {name of copyright owner}
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

View File

@@ -1 +0,0 @@
Copyright 2012 Matt T. Proud (matt.proud@gmail.com)

View File

@@ -1 +0,0 @@
cover.dat

View File

@@ -1,7 +0,0 @@
all:
cover:
go test -cover -v -coverprofile=cover.dat ./...
go tool cover -func cover.dat
.PHONY: cover

View File

@@ -1,75 +0,0 @@
// Copyright 2013 Matt T. Proud
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package pbutil
import (
"encoding/binary"
"errors"
"io"
"github.com/golang/protobuf/proto"
)
var errInvalidVarint = errors.New("invalid varint32 encountered")
// ReadDelimited decodes a message from the provided length-delimited stream,
// where the length is encoded as 32-bit varint prefix to the message body.
// It returns the total number of bytes read and any applicable error. This is
// roughly equivalent to the companion Java API's
// MessageLite#parseDelimitedFrom. As per the reader contract, this function
// calls r.Read repeatedly as required until exactly one message including its
// prefix is read and decoded (or an error has occurred). The function never
// reads more bytes from the stream than required. The function never returns
// an error if a message has been read and decoded correctly, even if the end
// of the stream has been reached in doing so. In that case, any subsequent
// calls return (0, io.EOF).
func ReadDelimited(r io.Reader, m proto.Message) (n int, err error) {
// Per AbstractParser#parsePartialDelimitedFrom with
// CodedInputStream#readRawVarint32.
var headerBuf [binary.MaxVarintLen32]byte
var bytesRead, varIntBytes int
var messageLength uint64
for varIntBytes == 0 { // i.e. no varint has been decoded yet.
if bytesRead >= len(headerBuf) {
return bytesRead, errInvalidVarint
}
// We have to read byte by byte here to avoid reading more bytes
// than required. Each read byte is appended to what we have
// read before.
newBytesRead, err := r.Read(headerBuf[bytesRead : bytesRead+1])
if newBytesRead == 0 {
if err != nil {
return bytesRead, err
}
// A Reader should not return (0, nil), but if it does,
// it should be treated as no-op (according to the
// Reader contract). So let's go on...
continue
}
bytesRead += newBytesRead
// Now present everything read so far to the varint decoder and
// see if a varint can be decoded already.
messageLength, varIntBytes = proto.DecodeVarint(headerBuf[:bytesRead])
}
messageBuf := make([]byte, messageLength)
newBytesRead, err := io.ReadFull(r, messageBuf)
bytesRead += newBytesRead
if err != nil {
return bytesRead, err
}
return bytesRead, proto.Unmarshal(messageBuf, m)
}

View File

@@ -1,46 +0,0 @@
// Copyright 2013 Matt T. Proud
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package pbutil
import (
"encoding/binary"
"io"
"github.com/golang/protobuf/proto"
)
// WriteDelimited encodes and dumps a message to the provided writer prefixed
// with a 32-bit varint indicating the length of the encoded message, producing
// a length-delimited record stream, which can be used to chain together
// encoded messages of the same type together in a file. It returns the total
// number of bytes written and any applicable error. This is roughly
// equivalent to the companion Java API's MessageLite#writeDelimitedTo.
func WriteDelimited(w io.Writer, m proto.Message) (n int, err error) {
buffer, err := proto.Marshal(m)
if err != nil {
return 0, err
}
var buf [binary.MaxVarintLen32]byte
encodedLength := binary.PutUvarint(buf[:], uint64(len(buffer)))
sync, err := w.Write(buf[:encodedLength])
if err != nil {
return sync, err
}
n, err = w.Write(buffer)
return n + sync, err
}

View File

@@ -16,8 +16,3 @@ Go support for Protocol Buffers - Google's data interchange format
http://github.com/golang/protobuf/ http://github.com/golang/protobuf/
Copyright 2010 The Go Authors Copyright 2010 The Go Authors
See source code for license details. See source code for license details.
Support for streaming Protocol Buffer messages for the Go language (golang).
https://github.com/matttproud/golang_protobuf_extensions
Copyright 2013 Matt T. Proud
Licensed under the Apache License, Version 2.0

View File

@@ -20,6 +20,7 @@ import (
"time" "time"
dto "github.com/prometheus/client_model/go" dto "github.com/prometheus/client_model/go"
"google.golang.org/protobuf/types/known/timestamppb"
) )
// Counter is a Metric that represents a single numerical value that only ever // Counter is a Metric that represents a single numerical value that only ever
@@ -59,6 +60,18 @@ type ExemplarAdder interface {
// CounterOpts is an alias for Opts. See there for doc comments. // CounterOpts is an alias for Opts. See there for doc comments.
type CounterOpts Opts type CounterOpts Opts
// CounterVecOpts bundles the options to create a CounterVec metric.
// It is mandatory to set CounterOpts, see there for mandatory fields. VariableLabels
// is optional and can safely be left to its default value.
type CounterVecOpts struct {
CounterOpts
// VariableLabels are used to partition the metric vector by the given set
// of labels. Each label value will be constrained with the optional Constraint
// function, if provided.
VariableLabels ConstrainableLabels
}
// NewCounter creates a new Counter based on the provided CounterOpts. // NewCounter creates a new Counter based on the provided CounterOpts.
// //
// The returned implementation also implements ExemplarAdder. It is safe to // The returned implementation also implements ExemplarAdder. It is safe to
@@ -78,8 +91,12 @@ func NewCounter(opts CounterOpts) Counter {
nil, nil,
opts.ConstLabels, opts.ConstLabels,
) )
result := &counter{desc: desc, labelPairs: desc.constLabelPairs, now: time.Now} if opts.now == nil {
opts.now = time.Now
}
result := &counter{desc: desc, labelPairs: desc.constLabelPairs, now: opts.now}
result.init(result) // Init self-collection. result.init(result) // Init self-collection.
result.createdTs = timestamppb.New(opts.now())
return result return result
} }
@@ -94,10 +111,12 @@ type counter struct {
selfCollector selfCollector
desc *Desc desc *Desc
createdTs *timestamppb.Timestamp
labelPairs []*dto.LabelPair labelPairs []*dto.LabelPair
exemplar atomic.Value // Containing nil or a *dto.Exemplar. exemplar atomic.Value // Containing nil or a *dto.Exemplar.
now func() time.Time // To mock out time.Now() for testing. // now is for testing purposes, by default it's time.Now.
now func() time.Time
} }
func (c *counter) Desc() *Desc { func (c *counter) Desc() *Desc {
@@ -147,8 +166,7 @@ func (c *counter) Write(out *dto.Metric) error {
exemplar = e.(*dto.Exemplar) exemplar = e.(*dto.Exemplar)
} }
val := c.get() val := c.get()
return populateMetric(CounterValue, val, c.labelPairs, exemplar, out, c.createdTs)
return populateMetric(CounterValue, val, c.labelPairs, exemplar, out)
} }
func (c *counter) updateExemplar(v float64, l Labels) { func (c *counter) updateExemplar(v float64, l Labels) {
@@ -174,19 +192,31 @@ type CounterVec struct {
// NewCounterVec creates a new CounterVec based on the provided CounterOpts and // NewCounterVec creates a new CounterVec based on the provided CounterOpts and
// partitioned by the given label names. // partitioned by the given label names.
func NewCounterVec(opts CounterOpts, labelNames []string) *CounterVec { func NewCounterVec(opts CounterOpts, labelNames []string) *CounterVec {
desc := NewDesc( return V2.NewCounterVec(CounterVecOpts{
CounterOpts: opts,
VariableLabels: UnconstrainedLabels(labelNames),
})
}
// NewCounterVec creates a new CounterVec based on the provided CounterVecOpts.
func (v2) NewCounterVec(opts CounterVecOpts) *CounterVec {
desc := V2.NewDesc(
BuildFQName(opts.Namespace, opts.Subsystem, opts.Name), BuildFQName(opts.Namespace, opts.Subsystem, opts.Name),
opts.Help, opts.Help,
labelNames, opts.VariableLabels,
opts.ConstLabels, opts.ConstLabels,
) )
if opts.now == nil {
opts.now = time.Now
}
return &CounterVec{ return &CounterVec{
MetricVec: NewMetricVec(desc, func(lvs ...string) Metric { MetricVec: NewMetricVec(desc, func(lvs ...string) Metric {
if len(lvs) != len(desc.variableLabels) { if len(lvs) != len(desc.variableLabels.names) {
panic(makeInconsistentCardinalityError(desc.fqName, desc.variableLabels, lvs)) panic(makeInconsistentCardinalityError(desc.fqName, desc.variableLabels.names, lvs))
} }
result := &counter{desc: desc, labelPairs: MakeLabelPairs(desc, lvs), now: time.Now} result := &counter{desc: desc, labelPairs: MakeLabelPairs(desc, lvs), now: opts.now}
result.init(result) // Init self-collection. result.init(result) // Init self-collection.
result.createdTs = timestamppb.New(opts.now())
return result return result
}), }),
} }

View File

@@ -14,20 +14,16 @@
package prometheus package prometheus
import ( import (
"errors"
"fmt" "fmt"
"sort" "sort"
"strings" "strings"
"github.com/cespare/xxhash/v2" "github.com/cespare/xxhash/v2"
dto "github.com/prometheus/client_model/go"
"github.com/prometheus/common/model"
"google.golang.org/protobuf/proto"
"github.com/prometheus/client_golang/prometheus/internal" "github.com/prometheus/client_golang/prometheus/internal"
//nolint:staticcheck // Ignore SA1019. Need to keep deprecated package for compatibility.
"github.com/golang/protobuf/proto"
"github.com/prometheus/common/model"
dto "github.com/prometheus/client_model/go"
) )
// Desc is the descriptor used by every Prometheus Metric. It is essentially // Desc is the descriptor used by every Prometheus Metric. It is essentially
@@ -54,9 +50,9 @@ type Desc struct {
// constLabelPairs contains precalculated DTO label pairs based on // constLabelPairs contains precalculated DTO label pairs based on
// the constant labels. // the constant labels.
constLabelPairs []*dto.LabelPair constLabelPairs []*dto.LabelPair
// variableLabels contains names of labels for which the metric // variableLabels contains names of labels and normalization function for
// maintains variable values. // which the metric maintains variable values.
variableLabels []string variableLabels *compiledLabels
// id is a hash of the values of the ConstLabels and fqName. This // id is a hash of the values of the ConstLabels and fqName. This
// must be unique among all registered descriptors and can therefore be // must be unique among all registered descriptors and can therefore be
// used as an identifier of the descriptor. // used as an identifier of the descriptor.
@@ -80,12 +76,27 @@ type Desc struct {
// For constLabels, the label values are constant. Therefore, they are fully // For constLabels, the label values are constant. Therefore, they are fully
// specified in the Desc. See the Collector example for a usage pattern. // specified in the Desc. See the Collector example for a usage pattern.
func NewDesc(fqName, help string, variableLabels []string, constLabels Labels) *Desc { func NewDesc(fqName, help string, variableLabels []string, constLabels Labels) *Desc {
return V2.NewDesc(fqName, help, UnconstrainedLabels(variableLabels), constLabels)
}
// NewDesc allocates and initializes a new Desc. Errors are recorded in the Desc
// and will be reported on registration time. variableLabels and constLabels can
// be nil if no such labels should be set. fqName must not be empty.
//
// variableLabels only contain the label names and normalization functions. Their
// label values are variable and therefore not part of the Desc. (They are managed
// within the Metric.)
//
// For constLabels, the label values are constant. Therefore, they are fully
// specified in the Desc. See the Collector example for a usage pattern.
func (v2) NewDesc(fqName, help string, variableLabels ConstrainableLabels, constLabels Labels) *Desc {
d := &Desc{ d := &Desc{
fqName: fqName, fqName: fqName,
help: help, help: help,
variableLabels: variableLabels, variableLabels: variableLabels.compile(),
} }
if !model.IsValidMetricName(model.LabelValue(fqName)) { //nolint:staticcheck // TODO: Don't use deprecated model.NameValidationScheme.
if !model.NameValidationScheme.IsValidMetricName(fqName) {
d.err = fmt.Errorf("%q is not a valid metric name", fqName) d.err = fmt.Errorf("%q is not a valid metric name", fqName)
return d return d
} }
@@ -93,7 +104,7 @@ func NewDesc(fqName, help string, variableLabels []string, constLabels Labels) *
// their sorted label names) plus the fqName (at position 0). // their sorted label names) plus the fqName (at position 0).
labelValues := make([]string, 1, len(constLabels)+1) labelValues := make([]string, 1, len(constLabels)+1)
labelValues[0] = fqName labelValues[0] = fqName
labelNames := make([]string, 0, len(constLabels)+len(variableLabels)) labelNames := make([]string, 0, len(constLabels)+len(d.variableLabels.names))
labelNameSet := map[string]struct{}{} labelNameSet := map[string]struct{}{}
// First add only the const label names and sort them... // First add only the const label names and sort them...
for labelName := range constLabels { for labelName := range constLabels {
@@ -118,16 +129,16 @@ func NewDesc(fqName, help string, variableLabels []string, constLabels Labels) *
// Now add the variable label names, but prefix them with something that // Now add the variable label names, but prefix them with something that
// cannot be in a regular label name. That prevents matching the label // cannot be in a regular label name. That prevents matching the label
// dimension with a different mix between preset and variable labels. // dimension with a different mix between preset and variable labels.
for _, labelName := range variableLabels { for _, label := range d.variableLabels.names {
if !checkLabelName(labelName) { if !checkLabelName(label) {
d.err = fmt.Errorf("%q is not a valid label name for metric %q", labelName, fqName) d.err = fmt.Errorf("%q is not a valid label name for metric %q", label, fqName)
return d return d
} }
labelNames = append(labelNames, "$"+labelName) labelNames = append(labelNames, "$"+label)
labelNameSet[labelName] = struct{}{} labelNameSet[label] = struct{}{}
} }
if len(labelNames) != len(labelNameSet) { if len(labelNames) != len(labelNameSet) {
d.err = errors.New("duplicate label names") d.err = fmt.Errorf("duplicate label names in constant and variable labels for metric %q", fqName)
return d return d
} }
@@ -179,11 +190,22 @@ func (d *Desc) String() string {
fmt.Sprintf("%s=%q", lp.GetName(), lp.GetValue()), fmt.Sprintf("%s=%q", lp.GetName(), lp.GetValue()),
) )
} }
vlStrings := []string{}
if d.variableLabels != nil {
vlStrings = make([]string, 0, len(d.variableLabels.names))
for _, vl := range d.variableLabels.names {
if fn, ok := d.variableLabels.labelConstraints[vl]; ok && fn != nil {
vlStrings = append(vlStrings, fmt.Sprintf("c(%s)", vl))
} else {
vlStrings = append(vlStrings, vl)
}
}
}
return fmt.Sprintf( return fmt.Sprintf(
"Desc{fqName: %q, help: %q, constLabels: {%s}, variableLabels: %v}", "Desc{fqName: %q, help: %q, constLabels: {%s}, variableLabels: {%s}}",
d.fqName, d.fqName,
d.help, d.help,
strings.Join(lpStrings, ","), strings.Join(lpStrings, ","),
d.variableLabels, strings.Join(vlStrings, ","),
) )
} }

View File

@@ -37,35 +37,35 @@
// //
// type metrics struct { // type metrics struct {
// cpuTemp prometheus.Gauge // cpuTemp prometheus.Gauge
// hdFailures *prometheus.CounterVec // hdFailures *prometheus.CounterVec
// } // }
// //
// func NewMetrics(reg prometheus.Registerer) *metrics { // func NewMetrics(reg prometheus.Registerer) *metrics {
// m := &metrics{ // m := &metrics{
// cpuTemp: prometheus.NewGauge(prometheus.GaugeOpts{ // cpuTemp: prometheus.NewGauge(prometheus.GaugeOpts{
// Name: "cpu_temperature_celsius", // Name: "cpu_temperature_celsius",
// Help: "Current temperature of the CPU.", // Help: "Current temperature of the CPU.",
// }), // }),
// hdFailures: prometheus.NewCounterVec( // hdFailures: prometheus.NewCounterVec(
// prometheus.CounterOpts{ // prometheus.CounterOpts{
// Name: "hd_errors_total", // Name: "hd_errors_total",
// Help: "Number of hard-disk errors.", // Help: "Number of hard-disk errors.",
// }, // },
// []string{"device"}, // []string{"device"},
// ), // ),
// } // }
// reg.MustRegister(m.cpuTemp) // reg.MustRegister(m.cpuTemp)
// reg.MustRegister(m.hdFailures) // reg.MustRegister(m.hdFailures)
// return m // return m
// } // }
// //
// func main() { // func main() {
// // Create a non-global registry. // // Create a non-global registry.
// reg := prometheus.NewRegistry() // reg := prometheus.NewRegistry()
// //
// // Create new metrics and register them using the custom registry. // // Create new metrics and register them using the custom registry.
// m := NewMetrics(reg) // m := NewMetrics(reg)
// // Set values for the new created metrics. // // Set values for the new created metrics.
// m.cpuTemp.Set(65.3) // m.cpuTemp.Set(65.3)
// m.hdFailures.With(prometheus.Labels{"device":"/dev/sda"}).Inc() // m.hdFailures.With(prometheus.Labels{"device":"/dev/sda"}).Inc()
// //

View File

@@ -48,7 +48,7 @@ func (e *expvarCollector) Collect(ch chan<- Metric) {
continue continue
} }
var v interface{} var v interface{}
labels := make([]string, len(desc.variableLabels)) labels := make([]string, len(desc.variableLabels.names))
if err := json.Unmarshal([]byte(expVar.String()), &v); err != nil { if err := json.Unmarshal([]byte(expVar.String()), &v); err != nil {
ch <- NewInvalidMetric(desc, err) ch <- NewInvalidMetric(desc, err)
continue continue

View File

@@ -55,6 +55,18 @@ type Gauge interface {
// GaugeOpts is an alias for Opts. See there for doc comments. // GaugeOpts is an alias for Opts. See there for doc comments.
type GaugeOpts Opts type GaugeOpts Opts
// GaugeVecOpts bundles the options to create a GaugeVec metric.
// It is mandatory to set GaugeOpts, see there for mandatory fields. VariableLabels
// is optional and can safely be left to its default value.
type GaugeVecOpts struct {
GaugeOpts
// VariableLabels are used to partition the metric vector by the given set
// of labels. Each label value will be constrained with the optional Constraint
// function, if provided.
VariableLabels ConstrainableLabels
}
// NewGauge creates a new Gauge based on the provided GaugeOpts. // NewGauge creates a new Gauge based on the provided GaugeOpts.
// //
// The returned implementation is optimized for a fast Set method. If you have a // The returned implementation is optimized for a fast Set method. If you have a
@@ -123,7 +135,7 @@ func (g *gauge) Sub(val float64) {
func (g *gauge) Write(out *dto.Metric) error { func (g *gauge) Write(out *dto.Metric) error {
val := math.Float64frombits(atomic.LoadUint64(&g.valBits)) val := math.Float64frombits(atomic.LoadUint64(&g.valBits))
return populateMetric(GaugeValue, val, g.labelPairs, nil, out) return populateMetric(GaugeValue, val, g.labelPairs, nil, out, nil)
} }
// GaugeVec is a Collector that bundles a set of Gauges that all share the same // GaugeVec is a Collector that bundles a set of Gauges that all share the same
@@ -138,16 +150,24 @@ type GaugeVec struct {
// NewGaugeVec creates a new GaugeVec based on the provided GaugeOpts and // NewGaugeVec creates a new GaugeVec based on the provided GaugeOpts and
// partitioned by the given label names. // partitioned by the given label names.
func NewGaugeVec(opts GaugeOpts, labelNames []string) *GaugeVec { func NewGaugeVec(opts GaugeOpts, labelNames []string) *GaugeVec {
desc := NewDesc( return V2.NewGaugeVec(GaugeVecOpts{
GaugeOpts: opts,
VariableLabels: UnconstrainedLabels(labelNames),
})
}
// NewGaugeVec creates a new GaugeVec based on the provided GaugeVecOpts.
func (v2) NewGaugeVec(opts GaugeVecOpts) *GaugeVec {
desc := V2.NewDesc(
BuildFQName(opts.Namespace, opts.Subsystem, opts.Name), BuildFQName(opts.Namespace, opts.Subsystem, opts.Name),
opts.Help, opts.Help,
labelNames, opts.VariableLabels,
opts.ConstLabels, opts.ConstLabels,
) )
return &GaugeVec{ return &GaugeVec{
MetricVec: NewMetricVec(desc, func(lvs ...string) Metric { MetricVec: NewMetricVec(desc, func(lvs ...string) Metric {
if len(lvs) != len(desc.variableLabels) { if len(lvs) != len(desc.variableLabels.names) {
panic(makeInconsistentCardinalityError(desc.fqName, desc.variableLabels, lvs)) panic(makeInconsistentCardinalityError(desc.fqName, desc.variableLabels.names, lvs))
} }
result := &gauge{desc: desc, labelPairs: MakeLabelPairs(desc, lvs)} result := &gauge{desc: desc, labelPairs: MakeLabelPairs(desc, lvs)}
result.init(result) // Init self-collection. result.init(result) // Init self-collection.

View File

@@ -22,13 +22,13 @@ import (
// goRuntimeMemStats provides the metrics initially provided by runtime.ReadMemStats. // goRuntimeMemStats provides the metrics initially provided by runtime.ReadMemStats.
// From Go 1.17 those similar (and better) statistics are provided by runtime/metrics, so // From Go 1.17 those similar (and better) statistics are provided by runtime/metrics, so
// while eval closure works on runtime.MemStats, the struct from Go 1.17+ is // while eval closure works on runtime.MemStats, the struct from Go 1.17+ is
// populated using runtime/metrics. // populated using runtime/metrics. Those are the defaults we can't alter.
func goRuntimeMemStats() memStatsMetrics { func goRuntimeMemStats() memStatsMetrics {
return memStatsMetrics{ return memStatsMetrics{
{ {
desc: NewDesc( desc: NewDesc(
memstatNamespace("alloc_bytes"), memstatNamespace("alloc_bytes"),
"Number of bytes allocated and still in use.", "Number of bytes allocated in heap and currently in use. Equals to /memory/classes/heap/objects:bytes.",
nil, nil, nil, nil,
), ),
eval: func(ms *runtime.MemStats) float64 { return float64(ms.Alloc) }, eval: func(ms *runtime.MemStats) float64 { return float64(ms.Alloc) },
@@ -36,7 +36,7 @@ func goRuntimeMemStats() memStatsMetrics {
}, { }, {
desc: NewDesc( desc: NewDesc(
memstatNamespace("alloc_bytes_total"), memstatNamespace("alloc_bytes_total"),
"Total number of bytes allocated, even if freed.", "Total number of bytes allocated in heap until now, even if released already. Equals to /gc/heap/allocs:bytes.",
nil, nil, nil, nil,
), ),
eval: func(ms *runtime.MemStats) float64 { return float64(ms.TotalAlloc) }, eval: func(ms *runtime.MemStats) float64 { return float64(ms.TotalAlloc) },
@@ -44,23 +44,16 @@ func goRuntimeMemStats() memStatsMetrics {
}, { }, {
desc: NewDesc( desc: NewDesc(
memstatNamespace("sys_bytes"), memstatNamespace("sys_bytes"),
"Number of bytes obtained from system.", "Number of bytes obtained from system. Equals to /memory/classes/total:byte.",
nil, nil, nil, nil,
), ),
eval: func(ms *runtime.MemStats) float64 { return float64(ms.Sys) }, eval: func(ms *runtime.MemStats) float64 { return float64(ms.Sys) },
valType: GaugeValue, valType: GaugeValue,
}, {
desc: NewDesc(
memstatNamespace("lookups_total"),
"Total number of pointer lookups.",
nil, nil,
),
eval: func(ms *runtime.MemStats) float64 { return float64(ms.Lookups) },
valType: CounterValue,
}, { }, {
desc: NewDesc( desc: NewDesc(
memstatNamespace("mallocs_total"), memstatNamespace("mallocs_total"),
"Total number of mallocs.", // TODO(bwplotka): We could add go_memstats_heap_objects, probably useful for discovery. Let's gather more feedback, kind of a waste of bytes for everybody for compatibility reasons to keep both, and we can't really rename/remove useful metric.
"Total number of heap objects allocated, both live and gc-ed. Semantically a counter version for go_memstats_heap_objects gauge. Equals to /gc/heap/allocs:objects + /gc/heap/tiny/allocs:objects.",
nil, nil, nil, nil,
), ),
eval: func(ms *runtime.MemStats) float64 { return float64(ms.Mallocs) }, eval: func(ms *runtime.MemStats) float64 { return float64(ms.Mallocs) },
@@ -68,7 +61,7 @@ func goRuntimeMemStats() memStatsMetrics {
}, { }, {
desc: NewDesc( desc: NewDesc(
memstatNamespace("frees_total"), memstatNamespace("frees_total"),
"Total number of frees.", "Total number of heap objects frees. Equals to /gc/heap/frees:objects + /gc/heap/tiny/allocs:objects.",
nil, nil, nil, nil,
), ),
eval: func(ms *runtime.MemStats) float64 { return float64(ms.Frees) }, eval: func(ms *runtime.MemStats) float64 { return float64(ms.Frees) },
@@ -76,7 +69,7 @@ func goRuntimeMemStats() memStatsMetrics {
}, { }, {
desc: NewDesc( desc: NewDesc(
memstatNamespace("heap_alloc_bytes"), memstatNamespace("heap_alloc_bytes"),
"Number of heap bytes allocated and still in use.", "Number of heap bytes allocated and currently in use, same as go_memstats_alloc_bytes. Equals to /memory/classes/heap/objects:bytes.",
nil, nil, nil, nil,
), ),
eval: func(ms *runtime.MemStats) float64 { return float64(ms.HeapAlloc) }, eval: func(ms *runtime.MemStats) float64 { return float64(ms.HeapAlloc) },
@@ -84,7 +77,7 @@ func goRuntimeMemStats() memStatsMetrics {
}, { }, {
desc: NewDesc( desc: NewDesc(
memstatNamespace("heap_sys_bytes"), memstatNamespace("heap_sys_bytes"),
"Number of heap bytes obtained from system.", "Number of heap bytes obtained from system. Equals to /memory/classes/heap/objects:bytes + /memory/classes/heap/unused:bytes + /memory/classes/heap/released:bytes + /memory/classes/heap/free:bytes.",
nil, nil, nil, nil,
), ),
eval: func(ms *runtime.MemStats) float64 { return float64(ms.HeapSys) }, eval: func(ms *runtime.MemStats) float64 { return float64(ms.HeapSys) },
@@ -92,7 +85,7 @@ func goRuntimeMemStats() memStatsMetrics {
}, { }, {
desc: NewDesc( desc: NewDesc(
memstatNamespace("heap_idle_bytes"), memstatNamespace("heap_idle_bytes"),
"Number of heap bytes waiting to be used.", "Number of heap bytes waiting to be used. Equals to /memory/classes/heap/released:bytes + /memory/classes/heap/free:bytes.",
nil, nil, nil, nil,
), ),
eval: func(ms *runtime.MemStats) float64 { return float64(ms.HeapIdle) }, eval: func(ms *runtime.MemStats) float64 { return float64(ms.HeapIdle) },
@@ -100,7 +93,7 @@ func goRuntimeMemStats() memStatsMetrics {
}, { }, {
desc: NewDesc( desc: NewDesc(
memstatNamespace("heap_inuse_bytes"), memstatNamespace("heap_inuse_bytes"),
"Number of heap bytes that are in use.", "Number of heap bytes that are in use. Equals to /memory/classes/heap/objects:bytes + /memory/classes/heap/unused:bytes",
nil, nil, nil, nil,
), ),
eval: func(ms *runtime.MemStats) float64 { return float64(ms.HeapInuse) }, eval: func(ms *runtime.MemStats) float64 { return float64(ms.HeapInuse) },
@@ -108,7 +101,7 @@ func goRuntimeMemStats() memStatsMetrics {
}, { }, {
desc: NewDesc( desc: NewDesc(
memstatNamespace("heap_released_bytes"), memstatNamespace("heap_released_bytes"),
"Number of heap bytes released to OS.", "Number of heap bytes released to OS. Equals to /memory/classes/heap/released:bytes.",
nil, nil, nil, nil,
), ),
eval: func(ms *runtime.MemStats) float64 { return float64(ms.HeapReleased) }, eval: func(ms *runtime.MemStats) float64 { return float64(ms.HeapReleased) },
@@ -116,7 +109,7 @@ func goRuntimeMemStats() memStatsMetrics {
}, { }, {
desc: NewDesc( desc: NewDesc(
memstatNamespace("heap_objects"), memstatNamespace("heap_objects"),
"Number of allocated objects.", "Number of currently allocated objects. Equals to /gc/heap/objects:objects.",
nil, nil, nil, nil,
), ),
eval: func(ms *runtime.MemStats) float64 { return float64(ms.HeapObjects) }, eval: func(ms *runtime.MemStats) float64 { return float64(ms.HeapObjects) },
@@ -124,7 +117,7 @@ func goRuntimeMemStats() memStatsMetrics {
}, { }, {
desc: NewDesc( desc: NewDesc(
memstatNamespace("stack_inuse_bytes"), memstatNamespace("stack_inuse_bytes"),
"Number of bytes in use by the stack allocator.", "Number of bytes obtained from system for stack allocator in non-CGO environments. Equals to /memory/classes/heap/stacks:bytes.",
nil, nil, nil, nil,
), ),
eval: func(ms *runtime.MemStats) float64 { return float64(ms.StackInuse) }, eval: func(ms *runtime.MemStats) float64 { return float64(ms.StackInuse) },
@@ -132,7 +125,7 @@ func goRuntimeMemStats() memStatsMetrics {
}, { }, {
desc: NewDesc( desc: NewDesc(
memstatNamespace("stack_sys_bytes"), memstatNamespace("stack_sys_bytes"),
"Number of bytes obtained from system for stack allocator.", "Number of bytes obtained from system for stack allocator. Equals to /memory/classes/heap/stacks:bytes + /memory/classes/os-stacks:bytes.",
nil, nil, nil, nil,
), ),
eval: func(ms *runtime.MemStats) float64 { return float64(ms.StackSys) }, eval: func(ms *runtime.MemStats) float64 { return float64(ms.StackSys) },
@@ -140,7 +133,7 @@ func goRuntimeMemStats() memStatsMetrics {
}, { }, {
desc: NewDesc( desc: NewDesc(
memstatNamespace("mspan_inuse_bytes"), memstatNamespace("mspan_inuse_bytes"),
"Number of bytes in use by mspan structures.", "Number of bytes in use by mspan structures. Equals to /memory/classes/metadata/mspan/inuse:bytes.",
nil, nil, nil, nil,
), ),
eval: func(ms *runtime.MemStats) float64 { return float64(ms.MSpanInuse) }, eval: func(ms *runtime.MemStats) float64 { return float64(ms.MSpanInuse) },
@@ -148,7 +141,7 @@ func goRuntimeMemStats() memStatsMetrics {
}, { }, {
desc: NewDesc( desc: NewDesc(
memstatNamespace("mspan_sys_bytes"), memstatNamespace("mspan_sys_bytes"),
"Number of bytes used for mspan structures obtained from system.", "Number of bytes used for mspan structures obtained from system. Equals to /memory/classes/metadata/mspan/inuse:bytes + /memory/classes/metadata/mspan/free:bytes.",
nil, nil, nil, nil,
), ),
eval: func(ms *runtime.MemStats) float64 { return float64(ms.MSpanSys) }, eval: func(ms *runtime.MemStats) float64 { return float64(ms.MSpanSys) },
@@ -156,7 +149,7 @@ func goRuntimeMemStats() memStatsMetrics {
}, { }, {
desc: NewDesc( desc: NewDesc(
memstatNamespace("mcache_inuse_bytes"), memstatNamespace("mcache_inuse_bytes"),
"Number of bytes in use by mcache structures.", "Number of bytes in use by mcache structures. Equals to /memory/classes/metadata/mcache/inuse:bytes.",
nil, nil, nil, nil,
), ),
eval: func(ms *runtime.MemStats) float64 { return float64(ms.MCacheInuse) }, eval: func(ms *runtime.MemStats) float64 { return float64(ms.MCacheInuse) },
@@ -164,7 +157,7 @@ func goRuntimeMemStats() memStatsMetrics {
}, { }, {
desc: NewDesc( desc: NewDesc(
memstatNamespace("mcache_sys_bytes"), memstatNamespace("mcache_sys_bytes"),
"Number of bytes used for mcache structures obtained from system.", "Number of bytes used for mcache structures obtained from system. Equals to /memory/classes/metadata/mcache/inuse:bytes + /memory/classes/metadata/mcache/free:bytes.",
nil, nil, nil, nil,
), ),
eval: func(ms *runtime.MemStats) float64 { return float64(ms.MCacheSys) }, eval: func(ms *runtime.MemStats) float64 { return float64(ms.MCacheSys) },
@@ -172,7 +165,7 @@ func goRuntimeMemStats() memStatsMetrics {
}, { }, {
desc: NewDesc( desc: NewDesc(
memstatNamespace("buck_hash_sys_bytes"), memstatNamespace("buck_hash_sys_bytes"),
"Number of bytes used by the profiling bucket hash table.", "Number of bytes used by the profiling bucket hash table. Equals to /memory/classes/profiling/buckets:bytes.",
nil, nil, nil, nil,
), ),
eval: func(ms *runtime.MemStats) float64 { return float64(ms.BuckHashSys) }, eval: func(ms *runtime.MemStats) float64 { return float64(ms.BuckHashSys) },
@@ -180,7 +173,7 @@ func goRuntimeMemStats() memStatsMetrics {
}, { }, {
desc: NewDesc( desc: NewDesc(
memstatNamespace("gc_sys_bytes"), memstatNamespace("gc_sys_bytes"),
"Number of bytes used for garbage collection system metadata.", "Number of bytes used for garbage collection system metadata. Equals to /memory/classes/metadata/other:bytes.",
nil, nil, nil, nil,
), ),
eval: func(ms *runtime.MemStats) float64 { return float64(ms.GCSys) }, eval: func(ms *runtime.MemStats) float64 { return float64(ms.GCSys) },
@@ -188,7 +181,7 @@ func goRuntimeMemStats() memStatsMetrics {
}, { }, {
desc: NewDesc( desc: NewDesc(
memstatNamespace("other_sys_bytes"), memstatNamespace("other_sys_bytes"),
"Number of bytes used for other system allocations.", "Number of bytes used for other system allocations. Equals to /memory/classes/other:bytes.",
nil, nil, nil, nil,
), ),
eval: func(ms *runtime.MemStats) float64 { return float64(ms.OtherSys) }, eval: func(ms *runtime.MemStats) float64 { return float64(ms.OtherSys) },
@@ -196,7 +189,7 @@ func goRuntimeMemStats() memStatsMetrics {
}, { }, {
desc: NewDesc( desc: NewDesc(
memstatNamespace("next_gc_bytes"), memstatNamespace("next_gc_bytes"),
"Number of heap bytes when next garbage collection will take place.", "Number of heap bytes when next garbage collection will take place. Equals to /gc/heap/goal:bytes.",
nil, nil, nil, nil,
), ),
eval: func(ms *runtime.MemStats) float64 { return float64(ms.NextGC) }, eval: func(ms *runtime.MemStats) float64 { return float64(ms.NextGC) },
@@ -225,7 +218,7 @@ func newBaseGoCollector() baseGoCollector {
nil, nil), nil, nil),
gcDesc: NewDesc( gcDesc: NewDesc(
"go_gc_duration_seconds", "go_gc_duration_seconds",
"A summary of the pause duration of garbage collection cycles.", "A summary of the wall-time pause (stop-the-world) duration in garbage collection cycles.",
nil, nil), nil, nil),
gcLastTimeDesc: NewDesc( gcLastTimeDesc: NewDesc(
"go_memstats_last_gc_time_seconds", "go_memstats_last_gc_time_seconds",

View File

@@ -17,17 +17,17 @@
package prometheus package prometheus
import ( import (
"fmt"
"math" "math"
"runtime" "runtime"
"runtime/metrics" "runtime/metrics"
"strings" "strings"
"sync" "sync"
//nolint:staticcheck // Ignore SA1019. Need to keep deprecated package for compatibility.
"github.com/golang/protobuf/proto"
dto "github.com/prometheus/client_model/go"
"github.com/prometheus/client_golang/prometheus/internal" "github.com/prometheus/client_golang/prometheus/internal"
dto "github.com/prometheus/client_model/go"
"google.golang.org/protobuf/proto"
) )
const ( const (
@@ -154,7 +154,8 @@ func defaultGoCollectorOptions() internal.GoCollectorOptions {
"/gc/heap/frees-by-size:bytes": goGCHeapFreesBytes, "/gc/heap/frees-by-size:bytes": goGCHeapFreesBytes,
}, },
RuntimeMetricRules: []internal.GoCollectorRule{ RuntimeMetricRules: []internal.GoCollectorRule{
//{Matcher: regexp.MustCompile("")}, // Recommended metrics we want by default from runtime/metrics.
{Matcher: internal.GoCollectorDefaultRuntimeMetrics},
}, },
} }
} }
@@ -204,6 +205,7 @@ func NewGoCollector(opts ...func(o *internal.GoCollectorOptions)) Collector {
// to fail here. This condition is tested in TestExpectedRuntimeMetrics. // to fail here. This condition is tested in TestExpectedRuntimeMetrics.
continue continue
} }
help := attachOriginalName(d.Description.Description, d.Name)
sampleBuf = append(sampleBuf, metrics.Sample{Name: d.Name}) sampleBuf = append(sampleBuf, metrics.Sample{Name: d.Name})
sampleMap[d.Name] = &sampleBuf[len(sampleBuf)-1] sampleMap[d.Name] = &sampleBuf[len(sampleBuf)-1]
@@ -215,7 +217,7 @@ func NewGoCollector(opts ...func(o *internal.GoCollectorOptions)) Collector {
m = newBatchHistogram( m = newBatchHistogram(
NewDesc( NewDesc(
BuildFQName(namespace, subsystem, name), BuildFQName(namespace, subsystem, name),
d.Description.Description, help,
nil, nil,
nil, nil,
), ),
@@ -227,7 +229,7 @@ func NewGoCollector(opts ...func(o *internal.GoCollectorOptions)) Collector {
Namespace: namespace, Namespace: namespace,
Subsystem: subsystem, Subsystem: subsystem,
Name: name, Name: name,
Help: d.Description.Description, Help: help,
}, },
) )
} else { } else {
@@ -235,7 +237,7 @@ func NewGoCollector(opts ...func(o *internal.GoCollectorOptions)) Collector {
Namespace: namespace, Namespace: namespace,
Subsystem: subsystem, Subsystem: subsystem,
Name: name, Name: name,
Help: d.Description.Description, Help: help,
}) })
} }
metricSet = append(metricSet, m) metricSet = append(metricSet, m)
@@ -285,6 +287,10 @@ func NewGoCollector(opts ...func(o *internal.GoCollectorOptions)) Collector {
} }
} }
func attachOriginalName(desc, origName string) string {
return fmt.Sprintf("%s Sourced from %s.", desc, origName)
}
// Describe returns all descriptions of the collector. // Describe returns all descriptions of the collector.
func (c *goCollector) Describe(ch chan<- *Desc) { func (c *goCollector) Describe(ch chan<- *Desc) {
c.base.Describe(ch) c.base.Describe(ch)
@@ -377,13 +383,13 @@ func unwrapScalarRMValue(v metrics.Value) float64 {
// //
// This should never happen because we always populate our metric // This should never happen because we always populate our metric
// set from the runtime/metrics package. // set from the runtime/metrics package.
panic("unexpected unsupported metric") panic("unexpected bad kind metric")
default: default:
// Unsupported metric kind. // Unsupported metric kind.
// //
// This should never happen because we check for this during initialization // This should never happen because we check for this during initialization
// and flag and filter metrics whose kinds we don't understand. // and flag and filter metrics whose kinds we don't understand.
panic("unexpected unsupported metric kind") panic(fmt.Sprintf("unexpected unsupported metric: %v", v.Kind()))
} }
} }

Some files were not shown because too many files have changed in this diff Show More