├── .gitignore
├── pkg
├── pty
│ ├── ipty.go
│ ├── pty_windowsarm.go
│ ├── pty.go
│ └── pty_windows.go
├── monitor
│ ├── load
│ │ └── load.go
│ ├── gpu
│ │ ├── gpu_fallback.go
│ │ ├── vendor
│ │ │ ├── nvidia_smi.go
│ │ │ └── amd_rocm_smi.go
│ │ ├── gpu_linux.go
│ │ ├── gpu_windows.go
│ │ └── gpu_darwin.go
│ ├── conn
│ │ ├── conn_fallback.go
│ │ └── conn_linux.go
│ ├── myip_test.go
│ ├── cpu
│ │ └── cpu.go
│ ├── nic
│ │ └── nic.go
│ ├── temperature
│ │ └── temperature.go
│ ├── disk
│ │ └── disk.go
│ ├── myip.go
│ └── monitor.go
├── util
│ ├── util_test.go
│ ├── http.go
│ └── util.go
├── processgroup
│ ├── process_group.go
│ └── process_group_windows.go
├── utls
│ ├── roundtripper_test.go
│ └── roundtripper.go
├── logger
│ └── logger.go
└── fm
│ ├── binary.go
│ └── tasks.go
├── .github
├── workflows
│ ├── sync-release.yml
│ ├── sync-code.yml
│ ├── sync.yml
│ ├── contributors.yml
│ ├── test.yml
│ ├── codeql-analysis.yml
│ ├── agent.yml
│ └── sync.py
└── ISSUE_TEMPLATE
│ ├── bug_report_zh.yml
│ └── bug_report.yml
├── model
├── auth.go
├── task.go
├── host.go
└── config.go
├── cmd
└── agent
│ ├── commands
│ ├── service.go
│ └── edit.go
│ ├── main_test.go
│ └── main.go
├── .goreleaser.yml
├── README.md
├── proto
├── nezha.proto
└── nezha_grpc.pb.go
├── sync.py
├── go.mod
└── go.sum
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | /agent
3 | /cmd/agent/agent
4 | /cmd/agent/config.yml
5 | *.pprof
6 | dist
--------------------------------------------------------------------------------
/pkg/pty/ipty.go:
--------------------------------------------------------------------------------
1 | package pty
2 |
3 | type IPty interface {
4 | Write(p []byte) (n int, err error)
5 | Read(p []byte) (n int, err error)
6 | Getsize() (uint16, uint16, error)
7 | Setsize(cols, rows uint32) error
8 | Close() error
9 | }
10 |
--------------------------------------------------------------------------------
/pkg/monitor/load/load.go:
--------------------------------------------------------------------------------
1 | package load
2 |
3 | import (
4 | "context"
5 |
6 | psLoad "github.com/shirou/gopsutil/v4/load"
7 | )
8 |
9 | func GetState(ctx context.Context) (*psLoad.AvgStat, error) {
10 | return psLoad.AvgWithContext(ctx)
11 | }
12 |
--------------------------------------------------------------------------------
/pkg/monitor/gpu/gpu_fallback.go:
--------------------------------------------------------------------------------
1 | //go:build !darwin && !linux && !windows
2 |
3 | package gpu
4 |
5 | import "context"
6 |
7 | func GetHost(_ context.Context) ([]string, error) {
8 | return nil, nil
9 | }
10 |
11 | func GetState(_ context.Context) ([]float64, error) {
12 | return nil, nil
13 | }
14 |
--------------------------------------------------------------------------------
/.github/workflows/sync-release.yml:
--------------------------------------------------------------------------------
1 | name: Sync Release to Gitee
2 |
3 | on:
4 | workflow_dispatch:
5 |
6 | jobs:
7 | sync-release-to-gitee:
8 | runs-on: ubuntu-latest
9 | timeout-minutes: 30
10 | env:
11 | GITEE_TOKEN: ${{ secrets.GITEE_TOKEN }}
12 | steps:
13 | - uses: actions/checkout@v4
14 | - name: Sync to Gitee
15 | run: |
16 | pip3 install PyGitHub
17 | python3 .github/workflows/sync.py
--------------------------------------------------------------------------------
/model/auth.go:
--------------------------------------------------------------------------------
1 | package model
2 |
3 | import (
4 | "context"
5 | )
6 |
7 | type AuthHandler struct {
8 | ClientSecret string
9 | ClientUUID string
10 | }
11 |
12 | func (a *AuthHandler) GetRequestMetadata(ctx context.Context, uri ...string) (map[string]string, error) {
13 | return map[string]string{"client_secret": a.ClientSecret, "client_uuid": a.ClientUUID}, nil
14 | }
15 |
16 | func (a *AuthHandler) RequireTransportSecurity() bool {
17 | return false
18 | }
19 |
--------------------------------------------------------------------------------
/.github/workflows/sync-code.yml:
--------------------------------------------------------------------------------
1 | name: Sync Code to Gitee
2 |
3 | on:
4 | push:
5 | branches: [main]
6 |
7 | jobs:
8 | sync-code-to-gitee:
9 | runs-on: ubuntu-latest
10 | steps:
11 | - uses: actions/checkout@v4
12 | - uses: adambirds/sync-github-to-gitlab-action@v1.1.0
13 | with:
14 | destination_repository: git@gitee.com:naibahq/agent.git
15 | destination_branch_name: main
16 | destination_ssh_key: ${{ secrets.GITEE_SSH_KEY }}
17 |
--------------------------------------------------------------------------------
/model/task.go:
--------------------------------------------------------------------------------
1 | package model
2 |
3 | const (
4 | _ = iota
5 | TaskTypeHTTPGet
6 | TaskTypeICMPPing
7 | TaskTypeTCPPing
8 | TaskTypeCommand
9 | TaskTypeTerminal
10 | TaskTypeUpgrade
11 | TaskTypeKeepalive
12 | TaskTypeTerminalGRPC
13 | TaskTypeNAT
14 | TaskTypeReportHostInfoDeprecated
15 | TaskTypeFM
16 | )
17 |
18 | type TerminalTask struct {
19 | StreamID string
20 | }
21 |
22 | type TaskNAT struct {
23 | StreamID string
24 | Host string
25 | }
26 |
27 | type TaskFM struct {
28 | StreamID string
29 | }
30 |
--------------------------------------------------------------------------------
/pkg/monitor/conn/conn_fallback.go:
--------------------------------------------------------------------------------
1 | //go:build !linux
2 |
3 | package conn
4 |
5 | import (
6 | "context"
7 | "syscall"
8 |
9 | "github.com/shirou/gopsutil/v4/net"
10 | )
11 |
12 | func GetState(_ context.Context) ([]uint64, error) {
13 | var tcpConnCount, udpConnCount uint64
14 |
15 | conns, _ := net.Connections("all")
16 | for i := 0; i < len(conns); i++ {
17 | switch conns[i].Type {
18 | case syscall.SOCK_STREAM:
19 | tcpConnCount++
20 | case syscall.SOCK_DGRAM:
21 | udpConnCount++
22 | }
23 | }
24 |
25 | return []uint64{tcpConnCount, udpConnCount}, nil
26 | }
27 |
--------------------------------------------------------------------------------
/cmd/agent/commands/service.go:
--------------------------------------------------------------------------------
1 | package commands
2 |
3 | import (
4 | "os"
5 |
6 | "github.com/nezhahq/service"
7 | )
8 |
9 | type Program struct {
10 | Exit chan struct{}
11 | Service service.Service
12 | Run func()
13 | }
14 |
15 | func (p *Program) Start(s service.Service) error {
16 | go p.run()
17 | return nil
18 | }
19 |
20 | func (p *Program) Stop(s service.Service) error {
21 | close(p.Exit)
22 | if service.Interactive() {
23 | os.Exit(0)
24 | }
25 | return nil
26 | }
27 |
28 | func (p *Program) run() {
29 | defer func() {
30 | if service.Interactive() {
31 | p.Stop(p.Service)
32 | } else {
33 | p.Service.Stop()
34 | }
35 | }()
36 | p.Run()
37 | }
38 |
--------------------------------------------------------------------------------
/.goreleaser.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | before:
3 | hooks:
4 | - go mod tidy -v
5 | builds:
6 | - id: universal
7 | env:
8 | - CGO_ENABLED=0
9 | ldflags:
10 | - -s -w -X github.com/nezhahq/agent/pkg/monitor.Version={{.Version}} -X main.arch={{.Arch}}
11 | goos:
12 | - linux
13 | - windows
14 | - freebsd
15 | - darwin
16 | goarch:
17 | - arm
18 | - arm64
19 | - 386
20 | - amd64
21 | - mips
22 | - mipsle
23 | - s390x
24 | - riscv64
25 | gomips:
26 | - softfloat
27 | ignore:
28 | - goos: windows
29 | goarch: arm
30 | main: ./cmd/agent
31 | binary: nezha-agent
32 | snapshot:
33 | version_template: "nezha-agent"
34 |
--------------------------------------------------------------------------------
/pkg/util/util_test.go:
--------------------------------------------------------------------------------
1 | package util
2 |
3 | import (
4 | "reflect"
5 | "testing"
6 | )
7 |
8 | func TestGenerateQueue(t *testing.T) {
9 | cases := []struct {
10 | start, size int
11 | want []int
12 | }{
13 | {0, 2, []int{0, 1}},
14 | {1, 2, []int{1, 0}},
15 | {0, 3, []int{0, 1, 2}},
16 | {1, 3, []int{1, 2, 0}},
17 | {2, 3, []int{2, 0, 1}},
18 | }
19 |
20 | gq := func(start, size int) []int {
21 | result := make([]int, size)
22 | for i := 0; i < size; i++ {
23 | result[i] = RotateQueue1(start, i, size)
24 | }
25 | return result
26 | }
27 |
28 | for _, c := range cases {
29 | if !reflect.DeepEqual(c.want, gq(c.start, c.size)) {
30 | t.Errorf("generateQueue(%d, %d) == %d, want %d", c.start, c.size, gq(c.start, c.size), c.want)
31 | }
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/.github/workflows/sync.yml:
--------------------------------------------------------------------------------
1 | name: Sync Releases and Tags
2 |
3 | on:
4 | workflow_dispatch:
5 | schedule:
6 | - cron: '0 0 * * *' # 每天运行一次
7 | push:
8 | tags:
9 | - '*'
10 |
11 | jobs:
12 | sync:
13 | runs-on: ubuntu-latest
14 |
15 | steps:
16 | - name: Checkout the repository
17 | uses: actions/checkout@v3
18 |
19 | - name: Set up Python
20 | uses: actions/setup-python@v4
21 | with:
22 | python-version: '3.x'
23 |
24 | - name: Install dependencies
25 | run: pip install requests
26 |
27 | - name: Sync Tags and Releases
28 | env:
29 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
30 | SOURCE_REPO: nezhahq/agent
31 | TARGET_REPO: amclubs/am-nezha-agent
32 | run: |
33 | python sync.py
34 |
--------------------------------------------------------------------------------
/pkg/monitor/myip_test.go:
--------------------------------------------------------------------------------
1 | package monitor
2 |
3 | import (
4 | "io"
5 | "testing"
6 | )
7 |
8 | func TestGeoIPApi(t *testing.T) {
9 | for i := 0; i < len(cfList); i++ {
10 | resp, err := httpGetWithUA(httpClientV4, cfList[i])
11 | if err != nil {
12 | t.Fatalf("httpGetWithUA(%s) error: %v", cfList[i], err)
13 | }
14 | body, err := io.ReadAll(resp.Body)
15 | if err != nil {
16 | t.Fatalf("io.ReadAll(%s) error: %v", cfList[i], err)
17 | }
18 | resp.Body.Close()
19 | ip := string(body)
20 | t.Logf("%s %s", cfList[i], ip)
21 | if ip == "" {
22 | t.Fatalf("httpGetWithUA(%s) error: %v", cfList[i], err)
23 | }
24 | }
25 | }
26 |
27 | func TestFetchGeoIP(t *testing.T) {
28 | ip := fetchIP(cfList, false)
29 | if ip == "" {
30 | t.Fatalf("fetchGeoIP() error: %v", ip)
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/cmd/agent/main_test.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 | "net"
6 | "os"
7 | "testing"
8 | )
9 |
10 | func TestLookupIP(t *testing.T) {
11 | if ci := os.Getenv("CI"); ci != "" { // skip if test on CI
12 | return
13 | }
14 |
15 | ip, err := lookupIP("www.google.com")
16 | fmt.Printf("ip: %v, err: %v\n", ip, err)
17 | if err != nil {
18 | t.Errorf("lookupIP failed: %v", err)
19 | }
20 | _, err = net.ResolveIPAddr("ip", "www.google.com")
21 | if err != nil {
22 | t.Errorf("ResolveIPAddr failed: %v", err)
23 | }
24 |
25 | ip, err = lookupIP("ipv6.google.com")
26 | fmt.Printf("ip: %v, err: %v\n", ip, err)
27 | if err != nil {
28 | t.Errorf("lookupIP failed: %v", err)
29 | }
30 | _, err = net.ResolveIPAddr("ip", "ipv6.google.com")
31 | if err != nil {
32 | t.Errorf("ResolveIPAddr failed: %v", err)
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/.github/workflows/contributors.yml:
--------------------------------------------------------------------------------
1 | name: Contributors
2 |
3 | on:
4 | push:
5 | branches: [main]
6 |
7 | jobs:
8 | contributors:
9 | runs-on: ubuntu-latest
10 | steps:
11 | - name: Generate Contributors Images
12 | uses: jaywcjlove/github-action-contributors@main
13 | id: contributors
14 | with:
15 | filter-author: (renovate\[bot\]|renovate-bot|dependabot\[bot\])
16 | hideName: 'false' # Hide names in htmlTable
17 | avatarSize: 50 # Set the avatar size.
18 | truncate: 6
19 | avatarMargin: 8
20 |
21 | - name: Modify htmlTable README.md
22 | uses: jaywcjlove/github-action-modify-file-content@main
23 | with:
24 | message: update contributors[no ci]
25 | token: ${{ secrets.NAIBA_PAT }}
26 | openDelimiter: ''
27 | closeDelimiter: ''
28 | path: README.md
29 | body: '${{steps.contributors.outputs.htmlList}}'
--------------------------------------------------------------------------------
/.github/workflows/test.yml:
--------------------------------------------------------------------------------
1 | name: Run Tests
2 |
3 | on:
4 | push:
5 | paths-ignore:
6 | - ".github/workflows/agent.yml"
7 | - ".github/workflows/codeql-analysis.yml"
8 | - ".github/workflows/test-on-pr.yml"
9 | - ".github/workflows/contributors.yml"
10 | - "README.md"
11 | - ".goreleaser.yml"
12 | pull_request:
13 | branches:
14 | - main
15 |
16 | jobs:
17 | tests:
18 | strategy:
19 | fail-fast: true
20 | matrix:
21 | os: [ubuntu, windows, macos]
22 |
23 | runs-on: ${{ matrix.os }}-latest
24 | steps:
25 | - uses: actions/checkout@v4
26 | - uses: actions/setup-go@v5
27 | with:
28 | go-version: "1.20.14"
29 |
30 | - name: Unit test
31 | run: |
32 | go test -v ./...
33 |
34 | #- name: Run Gosec Security Scanner
35 | # if: runner.os == 'Linux'
36 | # run: |
37 | # go install github.com/securego/gosec/v2/cmd/gosec@v2.19.0
38 | # gosec -exclude=G104 ./...
39 |
--------------------------------------------------------------------------------
/pkg/monitor/cpu/cpu.go:
--------------------------------------------------------------------------------
1 | package cpu
2 |
3 | import (
4 | "context"
5 | "fmt"
6 |
7 | psCpu "github.com/shirou/gopsutil/v4/cpu"
8 | )
9 |
10 | type CPUHostType string
11 |
12 | const CPUHostKey CPUHostType = "cpu"
13 |
14 | func GetHost(ctx context.Context) ([]string, error) {
15 | ci, err := psCpu.InfoWithContext(ctx)
16 | if err != nil {
17 | return nil, err
18 | }
19 |
20 | cpuModelCount := make(map[string]int)
21 | for _, c := range ci {
22 | cpuModelCount[c.ModelName] += int(c.Cores)
23 | }
24 |
25 | var cpuType string
26 | if t, ok := ctx.Value(CPUHostKey).(string); ok {
27 | cpuType = t
28 | }
29 |
30 | ch := make([]string, 0, len(cpuModelCount))
31 | for model, count := range cpuModelCount {
32 | ch = append(ch, fmt.Sprintf("%s %d %s Core", model, count, cpuType))
33 | }
34 |
35 | return ch, nil
36 | }
37 |
38 | func GetState(ctx context.Context) ([]float64, error) {
39 | cp, err := psCpu.PercentWithContext(ctx, 0, false)
40 | if err != nil {
41 | return nil, err
42 | }
43 |
44 | return cp, nil
45 | }
46 |
--------------------------------------------------------------------------------
/pkg/monitor/nic/nic.go:
--------------------------------------------------------------------------------
1 | package nic
2 |
3 | import (
4 | "context"
5 |
6 | "github.com/cloudflare/ahocorasick"
7 | "github.com/shirou/gopsutil/v4/net"
8 | )
9 |
10 | type NICKeyType string
11 |
12 | const NICKey NICKeyType = "nic"
13 |
14 | var (
15 | excludeNetInterfaces = []string{
16 | "lo", "tun", "docker", "veth", "br-", "vmbr", "vnet", "kube",
17 | }
18 |
19 | defaultMatcher = ahocorasick.NewStringMatcher(excludeNetInterfaces)
20 | )
21 |
22 | func GetState(ctx context.Context) ([]uint64, error) {
23 | var netInTransfer, netOutTransfer uint64
24 | nc, err := net.IOCountersWithContext(ctx, true)
25 | if err != nil {
26 | return nil, err
27 | }
28 |
29 | allowList, _ := ctx.Value(NICKey).(map[string]bool)
30 |
31 | for _, v := range nc {
32 | if defaultMatcher.Contains([]byte(v.Name)) && !allowList[v.Name] {
33 | continue
34 | }
35 | if len(allowList) > 0 && !allowList[v.Name] {
36 | continue
37 | }
38 | netInTransfer += v.BytesRecv
39 | netOutTransfer += v.BytesSent
40 | }
41 |
42 | return []uint64{netInTransfer, netOutTransfer}, nil
43 | }
44 |
--------------------------------------------------------------------------------
/pkg/monitor/temperature/temperature.go:
--------------------------------------------------------------------------------
1 | package temperature
2 |
3 | import (
4 | "context"
5 | "fmt"
6 | "sort"
7 |
8 | "github.com/shirou/gopsutil/v4/sensors"
9 |
10 | "github.com/nezhahq/agent/model"
11 | "github.com/nezhahq/agent/pkg/util"
12 | )
13 |
14 | var sensorIgnoreList = []string{
15 | "PMU tcal", // the calibration sensor on arm macs, value is fixed
16 | "noname",
17 | }
18 |
19 | func GetState(_ context.Context) ([]model.SensorTemperature, error) {
20 | temperatures, err := sensors.SensorsTemperatures()
21 | if err != nil {
22 | return nil, fmt.Errorf("SensorsTemperatures: %v", err)
23 | }
24 |
25 | var tempStat []model.SensorTemperature
26 | for _, t := range temperatures {
27 | if t.Temperature > 0 && !util.ContainsStr(sensorIgnoreList, t.SensorKey) {
28 | tempStat = append(tempStat, model.SensorTemperature{
29 | Name: t.SensorKey,
30 | Temperature: t.Temperature,
31 | })
32 | }
33 | }
34 |
35 | sort.Slice(tempStat, func(i, j int) bool {
36 | return tempStat[i].Name < tempStat[j].Name
37 | })
38 |
39 | return tempStat, nil
40 | }
41 |
--------------------------------------------------------------------------------
/pkg/processgroup/process_group.go:
--------------------------------------------------------------------------------
1 | //go:build !windows
2 |
3 | package processgroup
4 |
5 | import (
6 | "os/exec"
7 | "sync"
8 | "syscall"
9 | )
10 |
11 | type ProcessExitGroup struct {
12 | cmds []*exec.Cmd
13 | }
14 |
15 | func NewProcessExitGroup() (ProcessExitGroup, error) {
16 | return ProcessExitGroup{}, nil
17 | }
18 |
19 | func NewCommand(arg string) *exec.Cmd {
20 | cmd := exec.Command("sh", "-c", arg)
21 | cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
22 | return cmd
23 | }
24 |
25 | func (g *ProcessExitGroup) Dispose() error {
26 | var wg sync.WaitGroup
27 | wg.Add(len(g.cmds))
28 |
29 | for _, c := range g.cmds {
30 | go func(c *exec.Cmd) {
31 | defer wg.Done()
32 | killChildProcess(c)
33 | }(c)
34 | }
35 |
36 | wg.Wait()
37 | return nil
38 | }
39 |
40 | func (g *ProcessExitGroup) AddProcess(cmd *exec.Cmd) error {
41 | g.cmds = append(g.cmds, cmd)
42 | return nil
43 | }
44 |
45 | func killChildProcess(c *exec.Cmd) {
46 | pgid, err := syscall.Getpgid(c.Process.Pid)
47 | if err != nil {
48 | // Fall-back on error. Kill the main process only.
49 | c.Process.Kill()
50 | } else {
51 | // Kill the whole process group.
52 | syscall.Kill(-pgid, syscall.SIGTERM)
53 | }
54 | c.Wait()
55 | }
56 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report_zh.yml:
--------------------------------------------------------------------------------
1 | name: 问题反馈
2 | description: 提交 nezha-agent 问题反馈
3 |
4 | body:
5 | - type: input
6 | attributes:
7 | label: 运行环境
8 | description: 请在这里输入你的系统信息及设备架构
9 | placeholder: Debian GNU/Linux 12 6.1.0-22-amd64
10 | validations:
11 | required: true
12 | - type: input
13 | attributes:
14 | label: Agent 版本
15 | description: 在这里输入你的 nezha-agent 版本号
16 | validations:
17 | required: true
18 | - type: textarea
19 | attributes:
20 | label: 描述问题
21 | description: 一个清晰明了的问题描述
22 | value: |
23 |
24 | validations:
25 | required: true
26 | - type: textarea
27 | attributes:
28 | label: 复现步骤
29 | description: 在这里输入你的配置信息及复现问题的步骤
30 | value: |
31 |
32 | validations:
33 | required: true
34 | - type: textarea
35 | attributes:
36 | label: 附加信息
37 | description: 在这里输入其它对问题解决有帮助的信息
38 | value: |
39 |
40 | - type: checkboxes
41 | attributes:
42 | label: 验证
43 | options:
44 | - label: 我确认这是一个 nezha-agent 的问题。
45 | required: true
46 | - label: 我已经搜索了 Issues,并确认该问题之前没有被反馈过。
47 | required: true
48 |
--------------------------------------------------------------------------------
/pkg/monitor/conn/conn_linux.go:
--------------------------------------------------------------------------------
1 | //go:build linux
2 |
3 | package conn
4 |
5 | import (
6 | "context"
7 | "syscall"
8 |
9 | "github.com/dean2021/goss"
10 | "github.com/shirou/gopsutil/v4/net"
11 | )
12 |
13 | func GetState(_ context.Context) ([]uint64, error) {
14 | var tcpConnCount, udpConnCount uint64
15 |
16 | tcpStat, err := goss.ConnectionsWithProtocol(goss.AF_INET, syscall.IPPROTO_TCP)
17 | if err == nil {
18 | tcpConnCount = uint64(len(tcpStat))
19 | }
20 |
21 | udpStat, err := goss.ConnectionsWithProtocol(goss.AF_INET, syscall.IPPROTO_UDP)
22 | if err == nil {
23 | udpConnCount = uint64(len(udpStat))
24 | }
25 |
26 | tcpStat6, err := goss.ConnectionsWithProtocol(goss.AF_INET6, syscall.IPPROTO_TCP)
27 | if err == nil {
28 | tcpConnCount += uint64(len(tcpStat6))
29 | }
30 |
31 | udpStat6, err := goss.ConnectionsWithProtocol(goss.AF_INET6, syscall.IPPROTO_UDP)
32 | if err == nil {
33 | udpConnCount += uint64(len(udpStat6))
34 | }
35 |
36 | if tcpConnCount < 1 && udpConnCount < 1 {
37 | // fallback to parsing files
38 | conns, _ := net.Connections("all")
39 | for _, conn := range conns {
40 | switch conn.Type {
41 | case syscall.SOCK_STREAM:
42 | tcpConnCount++
43 | case syscall.SOCK_DGRAM:
44 | udpConnCount++
45 | }
46 | }
47 | }
48 |
49 | return []uint64{tcpConnCount, udpConnCount}, nil
50 | }
51 |
--------------------------------------------------------------------------------
/pkg/pty/pty_windowsarm.go:
--------------------------------------------------------------------------------
1 | //go:build windows && arm64
2 |
3 | package pty
4 |
5 | import (
6 | "os"
7 | "os/exec"
8 | "path/filepath"
9 |
10 | "github.com/UserExistsError/conpty"
11 | )
12 |
13 | var _ IPty = (*Pty)(nil)
14 |
15 | type Pty struct {
16 | tty *conpty.ConPty
17 | }
18 |
19 | func DownloadDependency() error {
20 | return nil
21 | }
22 |
23 | func getExecutableFilePath() (string, error) {
24 | ex, err := os.Executable()
25 | if err != nil {
26 | return "", err
27 | }
28 | return filepath.Dir(ex), nil
29 | }
30 |
31 | func Start() (IPty, error) {
32 | shellPath, err := exec.LookPath("powershell.exe")
33 | if err != nil || shellPath == "" {
34 | shellPath = "cmd.exe"
35 | }
36 | path, err := getExecutableFilePath()
37 | if err != nil {
38 | return nil, err
39 | }
40 | tty, err := conpty.Start(shellPath, conpty.ConPtyWorkDir(path))
41 | return &Pty{tty: tty}, err
42 | }
43 |
44 | func (pty *Pty) Write(p []byte) (n int, err error) {
45 | return pty.tty.Write(p)
46 | }
47 |
48 | func (pty *Pty) Read(p []byte) (n int, err error) {
49 | return pty.tty.Read(p)
50 | }
51 |
52 | func (pty *Pty) Getsize() (uint16, uint16, error) {
53 | return 80, 40, nil
54 | }
55 |
56 | func (pty *Pty) Setsize(cols, rows uint32) error {
57 | return pty.tty.Resize(int(cols), int(rows))
58 | }
59 |
60 | func (pty *Pty) Close() error {
61 | if err := pty.tty.Close(); err != nil {
62 | return err
63 | }
64 | return nil
65 | }
66 |
--------------------------------------------------------------------------------
/pkg/utls/roundtripper_test.go:
--------------------------------------------------------------------------------
1 | package utls_test
2 |
3 | import (
4 | "net/http"
5 | "os"
6 | "testing"
7 |
8 | utls "github.com/refraction-networking/utls"
9 |
10 | "github.com/nezhahq/agent/pkg/util"
11 | utlsx "github.com/nezhahq/agent/pkg/utls"
12 | )
13 |
14 | const url = "https://www.patreon.com/login"
15 |
16 | func TestCloudflareDetection(t *testing.T) {
17 | if ci := os.Getenv("CI"); ci != "" { // skip if test on CI
18 | return
19 | }
20 |
21 | client := http.DefaultClient
22 |
23 | t.Logf("testing connection to %s", url)
24 | resp, err := doRequest(client, url)
25 | if err != nil {
26 | t.Errorf("Get %s failed: %v", url, err)
27 | }
28 |
29 | if resp.StatusCode == 403 {
30 | t.Log("Default client is detected, switching to client with utls transport")
31 | headers := util.BrowserHeaders()
32 | client.Transport = utlsx.NewUTLSHTTPRoundTripperWithProxy(
33 | utls.HelloChrome_Auto, new(utls.Config),
34 | http.DefaultTransport, nil, &headers,
35 | )
36 | resp, err = doRequest(client, url)
37 | if err != nil {
38 | t.Errorf("Get %s failed: %v", url, err)
39 | }
40 | if resp.StatusCode == 403 {
41 | t.Fail()
42 | } else {
43 | t.Log("Client with utls transport passed Cloudflare detection")
44 | }
45 | } else {
46 | t.Log("Default client passed Cloudflare detection")
47 | }
48 | }
49 |
50 | func doRequest(client *http.Client, url string) (*http.Response, error) {
51 | resp, err := client.Get(url)
52 | if err != nil {
53 | return nil, err
54 | }
55 | defer resp.Body.Close()
56 | return resp, nil
57 | }
58 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.yml:
--------------------------------------------------------------------------------
1 | name: Bug Report
2 | description: File a bug report about nezha-agent.
3 |
4 | body:
5 | - type: input
6 | attributes:
7 | label: Environment
8 | description: Input your OS information and host architecture here.
9 | placeholder: Debian GNU/Linux 12 6.1.0-22-amd64
10 | validations:
11 | required: true
12 | - type: input
13 | attributes:
14 | label: Agent Version
15 | description: Input the version of your nezha-agent here.
16 | validations:
17 | required: true
18 | - type: textarea
19 | attributes:
20 | label: Describe the bug
21 | description: A clear and concise description of what the bug is.
22 | value: |
23 |
24 | validations:
25 | required: true
26 | - type: textarea
27 | attributes:
28 | label: To Reproduce
29 | description: Input your configuration and the steps to reproduce the bug here.
30 | value: |
31 |
32 | validations:
33 | required: true
34 | - type: textarea
35 | attributes:
36 | label: Additional Context
37 | description: Input any other relevant information that may help understand the issue.
38 | value: |
39 |
40 | - type: checkboxes
41 | attributes:
42 | label: Validation
43 | options:
44 | - label: I confirm this is a bug about nezha-agent.
45 | required: true
46 | - label: I have searched Issues and confirm this bug has been reported before.
47 | required: true
48 |
--------------------------------------------------------------------------------
/pkg/logger/logger.go:
--------------------------------------------------------------------------------
1 | package logger
2 |
3 | import (
4 | "fmt"
5 | "sync"
6 | "time"
7 |
8 | "github.com/nezhahq/service"
9 | )
10 |
11 | var (
12 | DefaultLogger = &ServiceLogger{enabled: true, logger: service.ConsoleLogger}
13 |
14 | loggerOnce sync.Once
15 | )
16 |
17 | type ServiceLogger struct {
18 | enabled bool
19 | logger service.Logger
20 | }
21 |
22 | func InitDefaultLogger(enabled bool, logger service.Logger) {
23 | loggerOnce.Do(func() {
24 | DefaultLogger.enabled = enabled
25 | DefaultLogger.logger = logger
26 | })
27 | }
28 |
29 | func NewServiceLogger(enable bool, logger service.Logger) *ServiceLogger {
30 | return &ServiceLogger{
31 | enabled: enable,
32 | logger: logger,
33 | }
34 | }
35 |
36 | func (s *ServiceLogger) Println(v ...interface{}) {
37 | if s.enabled {
38 | s.logger.Infof("NEZHA@%s>> %v", time.Now().Format("2006-01-02 15:04:05"), fmt.Sprint(v...))
39 | }
40 | }
41 |
42 | func (s *ServiceLogger) Printf(format string, v ...interface{}) {
43 | if s.enabled {
44 | s.logger.Infof("NEZHA@%s>> "+format, append([]interface{}{time.Now().Format("2006-01-02 15:04:05")}, v...)...)
45 | }
46 | }
47 |
48 | func (s *ServiceLogger) Error(v ...interface{}) error {
49 | if s.enabled {
50 | return s.logger.Errorf("NEZHA@%s>> %v", time.Now().Format("2006-01-02 15:04:05"), fmt.Sprint(v...))
51 | }
52 | return nil
53 | }
54 |
55 | func (s *ServiceLogger) Errorf(format string, v ...interface{}) error {
56 | if s.enabled {
57 | return s.logger.Errorf("NEZHA@%s>> "+format, append([]interface{}{time.Now().Format("2006-01-02 15:04:05")}, v...)...)
58 | }
59 | return nil
60 | }
61 |
--------------------------------------------------------------------------------
/pkg/fm/binary.go:
--------------------------------------------------------------------------------
1 | package fm
2 |
3 | import (
4 | "bytes"
5 | "encoding/binary"
6 | )
7 |
8 | var (
9 | fileIdentifier = []byte{0x4E, 0x5A, 0x54, 0x44} // NZTD
10 | fileNameIdentifier = []byte{0x4E, 0x5A, 0x46, 0x4E} // NZFN
11 | errorIdentifier = []byte{0x4E, 0x45, 0x52, 0x52} // NERR
12 | completeIdentifier = []byte{0x4E, 0x5A, 0x55, 0x50} // NZUP
13 | )
14 |
15 | func AppendFileName(bin []byte, data string, isDir bool) []byte {
16 | buffer := bytes.NewBuffer(bin)
17 | appendFileName(buffer, isDir, []byte(data))
18 | return buffer.Bytes()
19 | }
20 |
21 | func Create(buffer *bytes.Buffer, path string) []byte {
22 | // Write identifier for TypeFileName (4 bytes)
23 | binary.Write(buffer, binary.BigEndian, fileNameIdentifier)
24 |
25 | // Write length of path (4 byte)
26 | binary.Write(buffer, binary.BigEndian, uint32(len(path)))
27 |
28 | // Write path string
29 | binary.Write(buffer, binary.BigEndian, []byte(path))
30 | return buffer.Bytes()
31 | }
32 |
33 | func CreateFile(buffer *bytes.Buffer, size uint64) []byte {
34 | // Write identifier for TypeFile (4 bytes)
35 | binary.Write(buffer, binary.BigEndian, fileIdentifier)
36 |
37 | // Write file size (8 bytes)
38 | binary.Write(buffer, binary.BigEndian, size)
39 | return buffer.Bytes()
40 | }
41 |
42 | func CreateErr(err error) []byte {
43 | buffer := new(bytes.Buffer)
44 |
45 | binary.Write(buffer, binary.BigEndian, errorIdentifier)
46 | binary.Write(buffer, binary.BigEndian, []byte(err.Error()))
47 |
48 | return buffer.Bytes()
49 | }
50 |
51 | func appendFileName(buffer *bytes.Buffer, isDir bool, data []byte) {
52 | // Write file type (1 byte)
53 | if isDir {
54 | binary.Write(buffer, binary.BigEndian, byte(1))
55 | } else {
56 | binary.Write(buffer, binary.BigEndian, byte(0))
57 | }
58 |
59 | // Write the length of file name (1 byte)
60 | length := byte(len(data))
61 | binary.Write(buffer, binary.BigEndian, length)
62 |
63 | // Write file name
64 | buffer.Write(data)
65 | }
66 |
--------------------------------------------------------------------------------
/pkg/pty/pty.go:
--------------------------------------------------------------------------------
1 | //go:build !windows
2 |
3 | package pty
4 |
5 | import (
6 | "errors"
7 | "os"
8 | "os/exec"
9 | "syscall"
10 |
11 | opty "github.com/creack/pty"
12 | )
13 |
14 | var _ IPty = (*Pty)(nil)
15 |
16 | var defaultShells = []string{"zsh", "fish", "bash", "sh"}
17 |
18 | type Pty struct {
19 | tty *os.File
20 | cmd *exec.Cmd
21 | }
22 |
23 | func DownloadDependency() error {
24 | return nil
25 | }
26 |
27 | func Start() (IPty, error) {
28 | var shellPath string
29 | for i := 0; i < len(defaultShells); i++ {
30 | shellPath, _ = exec.LookPath(defaultShells[i])
31 | if shellPath != "" {
32 | break
33 | }
34 | }
35 | if shellPath == "" {
36 | return nil, errors.New("没有可用终端")
37 | }
38 | cmd := exec.Command(shellPath) // #nosec
39 | cmd.Env = append(os.Environ(), "TERM=xterm")
40 | tty, err := opty.Start(cmd)
41 | return &Pty{tty: tty, cmd: cmd}, err
42 | }
43 |
44 | func (pty *Pty) Write(p []byte) (n int, err error) {
45 | return pty.tty.Write(p)
46 | }
47 |
48 | func (pty *Pty) Read(p []byte) (n int, err error) {
49 | return pty.tty.Read(p)
50 | }
51 |
52 | func (pty *Pty) Getsize() (uint16, uint16, error) {
53 | ws, err := opty.GetsizeFull(pty.tty)
54 | if err != nil {
55 | return 0, 0, err
56 | }
57 | return ws.Cols, ws.Rows, nil
58 | }
59 |
60 | func (pty *Pty) Setsize(cols, rows uint32) error {
61 | return opty.Setsize(pty.tty, &opty.Winsize{
62 | Cols: uint16(cols),
63 | Rows: uint16(rows),
64 | })
65 | }
66 |
67 | func (pty *Pty) killChildProcess(c *exec.Cmd) error {
68 | pgid, err := syscall.Getpgid(c.Process.Pid)
69 | if err != nil {
70 | // Fall-back on error. Kill the main process only.
71 | c.Process.Kill()
72 | }
73 | // Kill the whole process group.
74 | syscall.Kill(-pgid, syscall.SIGKILL) // SIGKILL 直接杀掉 SIGTERM 发送信号,等待进程自己退出
75 | return c.Wait()
76 | }
77 |
78 | func (pty *Pty) Close() error {
79 | if err := pty.tty.Close(); err != nil {
80 | return err
81 | }
82 | return pty.killChildProcess(pty.cmd)
83 | }
84 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Nezha Agent
2 |
3 | Agent of Nezha Monitoring
4 |
5 | ## Contributors
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/proto/nezha.proto:
--------------------------------------------------------------------------------
1 | syntax = "proto3";
2 | option go_package = "./proto";
3 |
4 | package proto;
5 |
6 | service NezhaService {
7 | rpc ReportSystemState(stream State) returns (stream Receipt) {}
8 | rpc ReportSystemInfo(Host) returns (Receipt) {}
9 | rpc RequestTask(stream TaskResult) returns (stream Task) {}
10 | rpc IOStream(stream IOStreamData) returns (stream IOStreamData) {}
11 | rpc ReportGeoIP(GeoIP) returns (GeoIP) {}
12 | rpc ReportSystemInfo2(Host) returns (Uint64Receipt) {}
13 | }
14 |
15 | message Host {
16 | string platform = 1;
17 | string platform_version = 2;
18 | repeated string cpu = 3;
19 | uint64 mem_total = 4;
20 | uint64 disk_total = 5;
21 | uint64 swap_total = 6;
22 | string arch = 7;
23 | string virtualization = 8;
24 | uint64 boot_time = 9;
25 | string version = 10;
26 | repeated string gpu = 11;
27 | }
28 |
29 | message State {
30 | double cpu = 1;
31 | uint64 mem_used = 2;
32 | uint64 swap_used = 3;
33 | uint64 disk_used = 4;
34 | uint64 net_in_transfer = 5;
35 | uint64 net_out_transfer = 6;
36 | uint64 net_in_speed = 7;
37 | uint64 net_out_speed = 8;
38 | uint64 uptime = 9;
39 | double load1 = 10;
40 | double load5 = 11;
41 | double load15 = 12;
42 | uint64 tcp_conn_count = 13;
43 | uint64 udp_conn_count = 14;
44 | uint64 process_count = 15;
45 | repeated State_SensorTemperature temperatures = 16;
46 | repeated double gpu = 17;
47 | }
48 |
49 | message State_SensorTemperature {
50 | string name = 1;
51 | double temperature = 2;
52 | }
53 |
54 | message Task {
55 | uint64 id = 1;
56 | uint64 type = 2;
57 | string data = 3;
58 | }
59 |
60 | message TaskResult {
61 | uint64 id = 1;
62 | uint64 type = 2;
63 | float delay = 3;
64 | string data = 4;
65 | bool successful = 5;
66 | }
67 |
68 | message Receipt { bool proced = 1; }
69 |
70 | message Uint64Receipt { uint64 data = 1; }
71 |
72 | message IOStreamData { bytes data = 1; }
73 |
74 | message GeoIP {
75 | bool use6 = 1;
76 | IP ip = 2;
77 | string country_code = 3;
78 | }
79 |
80 | message IP {
81 | string ipv4 = 1;
82 | string ipv6 = 2;
83 | }
84 |
--------------------------------------------------------------------------------
/pkg/processgroup/process_group_windows.go:
--------------------------------------------------------------------------------
1 | //go:build windows
2 |
3 | package processgroup
4 |
5 | import (
6 | "fmt"
7 | "os/exec"
8 | "unsafe"
9 |
10 | "golang.org/x/sys/windows"
11 | )
12 |
13 | type ProcessExitGroup struct {
14 | cmds []*exec.Cmd
15 | jobHandle windows.Handle
16 | procs []windows.Handle
17 | }
18 |
19 | func NewProcessExitGroup() (*ProcessExitGroup, error) {
20 | job, err := windows.CreateJobObject(nil, nil)
21 | if err != nil {
22 | return nil, err
23 | }
24 |
25 | info := windows.JOBOBJECT_EXTENDED_LIMIT_INFORMATION{
26 | BasicLimitInformation: windows.JOBOBJECT_BASIC_LIMIT_INFORMATION{
27 | LimitFlags: windows.JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE,
28 | },
29 | }
30 |
31 | _, err = windows.SetInformationJobObject(
32 | job,
33 | windows.JobObjectExtendedLimitInformation,
34 | uintptr(unsafe.Pointer(&info)),
35 | uint32(unsafe.Sizeof(info)))
36 |
37 | return &ProcessExitGroup{jobHandle: job}, nil
38 | }
39 |
40 | func NewCommand(args string) *exec.Cmd {
41 | cmd := exec.Command("cmd")
42 | cmd.SysProcAttr = &windows.SysProcAttr{
43 | CmdLine: fmt.Sprintf("/c %s", args),
44 | CreationFlags: windows.CREATE_NEW_PROCESS_GROUP,
45 | }
46 | return cmd
47 | }
48 |
49 | func (g *ProcessExitGroup) AddProcess(cmd *exec.Cmd) error {
50 | proc, err := windows.OpenProcess(windows.PROCESS_TERMINATE|windows.PROCESS_SET_QUOTA|windows.PROCESS_SET_INFORMATION, false, uint32(cmd.Process.Pid))
51 | if err != nil {
52 | return err
53 | }
54 |
55 | g.procs = append(g.procs, proc)
56 | g.cmds = append(g.cmds, cmd)
57 |
58 | return windows.AssignProcessToJobObject(g.jobHandle, proc)
59 | }
60 |
61 | func (g *ProcessExitGroup) Dispose() error {
62 | defer func() {
63 | windows.CloseHandle(g.jobHandle)
64 | for _, proc := range g.procs {
65 | windows.CloseHandle(proc)
66 | }
67 | }()
68 |
69 | if err := windows.TerminateJobObject(g.jobHandle, 1); err != nil {
70 | // Fall-back on error. Kill the main process only.
71 | for _, cmd := range g.cmds {
72 | cmd.Process.Kill()
73 | }
74 | return err
75 | }
76 |
77 | // wait for job to be terminated
78 | status, err := windows.WaitForSingleObject(g.jobHandle, windows.INFINITE)
79 | if status != windows.WAIT_OBJECT_0 {
80 | return err
81 | }
82 |
83 | return nil
84 | }
85 |
--------------------------------------------------------------------------------
/pkg/monitor/gpu/vendor/nvidia_smi.go:
--------------------------------------------------------------------------------
1 | package vendor
2 |
3 | // Modified from https://github.com/influxdata/telegraf/blob/master/plugins/inputs/nvidia_smi/nvidia_smi.go
4 | // Original License: MIT
5 |
6 | import (
7 | "encoding/xml"
8 | "errors"
9 | "os"
10 | "os/exec"
11 | "strconv"
12 | "strings"
13 | )
14 |
15 | type NvidiaSMI struct {
16 | BinPath string
17 | data []byte
18 | }
19 |
20 | func (smi *NvidiaSMI) GatherModel() ([]string, error) {
21 | return smi.gatherModel()
22 | }
23 |
24 | func (smi *NvidiaSMI) GatherUsage() ([]float64, error) {
25 | return smi.gatherUsage()
26 | }
27 |
28 | func (smi *NvidiaSMI) Start() error {
29 | if _, err := os.Stat(smi.BinPath); os.IsNotExist(err) {
30 | binPath, err := exec.LookPath("nvidia-smi")
31 | if err != nil {
32 | return errors.New("didn't find the adequate tool to query GPU utilization")
33 | }
34 | smi.BinPath = binPath
35 | }
36 | smi.data = smi.pollNvidiaSMI()
37 | return nil
38 | }
39 |
40 | func (smi *NvidiaSMI) pollNvidiaSMI() []byte {
41 | cmd := exec.Command(smi.BinPath,
42 | "-q",
43 | "-x",
44 | )
45 | gs, err := cmd.CombinedOutput()
46 | if err != nil {
47 | return nil
48 | }
49 | return gs
50 | }
51 |
52 | func (smi *NvidiaSMI) gatherModel() ([]string, error) {
53 | var s smistat
54 | var models []string
55 |
56 | err := xml.Unmarshal(smi.data, &s)
57 | if err != nil {
58 | return nil, err
59 | }
60 |
61 | for _, gpu := range s.GPUs {
62 | models = append(models, gpu.ProductName)
63 | }
64 |
65 | return models, nil
66 | }
67 |
68 | func (smi *NvidiaSMI) gatherUsage() ([]float64, error) {
69 | var s smistat
70 | var percentage []float64
71 |
72 | err := xml.Unmarshal(smi.data, &s)
73 | if err != nil {
74 | return nil, err
75 | }
76 |
77 | for _, gpu := range s.GPUs {
78 | gp, _ := parsePercentage(gpu.Utilization.GpuUtil)
79 | percentage = append(percentage, gp)
80 | }
81 |
82 | return percentage, nil
83 | }
84 |
85 | func parsePercentage(p string) (float64, error) {
86 | per := strings.ReplaceAll(p, " ", "")
87 |
88 | t := strings.TrimSuffix(per, "%")
89 |
90 | value, err := strconv.ParseFloat(t, 64)
91 | if err != nil {
92 | return 0, err
93 | }
94 |
95 | return value, nil
96 | }
97 |
98 | type gpu struct {
99 | ProductName string `xml:"product_name"`
100 | Utilization struct {
101 | GpuUtil string `xml:"gpu_util"`
102 | } `xml:"utilization"`
103 | }
104 | type smistat struct {
105 | GPUs []gpu `xml:"gpu"`
106 | }
107 |
--------------------------------------------------------------------------------
/pkg/monitor/gpu/vendor/amd_rocm_smi.go:
--------------------------------------------------------------------------------
1 | package vendor
2 |
3 | // Modified from https://github.com/influxdata/telegraf/blob/master/plugins/inputs/amd_rocm_smi/amd_rocm_smi.go
4 | // Original License: MIT
5 |
6 | import (
7 | "errors"
8 | "os"
9 | "os/exec"
10 |
11 | "github.com/tidwall/gjson"
12 | )
13 |
14 | type ROCmSMI struct {
15 | BinPath string
16 | data []byte
17 | }
18 |
19 | func (rsmi *ROCmSMI) GatherModel() ([]string, error) {
20 | return rsmi.gatherModel()
21 | }
22 |
23 | func (rsmi *ROCmSMI) GatherUsage() ([]float64, error) {
24 | return rsmi.gatherUsage()
25 | }
26 |
27 | func (rsmi *ROCmSMI) Start() error {
28 | if _, err := os.Stat(rsmi.BinPath); os.IsNotExist(err) {
29 | binPath, err := exec.LookPath("rocm-smi")
30 | if err != nil {
31 | return errors.New("didn't find the adequate tool to query GPU utilization")
32 | }
33 | rsmi.BinPath = binPath
34 | }
35 |
36 | rsmi.data = rsmi.pollROCmSMI()
37 | return nil
38 | }
39 |
40 | func (rsmi *ROCmSMI) pollROCmSMI() []byte {
41 | cmd := exec.Command(rsmi.BinPath,
42 | "-u",
43 | "--showproductname",
44 | "--json",
45 | )
46 | gs, err := cmd.CombinedOutput()
47 | if err != nil {
48 | return nil
49 | }
50 | return gs
51 | }
52 |
53 | func (rsmi *ROCmSMI) gatherModel() ([]string, error) {
54 | m, err := parseModel(rsmi.data)
55 | if err != nil {
56 | return nil, err
57 | }
58 |
59 | return m, nil
60 | }
61 |
62 | func (rsmi *ROCmSMI) gatherUsage() ([]float64, error) {
63 | u, err := parseUsage(rsmi.data)
64 | if err != nil {
65 | return nil, err
66 | }
67 |
68 | return u, nil
69 | }
70 |
71 | func parseModel(jsonObject []byte) ([]string, error) {
72 | if jsonObject == nil {
73 | return nil, nil
74 | }
75 |
76 | result := gjson.ParseBytes(jsonObject)
77 | if !result.IsObject() {
78 | return nil, errors.New("invalid JSON")
79 | }
80 |
81 | ret := make([]string, 0)
82 | result.ForEach(func(_, value gjson.Result) bool {
83 | ret = append(ret, value.Get("Card series").String())
84 | return true
85 | })
86 |
87 | return ret, nil
88 | }
89 |
90 | func parseUsage(jsonObject []byte) ([]float64, error) {
91 | if jsonObject == nil {
92 | return nil, nil
93 | }
94 |
95 | result := gjson.ParseBytes(jsonObject)
96 | if !result.IsObject() {
97 | return nil, errors.New("invalid JSON")
98 | }
99 |
100 | ret := make([]float64, 0)
101 | result.ForEach(func(_, value gjson.Result) bool {
102 | ret = append(ret, value.Get("GPU use (%)").Float())
103 | return true
104 | })
105 |
106 | return ret, nil
107 | }
108 |
--------------------------------------------------------------------------------
/pkg/monitor/gpu/gpu_linux.go:
--------------------------------------------------------------------------------
1 | //go:build linux
2 |
3 | package gpu
4 |
5 | import (
6 | "context"
7 | "errors"
8 |
9 | "github.com/nezhahq/agent/pkg/monitor/gpu/vendor"
10 | )
11 |
12 | const (
13 | vendorAMD = iota + 1
14 | vendorNVIDIA
15 | )
16 |
17 | var vendorType = getVendor()
18 |
19 | func getVendor() uint8 {
20 | _, err := getNvidiaStat()
21 | if err != nil {
22 | return vendorAMD
23 | } else {
24 | return vendorNVIDIA
25 | }
26 | }
27 |
28 | func getNvidiaStat() ([]float64, error) {
29 | smi := &vendor.NvidiaSMI{
30 | BinPath: "/usr/bin/nvidia-smi",
31 | }
32 | err1 := smi.Start()
33 | if err1 != nil {
34 | return nil, err1
35 | }
36 | data, err2 := smi.GatherUsage()
37 | if err2 != nil {
38 | return nil, err2
39 | }
40 | return data, nil
41 | }
42 |
43 | func getAMDStat() ([]float64, error) {
44 | rsmi := &vendor.ROCmSMI{
45 | BinPath: "/opt/rocm/bin/rocm-smi",
46 | }
47 | err := rsmi.Start()
48 | if err != nil {
49 | return nil, err
50 | }
51 | data, err := rsmi.GatherUsage()
52 | if err != nil {
53 | return nil, err
54 | }
55 | return data, nil
56 | }
57 |
58 | func getNvidiaHost() ([]string, error) {
59 | smi := &vendor.NvidiaSMI{
60 | BinPath: "/usr/bin/nvidia-smi",
61 | }
62 | err := smi.Start()
63 | if err != nil {
64 | return nil, err
65 | }
66 | data, err := smi.GatherModel()
67 | if err != nil {
68 | return nil, err
69 | }
70 | return data, nil
71 | }
72 |
73 | func getAMDHost() ([]string, error) {
74 | rsmi := &vendor.ROCmSMI{
75 | BinPath: "/opt/rocm/bin/rocm-smi",
76 | }
77 | err := rsmi.Start()
78 | if err != nil {
79 | return nil, err
80 | }
81 | data, err := rsmi.GatherModel()
82 | if err != nil {
83 | return nil, err
84 | }
85 | return data, nil
86 | }
87 |
88 | func GetHost(_ context.Context) ([]string, error) {
89 | var gi []string
90 | var err error
91 |
92 | switch vendorType {
93 | case vendorAMD:
94 | gi, err = getAMDHost()
95 | case vendorNVIDIA:
96 | gi, err = getNvidiaHost()
97 | default:
98 | return nil, errors.New("invalid vendor")
99 | }
100 |
101 | if err != nil {
102 | return nil, err
103 | }
104 |
105 | return gi, nil
106 | }
107 |
108 | func GetState(_ context.Context) ([]float64, error) {
109 | var gs []float64
110 | var err error
111 |
112 | switch vendorType {
113 | case vendorAMD:
114 | gs, err = getAMDStat()
115 | case vendorNVIDIA:
116 | gs, err = getNvidiaStat()
117 | default:
118 | return nil, errors.New("invalid vendor")
119 | }
120 |
121 | if err != nil {
122 | return nil, err
123 | }
124 |
125 | return gs, nil
126 | }
127 |
--------------------------------------------------------------------------------
/pkg/util/http.go:
--------------------------------------------------------------------------------
1 | package util
2 |
3 | import (
4 | "context"
5 | "errors"
6 | "net"
7 | "net/http"
8 | "time"
9 | )
10 |
11 | var (
12 | DNSServersV4 = []string{"8.8.8.8:53", "8.8.4.4:53", "1.1.1.1:53", "1.0.0.1:53"}
13 | DNSServersV6 = []string{"[2001:4860:4860::8888]:53", "[2001:4860:4860::8844]:53", "[2606:4700:4700::1111]:53", "[2606:4700:4700::1001]:53"}
14 | DNSServersAll = append(DNSServersV4, DNSServersV6...)
15 | )
16 |
17 | func NewSingleStackHTTPClient(httpTimeout, dialTimeout, keepAliveTimeout time.Duration, ipv6 bool) *http.Client {
18 | dialer := &net.Dialer{
19 | Timeout: dialTimeout,
20 | KeepAlive: keepAliveTimeout,
21 | }
22 |
23 | transport := &http.Transport{
24 | Proxy: http.ProxyFromEnvironment,
25 | ForceAttemptHTTP2: false,
26 | DialContext: func(ctx context.Context, network string, addr string) (net.Conn, error) {
27 | ip, err := resolveIP(addr, ipv6)
28 | if err != nil {
29 | return nil, err
30 | }
31 | return dialer.DialContext(ctx, network, ip)
32 | },
33 | }
34 |
35 | return &http.Client{
36 | Transport: transport,
37 | Timeout: httpTimeout,
38 | }
39 | }
40 |
41 | func resolveIP(addr string, ipv6 bool) (string, error) {
42 | host, port, err := net.SplitHostPort(addr)
43 | if err != nil {
44 | return "", err
45 | }
46 |
47 | dnsServers := DNSServersV6
48 | if !ipv6 {
49 | dnsServers = DNSServersV4
50 | }
51 |
52 | res, err := LookupIP(host)
53 | if err != nil {
54 | for _, server := range dnsServers {
55 | r := &net.Resolver{
56 | PreferGo: true,
57 | Dial: func(ctx context.Context, network, address string) (net.Conn, error) {
58 | d := net.Dialer{
59 | Timeout: time.Second * 10,
60 | }
61 | return d.DialContext(ctx, "udp", server)
62 | },
63 | }
64 | res, err = r.LookupIP(context.Background(), "ip", host)
65 | if err == nil {
66 | break
67 | }
68 | }
69 | }
70 |
71 | if err != nil {
72 | return "", err
73 | }
74 |
75 | var ipv4Resolved, ipv6Resolved bool
76 | var resolved string
77 |
78 | for _, r := range res {
79 | if r.To4() != nil {
80 | if !ipv6 {
81 | ipv4Resolved = true
82 | resolved = r.String()
83 | break
84 | }
85 | } else if ipv6 {
86 | ipv6Resolved = true
87 | resolved = r.String()
88 | break
89 | }
90 | }
91 |
92 | if ipv6 && !ipv6Resolved {
93 | return "", errors.New("the AAAA record not resolved")
94 | }
95 |
96 | if !ipv6 && !ipv4Resolved {
97 | return "", errors.New("the A record not resolved")
98 | }
99 |
100 | return net.JoinHostPort(resolved, port), nil
101 | }
102 |
--------------------------------------------------------------------------------
/pkg/util/util.go:
--------------------------------------------------------------------------------
1 | package util
2 |
3 | import (
4 | "context"
5 | "net"
6 | "net/http"
7 | "os"
8 | "strings"
9 | "sync"
10 |
11 | jsoniter "github.com/json-iterator/go"
12 | )
13 |
14 | const MacOSChromeUA = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
15 |
16 | var (
17 | Json = jsoniter.ConfigCompatibleWithStandardLibrary
18 | )
19 |
20 | func IsWindows() bool {
21 | return os.PathSeparator == '\\' && os.PathListSeparator == ';'
22 | }
23 |
24 | func BrowserHeaders() http.Header {
25 | return http.Header{
26 | "Accept": {"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8"},
27 | "Accept-Language": {"en,zh-CN;q=0.9,zh;q=0.8"},
28 | "User-Agent": {MacOSChromeUA},
29 | }
30 | }
31 |
32 | func ContainsStr(slice []string, str string) bool {
33 | if str != "" {
34 | for _, item := range slice {
35 | if strings.Contains(str, item) {
36 | return true
37 | }
38 | }
39 | }
40 | return false
41 | }
42 |
43 | func RemoveDuplicate[T comparable](sliceList []T) []T {
44 | allKeys := make(map[T]bool)
45 | list := []T{}
46 | for _, item := range sliceList {
47 | if _, value := allKeys[item]; !value {
48 | allKeys[item] = true
49 | list = append(list, item)
50 | }
51 | }
52 | return list
53 | }
54 |
55 | // OnceValue returns a function that invokes f only once and returns the value
56 | // returned by f. The returned function may be called concurrently.
57 | //
58 | // If f panics, the returned function will panic with the same value on every call.
59 | func OnceValue[T any](f func() T) func() T {
60 | var (
61 | once sync.Once
62 | valid bool
63 | p any
64 | result T
65 | )
66 | g := func() {
67 | defer func() {
68 | p = recover()
69 | if !valid {
70 | panic(p)
71 | }
72 | }()
73 | result = f()
74 | f = nil
75 | valid = true
76 | }
77 | return func() T {
78 | once.Do(g)
79 | if !valid {
80 | panic(p)
81 | }
82 | return result
83 | }
84 | }
85 |
86 | func RotateQueue1(start, i, size int) int {
87 | return (start + i) % size
88 | }
89 |
90 | // LookupIP looks up host using the local resolver.
91 | // It returns a slice of that host's IPv4 and IPv6 addresses.
92 | func LookupIP(host string) ([]net.IP, error) {
93 | var defaultResolver = net.Resolver{PreferGo: true}
94 | addrs, err := defaultResolver.LookupIPAddr(context.Background(), host)
95 | if err != nil {
96 | return nil, err
97 | }
98 | ips := make([]net.IP, len(addrs))
99 | for i, ia := range addrs {
100 | ips[i] = ia.IP
101 | }
102 | return ips, nil
103 | }
104 |
--------------------------------------------------------------------------------
/.github/workflows/codeql-analysis.yml:
--------------------------------------------------------------------------------
1 | # For most projects, this workflow file will not need changing; you simply need
2 | # to commit it to your repository.
3 | #
4 | # You may wish to alter this file to override the set of languages analyzed,
5 | # or to provide custom queries or build logic.
6 | #
7 | # ******** NOTE ********
8 | # We have attempted to detect the languages in your repository. Please check
9 | # the `language` matrix defined below to confirm you have the correct set of
10 | # supported CodeQL languages.
11 | #
12 | name: "CodeQL"
13 |
14 | on:
15 | push:
16 | branches: [ main ]
17 | pull_request:
18 | # The branches below must be a subset of the branches above
19 | branches: [ main ]
20 | schedule:
21 | - cron: '15 20 * * 0'
22 |
23 | jobs:
24 | analyze:
25 | name: Analyze
26 | runs-on: ubuntu-latest
27 | permissions:
28 | actions: read
29 | contents: read
30 | security-events: write
31 |
32 | strategy:
33 | fail-fast: false
34 | matrix:
35 | language: [ 'go', 'javascript' ]
36 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ]
37 | # Learn more about CodeQL language support at https://git.io/codeql-language-support
38 |
39 | steps:
40 | - name: Checkout repository
41 | uses: actions/checkout@v4
42 |
43 | - name: Install Go
44 | uses: actions/setup-go@v4
45 | with:
46 | go-version-file: go.mod
47 |
48 | # Initializes the CodeQL tools for scanning.
49 | - name: Initialize CodeQL
50 | uses: github/codeql-action/init@v2
51 | with:
52 | languages: ${{ matrix.language }}
53 | # If you wish to specify custom queries, you can do so here or in a config file.
54 | # By default, queries listed here will override any specified in a config file.
55 | # Prefix the list here with "+" to use these queries and those in the config file.
56 | # queries: ./path/to/local/query, your-org/your-repo/queries@main
57 |
58 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
59 | # If this step fails, then you should remove it and run the build manually (see below)
60 | - name: Autobuild
61 | uses: github/codeql-action/autobuild@v2
62 |
63 | # ℹ️ Command-line programs to run using the OS shell.
64 | # 📚 https://git.io/JvXDl
65 |
66 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
67 | # and modify them (or add more) to build your code if your project
68 | # uses a compiled language
69 |
70 | #- run: |
71 | # make bootstrap
72 | # make release
73 |
74 | - name: Perform CodeQL Analysis
75 | uses: github/codeql-action/analyze@v2
76 |
--------------------------------------------------------------------------------
/model/host.go:
--------------------------------------------------------------------------------
1 | package model
2 |
3 | import (
4 | pb "github.com/nezhahq/agent/proto"
5 | )
6 |
7 | type SensorTemperature struct {
8 | Name string
9 | Temperature float64
10 | }
11 |
12 | type HostState struct {
13 | CPU float64
14 | MemUsed uint64
15 | SwapUsed uint64
16 | DiskUsed uint64
17 | NetInTransfer uint64
18 | NetOutTransfer uint64
19 | NetInSpeed uint64
20 | NetOutSpeed uint64
21 | Uptime uint64
22 | Load1 float64
23 | Load5 float64
24 | Load15 float64
25 | TcpConnCount uint64
26 | UdpConnCount uint64
27 | ProcessCount uint64
28 | Temperatures []SensorTemperature
29 | GPU []float64
30 | }
31 |
32 | func (s *HostState) PB() *pb.State {
33 | var ts []*pb.State_SensorTemperature
34 | for _, t := range s.Temperatures {
35 | ts = append(ts, &pb.State_SensorTemperature{
36 | Name: t.Name,
37 | Temperature: t.Temperature,
38 | })
39 | }
40 |
41 | return &pb.State{
42 | Cpu: s.CPU,
43 | MemUsed: s.MemUsed,
44 | SwapUsed: s.SwapUsed,
45 | DiskUsed: s.DiskUsed,
46 | NetInTransfer: s.NetInTransfer,
47 | NetOutTransfer: s.NetOutTransfer,
48 | NetInSpeed: s.NetInSpeed,
49 | NetOutSpeed: s.NetOutSpeed,
50 | Uptime: s.Uptime,
51 | Load1: s.Load1,
52 | Load5: s.Load5,
53 | Load15: s.Load15,
54 | TcpConnCount: s.TcpConnCount,
55 | UdpConnCount: s.UdpConnCount,
56 | ProcessCount: s.ProcessCount,
57 | Temperatures: ts,
58 | Gpu: s.GPU,
59 | }
60 | }
61 |
62 | type Host struct {
63 | Platform string
64 | PlatformVersion string
65 | CPU []string
66 | MemTotal uint64
67 | DiskTotal uint64
68 | SwapTotal uint64
69 | Arch string
70 | Virtualization string
71 | BootTime uint64
72 | Version string
73 | GPU []string
74 | }
75 |
76 | func (h *Host) PB() *pb.Host {
77 | return &pb.Host{
78 | Platform: h.Platform,
79 | PlatformVersion: h.PlatformVersion,
80 | Cpu: h.CPU,
81 | MemTotal: h.MemTotal,
82 | DiskTotal: h.DiskTotal,
83 | SwapTotal: h.SwapTotal,
84 | Arch: h.Arch,
85 | Virtualization: h.Virtualization,
86 | BootTime: h.BootTime,
87 | Version: h.Version,
88 | Gpu: h.GPU,
89 | }
90 | }
91 |
92 | type GeoIP struct {
93 | IP IP `json:"ip,omitempty"`
94 | CountryCode string `json:"country_code,omitempty"`
95 | }
96 |
97 | type IP struct {
98 | IPv4Addr string `json:"ipv4_addr,omitempty"`
99 | IPv6Addr string `json:"ipv6_addr,omitempty"`
100 | }
101 |
--------------------------------------------------------------------------------
/pkg/monitor/disk/disk.go:
--------------------------------------------------------------------------------
1 | package disk
2 |
3 | import (
4 | "context"
5 | "os/exec"
6 | "runtime"
7 | "strconv"
8 | "strings"
9 |
10 | psDisk "github.com/shirou/gopsutil/v4/disk"
11 |
12 | "github.com/nezhahq/agent/pkg/util"
13 | )
14 |
15 | type DiskKeyType string
16 |
17 | const DiskKey DiskKeyType = "disk"
18 |
19 | var expectDiskFsTypes = []string{
20 | "apfs", "ext4", "ext3", "ext2", "f2fs", "reiserfs", "jfs", "btrfs",
21 | "fuseblk", "zfs", "simfs", "ntfs", "fat32", "exfat", "xfs", "fuse.rclone",
22 | }
23 |
24 | func GetHost(ctx context.Context) (uint64, error) {
25 | devices, err := getDevices(ctx)
26 | if err != nil {
27 | return 0, err
28 | }
29 |
30 | var total uint64
31 | for _, mountPath := range devices {
32 | diskUsageOf, err := psDisk.Usage(mountPath)
33 | if err == nil {
34 | total += diskUsageOf.Total
35 | }
36 | }
37 |
38 | // Fallback 到这个方法,仅统计根路径,适用于OpenVZ之类的.
39 | if runtime.GOOS == "linux" && total == 0 {
40 | cmd := exec.Command("df")
41 | out, err := cmd.CombinedOutput()
42 | if err == nil {
43 | s := strings.Split(string(out), "\n")
44 | for _, c := range s {
45 | info := strings.Fields(c)
46 | if len(info) == 6 {
47 | if info[5] == "/" {
48 | total, _ = strconv.ParseUint(info[1], 0, 64)
49 | // 默认获取的是1K块为单位的.
50 | total = total * 1024
51 | }
52 | }
53 | }
54 | }
55 | }
56 |
57 | return total, nil
58 | }
59 |
60 | func GetState(ctx context.Context) (uint64, error) {
61 | devices, err := getDevices(ctx)
62 | if err != nil {
63 | return 0, err
64 | }
65 |
66 | var used uint64
67 | for _, mountPath := range devices {
68 | diskUsageOf, err := psDisk.Usage(mountPath)
69 | if err == nil {
70 | used += diskUsageOf.Used
71 | }
72 | }
73 |
74 | // Fallback 到这个方法,仅统计根路径,适用于OpenVZ之类的.
75 | if runtime.GOOS == "linux" && used == 0 {
76 | cmd := exec.Command("df")
77 | out, err := cmd.CombinedOutput()
78 | if err == nil {
79 | s := strings.Split(string(out), "\n")
80 | for _, c := range s {
81 | info := strings.Fields(c)
82 | if len(info) == 6 {
83 | if info[5] == "/" {
84 | used, _ = strconv.ParseUint(info[2], 0, 64)
85 | // 默认获取的是1K块为单位的.
86 | used = used * 1024
87 | }
88 | }
89 | }
90 | }
91 | }
92 |
93 | return used, nil
94 | }
95 |
96 | func getDevices(ctx context.Context) (map[string]string, error) {
97 | devices := make(map[string]string)
98 |
99 | // 如果配置了白名单,使用白名单的列表
100 | if s, ok := ctx.Value(DiskKey).([]string); ok && len(s) > 0 {
101 | for i, v := range s {
102 | devices[strconv.Itoa(i)] = v
103 | }
104 | return devices, nil
105 | }
106 |
107 | // 否则使用默认过滤规则
108 | diskList, err := psDisk.Partitions(false)
109 | if err != nil {
110 | return nil, err
111 | }
112 |
113 | for _, d := range diskList {
114 | fsType := strings.ToLower(d.Fstype)
115 | // 不统计 K8s 的虚拟挂载点:https://github.com/shirou/gopsutil/issues/1007
116 | if devices[d.Device] == "" && util.ContainsStr(expectDiskFsTypes, fsType) && !strings.Contains(d.Mountpoint, "/var/lib/kubelet") {
117 | devices[d.Device] = d.Mountpoint
118 | }
119 | }
120 |
121 | return devices, nil
122 | }
123 |
--------------------------------------------------------------------------------
/.github/workflows/agent.yml:
--------------------------------------------------------------------------------
1 | name: Build + Release
2 |
3 | on:
4 | push:
5 | tags:
6 | - "v*"
7 | branches:
8 | - main
9 | paths-ignore:
10 | - ".github/workflows/agent.yml"
11 | - ".github/workflows/codeql-analysis.yml"
12 | - ".github/workflows/test.yml"
13 | - ".github/workflows/contributors.yml"
14 | - "README.md"
15 | - ".goreleaser.yml"
16 | pull_request:
17 | branches:
18 | - main
19 |
20 | jobs:
21 | build:
22 | strategy:
23 | fail-fast: true
24 | matrix:
25 | goos: [linux, windows, darwin, freebsd]
26 | goarch: [amd64, arm64, 386]
27 | include:
28 | - goos: linux
29 | goarch: 386
30 | - goos: linux
31 | goarch: s390x
32 | - goos: linux
33 | goarch: riscv64
34 | - goos: linux
35 | goarch: arm
36 | - goos: linux
37 | goarch: mips
38 | gomips: softfloat
39 | - goos: linux
40 | goarch: mipsle
41 | gomips: softfloat
42 | - goos: freebsd
43 | goarch: arm
44 | exclude:
45 | - goos: darwin
46 | goarch: 386
47 |
48 | name: Build artifacts
49 | runs-on: ubuntu-latest
50 | env:
51 | GOOS: ${{ matrix.goos }}
52 | GOARCH: ${{ matrix.goarch }}
53 | GOMIPS: ${{ matrix.gomips }}
54 | steps:
55 | - uses: actions/checkout@v4
56 | with:
57 | fetch-depth: 0
58 |
59 | - name: Set up Go
60 | uses: actions/setup-go@v5
61 | with:
62 | go-version: "1.20.14"
63 |
64 | - name: Build Test
65 | if: github.event_name != 'push' || !contains(github.ref, 'refs/tags/')
66 | uses: goreleaser/goreleaser-action@v6
67 | with:
68 | distribution: goreleaser
69 | version: '~> v2'
70 | args: build --single-target --clean --snapshot
71 |
72 | - name: Build
73 | if: github.event_name == 'push' && contains(github.ref, 'refs/tags/')
74 | uses: goreleaser/goreleaser-action@v6
75 | with:
76 | distribution: goreleaser
77 | version: '~> v2'
78 | args: build --single-target --clean
79 |
80 | - name: Archive
81 | run: zip -jr dist/nezha-agent_${GOOS}_${GOARCH}.zip dist/*/*
82 |
83 | - name: Upload artifacts
84 | uses: actions/upload-artifact@v4
85 | with:
86 | name: nezha-agent_${{ env.GOOS }}_${{ env.GOARCH }}
87 | path: |
88 | ./dist/nezha-agent_${{ env.GOOS }}_${{ env.GOARCH }}.zip
89 |
90 | release:
91 | runs-on: ubuntu-latest
92 | if: github.event_name == 'push' && contains(github.ref, 'refs/tags/')
93 | needs: build
94 | name: Release
95 | steps:
96 | - name: Download artifacts
97 | uses: actions/download-artifact@v4
98 | with:
99 | path: ./assets
100 |
101 | - name: Checksum
102 | run: sha256sum ./assets/*/*.zip | awk -F" |/" '{print $1, $NF}' > checksums.txt
103 |
104 | - name: Release
105 | uses: ncipollo/release-action@v1
106 | with:
107 | artifacts: "checksums.txt,assets/*/*.zip"
108 | generateReleaseNotes: true
109 |
110 | - name: Trigger sync
111 | env:
112 | GH_REPO: ${{ github.repository }}
113 | GH_TOKEN: ${{ github.token }}
114 | GH_DEBUG: api
115 | run: |
116 | gh workflow run sync-release.yml
117 |
--------------------------------------------------------------------------------
/pkg/monitor/myip.go:
--------------------------------------------------------------------------------
1 | package monitor
2 |
3 | import (
4 | "io"
5 | "net"
6 | "net/http"
7 | "strings"
8 | "sync"
9 | "time"
10 |
11 | "github.com/nezhahq/agent/pkg/logger"
12 | "github.com/nezhahq/agent/pkg/util"
13 | pb "github.com/nezhahq/agent/proto"
14 | )
15 |
16 | var (
17 | cfList = []string{
18 | "https://blog.cloudflare.com/cdn-cgi/trace",
19 | "https://developers.cloudflare.com/cdn-cgi/trace",
20 | "https://hostinger.com/cdn-cgi/trace",
21 | "https://ahrefs.com/cdn-cgi/trace",
22 | }
23 | CustomEndpoints []string
24 | GeoQueryIP, CachedCountryCode string
25 | GeoQueryIPChanged bool = true
26 | httpClientV4 = util.NewSingleStackHTTPClient(time.Second*20, time.Second*5, time.Second*10, false)
27 | httpClientV6 = util.NewSingleStackHTTPClient(time.Second*20, time.Second*5, time.Second*10, true)
28 | )
29 |
30 | // UpdateIP 按设置时间间隔更新IP地址的缓存
31 | func FetchIP(useIPv6CountryCode bool) *pb.GeoIP {
32 | logger.DefaultLogger.Println("正在更新本地缓存IP信息")
33 | wg := new(sync.WaitGroup)
34 | wg.Add(2)
35 | var ipv4, ipv6 string
36 | go func() {
37 | defer wg.Done()
38 | if len(CustomEndpoints) > 0 {
39 | ipv4 = fetchIP(CustomEndpoints, false)
40 | } else {
41 | ipv4 = fetchIP(cfList, false)
42 | }
43 | }()
44 | go func() {
45 | defer wg.Done()
46 | if len(CustomEndpoints) > 0 {
47 | ipv6 = fetchIP(CustomEndpoints, true)
48 | } else {
49 | ipv6 = fetchIP(cfList, true)
50 | }
51 | }()
52 | wg.Wait()
53 |
54 | if ipv6 != "" && (useIPv6CountryCode || ipv4 == "") {
55 | GeoQueryIPChanged = GeoQueryIP != ipv6 || GeoQueryIPChanged
56 | GeoQueryIP = ipv6
57 | } else {
58 | GeoQueryIPChanged = GeoQueryIP != ipv4 || GeoQueryIPChanged
59 | GeoQueryIP = ipv4
60 | }
61 |
62 | if GeoQueryIP != "" {
63 | return &pb.GeoIP{
64 | Use6: useIPv6CountryCode,
65 | Ip: &pb.IP{
66 | Ipv4: ipv4,
67 | Ipv6: ipv6,
68 | },
69 | }
70 | }
71 |
72 | return nil
73 | }
74 |
75 | func fetchIP(servers []string, isV6 bool) string {
76 | var ip string
77 | var resp *http.Response
78 | var err error
79 |
80 | // 双栈支持参差不齐,不能随机请求,有些 IPv6 取不到 IP
81 | for i := 0; i < len(servers); i++ {
82 | if isV6 {
83 | resp, err = httpGetWithUA(httpClientV6, servers[i])
84 | } else {
85 | resp, err = httpGetWithUA(httpClientV4, servers[i])
86 | }
87 | // 遇到单栈机器提前退出
88 | if err != nil && strings.Contains(err.Error(), "no route to host") {
89 | return ip
90 | }
91 | if err == nil {
92 | body, err := io.ReadAll(resp.Body)
93 | if err != nil {
94 | continue
95 | }
96 | resp.Body.Close()
97 |
98 | bodyStr := string(body)
99 | var newIP string
100 |
101 | if !strings.Contains(bodyStr, "ip=") {
102 | newIP = strings.TrimSpace(strings.ReplaceAll(bodyStr, "\n", ""))
103 | } else {
104 | lines := strings.Split(bodyStr, "\n")
105 | for _, line := range lines {
106 | if strings.HasPrefix(line, "ip=") {
107 | newIP = strings.TrimPrefix(line, "ip=")
108 | break
109 | }
110 | }
111 | }
112 | parsedIP := net.ParseIP(newIP)
113 | // 没取到 v6 IP
114 | if isV6 && (parsedIP == nil || parsedIP.To4() != nil) {
115 | continue
116 | }
117 | // 没取到 v4 IP
118 | if !isV6 && (parsedIP == nil || parsedIP.To4() == nil) {
119 | continue
120 | }
121 | ip = newIP
122 | return ip
123 | }
124 | }
125 | return ip
126 | }
127 |
128 | func httpGetWithUA(client *http.Client, url string) (*http.Response, error) {
129 | req, err := http.NewRequest("GET", url, nil)
130 | if err != nil {
131 | return nil, err
132 | }
133 | req.Header.Add("User-Agent", util.MacOSChromeUA)
134 | return client.Do(req)
135 | }
136 |
--------------------------------------------------------------------------------
/cmd/agent/commands/edit.go:
--------------------------------------------------------------------------------
1 | package commands
2 |
3 | import (
4 | "errors"
5 | "fmt"
6 | "net"
7 | "strings"
8 |
9 | "github.com/AlecAivazis/survey/v2"
10 | "github.com/hashicorp/go-uuid"
11 | "github.com/shirou/gopsutil/v4/disk"
12 | psnet "github.com/shirou/gopsutil/v4/net"
13 |
14 | "github.com/nezhahq/agent/model"
15 | )
16 |
17 | // 修改Agent要监控的网卡与硬盘分区
18 | func EditAgentConfig(configPath string, agentConfig *model.AgentConfig) {
19 | agentConfig.Read(configPath)
20 |
21 | nc, err := psnet.IOCounters(true)
22 | if err != nil {
23 | panic(err)
24 | }
25 | var nicAllowlistOptions []string
26 | for _, v := range nc {
27 | nicAllowlistOptions = append(nicAllowlistOptions, v.Name)
28 | }
29 |
30 | var diskAllowlistOptions []string
31 | diskList, err := disk.Partitions(false)
32 | if err != nil {
33 | panic(err)
34 | }
35 | for _, p := range diskList {
36 | diskAllowlistOptions = append(diskAllowlistOptions, fmt.Sprintf("%s\t%s\t%s", p.Mountpoint, p.Fstype, p.Device))
37 | }
38 |
39 | uuid, err := uuid.GenerateUUID()
40 | if err != nil {
41 | panic(err)
42 | }
43 |
44 | var qs = []*survey.Question{
45 | {
46 | Name: "nic",
47 | Prompt: &survey.MultiSelect{
48 | Message: "选择要监控的网卡",
49 | Options: nicAllowlistOptions,
50 | },
51 | },
52 | {
53 | Name: "disk",
54 | Prompt: &survey.MultiSelect{
55 | Message: "选择要监控的硬盘分区",
56 | Options: diskAllowlistOptions,
57 | },
58 | },
59 | {
60 | Name: "dns",
61 | Prompt: &survey.Input{
62 | Message: "自定义 DNS,可输入空格跳过,如 1.1.1.1:53,1.0.0.1:53",
63 | Default: strings.Join(agentConfig.DNS, ","),
64 | },
65 | },
66 | {
67 | Name: "uuid",
68 | Prompt: &survey.Input{
69 | Message: "输入 Agent UUID",
70 | Default: agentConfig.UUID,
71 | Suggest: func(_ string) []string {
72 | return []string{uuid}
73 | },
74 | },
75 | },
76 | {
77 | Name: "gpu",
78 | Prompt: &survey.Confirm{
79 | Message: "是否启用 GPU 监控?",
80 | Default: false,
81 | },
82 | },
83 | {
84 | Name: "temperature",
85 | Prompt: &survey.Confirm{
86 | Message: "是否启用温度监控?",
87 | Default: false,
88 | },
89 | },
90 | {
91 | Name: "debug",
92 | Prompt: &survey.Confirm{
93 | Message: "是否开启调试模式?",
94 | Default: false,
95 | },
96 | },
97 | }
98 |
99 | answers := struct {
100 | Nic []string `mapstructure:"nic_allowlist" json:"nic_allowlist"`
101 | Disk []string `mapstructure:"hard_drive_partition_allowlist" json:"hard_drive_partition_allowlist"`
102 | DNS string `mapstructure:"dns" json:"dns"`
103 | GPU bool `mapstructure:"gpu" json:"gpu"`
104 | Temperature bool `mapstructure:"temperature" json:"temperature"`
105 | Debug bool `mapstructure:"debug" json:"debug"`
106 | UUID string `mapstructure:"uuid" json:"uuid"`
107 | }{}
108 |
109 | err = survey.Ask(qs, &answers, survey.WithValidator(survey.Required))
110 | if err != nil {
111 | fmt.Println("选择错误", err.Error())
112 | return
113 | }
114 |
115 | agentConfig.HardDrivePartitionAllowlist = []string{}
116 | for _, v := range answers.Disk {
117 | agentConfig.HardDrivePartitionAllowlist = append(agentConfig.HardDrivePartitionAllowlist, strings.Split(v, "\t")[0])
118 | }
119 |
120 | agentConfig.NICAllowlist = make(map[string]bool)
121 | for _, v := range answers.Nic {
122 | agentConfig.NICAllowlist[v] = true
123 | }
124 |
125 | dnsServers := strings.TrimSpace(answers.DNS)
126 |
127 | if dnsServers != "" {
128 | agentConfig.DNS = strings.Split(dnsServers, ",")
129 | for _, s := range agentConfig.DNS {
130 | host, _, err := net.SplitHostPort(s)
131 | if err == nil {
132 | if net.ParseIP(host) == nil {
133 | err = errors.New("格式错误")
134 | }
135 | }
136 | if err != nil {
137 | panic(fmt.Sprintf("自定义 DNS 格式错误:%s %v", s, err))
138 | }
139 | }
140 | } else {
141 | agentConfig.DNS = []string{}
142 | }
143 |
144 | agentConfig.GPU = answers.GPU
145 | agentConfig.Temperature = answers.Temperature
146 | agentConfig.Debug = answers.Debug
147 | agentConfig.UUID = answers.UUID
148 |
149 | if err = agentConfig.Save(); err != nil {
150 | panic(err)
151 | }
152 |
153 | fmt.Println("修改自定义配置成功,重启 Agent 后生效")
154 | }
155 |
--------------------------------------------------------------------------------
/pkg/pty/pty_windows.go:
--------------------------------------------------------------------------------
1 | //go:build windows && !arm64
2 |
3 | package pty
4 |
5 | import (
6 | "fmt"
7 | "io"
8 | "net/http"
9 | "os"
10 | "os/exec"
11 | "path/filepath"
12 | "regexp"
13 | "runtime"
14 | "strconv"
15 |
16 | "github.com/UserExistsError/conpty"
17 | "github.com/artdarek/go-unzip"
18 | "github.com/iamacarpet/go-winpty"
19 | "github.com/shirou/gopsutil/v4/host"
20 | )
21 |
22 | var _ IPty = (*winPTY)(nil)
23 | var _ IPty = (*conPty)(nil)
24 |
25 | var isWin10 = VersionCheck()
26 |
27 | type winPTY struct {
28 | tty *winpty.WinPTY
29 | }
30 |
31 | type conPty struct {
32 | tty *conpty.ConPty
33 | }
34 |
35 | func VersionCheck() bool {
36 | hi, err := host.Info()
37 | if err != nil {
38 | return false
39 | }
40 |
41 | re := regexp.MustCompile(`Build (\d+(\.\d+)?)`)
42 | match := re.FindStringSubmatch(hi.KernelVersion)
43 | if len(match) > 1 {
44 | versionStr := match[1]
45 |
46 | version, err := strconv.ParseFloat(versionStr, 64)
47 | if err != nil {
48 | return false
49 | }
50 |
51 | return version >= 17763
52 | }
53 | return false
54 | }
55 |
56 | func DownloadDependency() error {
57 | if !isWin10 {
58 | executablePath, err := getExecutableFilePath()
59 | if err != nil {
60 | return fmt.Errorf("winpty 获取文件路径失败: %v", err)
61 | }
62 |
63 | winptyAgentExe := filepath.Join(executablePath, "winpty-agent.exe")
64 | winptyAgentDll := filepath.Join(executablePath, "winpty.dll")
65 |
66 | fe, errFe := os.Stat(winptyAgentExe)
67 | fd, errFd := os.Stat(winptyAgentDll)
68 | if errFe == nil && fe.Size() > 300000 && errFd == nil && fd.Size() > 300000 {
69 | return fmt.Errorf("winpty 文件完整性检查失败")
70 | }
71 |
72 | resp, err := http.Get("https://github.com/rprichard/winpty/releases/download/0.4.3/winpty-0.4.3-msvc2015.zip")
73 | if err != nil {
74 | return fmt.Errorf("winpty 下载失败: %v", err)
75 | }
76 | defer resp.Body.Close()
77 | content, err := io.ReadAll(resp.Body)
78 | if err != nil {
79 | return fmt.Errorf("winpty 下载失败: %v", err)
80 | }
81 | if err := os.WriteFile("./wintty.zip", content, os.FileMode(0777)); err != nil {
82 | return fmt.Errorf("winpty 写入失败: %v", err)
83 | }
84 | if err := unzip.New("./wintty.zip", "./wintty").Extract(); err != nil {
85 | return fmt.Errorf("winpty 解压失败: %v", err)
86 | }
87 | arch := "x64"
88 | if runtime.GOARCH != "amd64" {
89 | arch = "ia32"
90 | }
91 |
92 | os.Rename("./wintty/"+arch+"/bin/winpty-agent.exe", winptyAgentExe)
93 | os.Rename("./wintty/"+arch+"/bin/winpty.dll", winptyAgentDll)
94 | os.RemoveAll("./wintty")
95 | os.RemoveAll("./wintty.zip")
96 | }
97 | return nil
98 | }
99 |
100 | func getExecutableFilePath() (string, error) {
101 | ex, err := os.Executable()
102 | if err != nil {
103 | return "", err
104 | }
105 | return filepath.Dir(ex), nil
106 | }
107 |
108 | func Start() (IPty, error) {
109 | shellPath, err := exec.LookPath("powershell.exe")
110 | if err != nil || shellPath == "" {
111 | shellPath = "cmd.exe"
112 | }
113 | path, err := getExecutableFilePath()
114 | if err != nil {
115 | return nil, err
116 | }
117 | if !isWin10 {
118 | tty, err := winpty.OpenDefault(path, shellPath)
119 | return &winPTY{tty: tty}, err
120 | }
121 | tty, err := conpty.Start(shellPath, conpty.ConPtyWorkDir(path))
122 | return &conPty{tty: tty}, err
123 | }
124 |
125 | func (w *winPTY) Write(p []byte) (n int, err error) {
126 | return w.tty.StdIn.Write(p)
127 | }
128 |
129 | func (w *winPTY) Read(p []byte) (n int, err error) {
130 | return w.tty.StdOut.Read(p)
131 | }
132 |
133 | func (w *winPTY) Getsize() (uint16, uint16, error) {
134 | return 80, 40, nil
135 | }
136 |
137 | func (w *winPTY) Setsize(cols, rows uint32) error {
138 | w.tty.SetSize(cols, rows)
139 | return nil
140 | }
141 |
142 | func (w *winPTY) Close() error {
143 | w.tty.Close()
144 | return nil
145 | }
146 |
147 | func (c *conPty) Write(p []byte) (n int, err error) {
148 | return c.tty.Write(p)
149 | }
150 |
151 | func (c *conPty) Read(p []byte) (n int, err error) {
152 | return c.tty.Read(p)
153 | }
154 |
155 | func (c *conPty) Getsize() (uint16, uint16, error) {
156 | return 80, 40, nil
157 | }
158 |
159 | func (c *conPty) Setsize(cols, rows uint32) error {
160 | c.tty.Resize(int(cols), int(rows))
161 | return nil
162 | }
163 |
164 | func (c *conPty) Close() error {
165 | if err := c.tty.Close(); err != nil {
166 | return err
167 | }
168 | return nil
169 | }
170 |
--------------------------------------------------------------------------------
/pkg/fm/tasks.go:
--------------------------------------------------------------------------------
1 | package fm
2 |
3 | import (
4 | "bytes"
5 | "encoding/binary"
6 | "errors"
7 | "io"
8 | "io/fs"
9 | "os"
10 | "os/user"
11 | "path/filepath"
12 |
13 | pb "github.com/nezhahq/agent/proto"
14 | )
15 |
16 | type Task struct {
17 | taskClient pb.NezhaService_IOStreamClient
18 | printf func(string, ...interface{})
19 | remoteData *pb.IOStreamData
20 | }
21 |
22 | func NewFMClient(client pb.NezhaService_IOStreamClient, printFunc func(string, ...interface{})) *Task {
23 | return &Task{
24 | taskClient: client,
25 | printf: printFunc,
26 | }
27 | }
28 |
29 | func (t *Task) DoTask(data *pb.IOStreamData) {
30 | t.remoteData = data
31 |
32 | switch t.remoteData.Data[0] {
33 | case 0:
34 | t.listDir()
35 | case 1:
36 | go t.download()
37 | case 2:
38 | t.upload()
39 | }
40 | }
41 |
42 | func (t *Task) listDir() {
43 | dir := string(t.remoteData.Data[1:])
44 | var entries []fs.DirEntry
45 | var err error
46 | for {
47 | entries, err = os.ReadDir(dir)
48 | if err != nil {
49 | usr, err := user.Current()
50 | if err != nil {
51 | t.taskClient.Send(&pb.IOStreamData{Data: CreateErr(err)})
52 | return
53 | }
54 | dir = usr.HomeDir + string(filepath.Separator)
55 | continue
56 | }
57 | break
58 | }
59 | var buffer bytes.Buffer
60 | td := Create(&buffer, dir)
61 | for _, e := range entries {
62 | newBin := AppendFileName(td, e.Name(), e.IsDir())
63 | td = newBin
64 | }
65 | t.taskClient.Send(&pb.IOStreamData{Data: td})
66 | }
67 |
68 | func (t *Task) download() {
69 | path := string(t.remoteData.Data[1:])
70 | file, err := os.Open(path)
71 | if err != nil {
72 | t.printf("Error opening file: %s", err)
73 | t.taskClient.Send(&pb.IOStreamData{Data: CreateErr(err)})
74 | return
75 | }
76 | defer file.Close()
77 |
78 | fileInfo, err := file.Stat()
79 | if err != nil {
80 | t.printf("Error getting file info: %s", err)
81 | t.taskClient.Send(&pb.IOStreamData{Data: CreateErr(err)})
82 | return
83 | }
84 |
85 | fileSize := fileInfo.Size()
86 | if fileSize <= 0 {
87 | t.taskClient.Send(&pb.IOStreamData{Data: CreateErr(errors.New("requested file is empty"))})
88 | return
89 | }
90 |
91 | // Send header (12 bytes)
92 | var header bytes.Buffer
93 | headerData := CreateFile(&header, uint64(fileSize))
94 | if err := t.taskClient.Send(&pb.IOStreamData{Data: headerData}); err != nil {
95 | t.printf("Error sending file header: %s", err)
96 | t.taskClient.Send(&pb.IOStreamData{Data: CreateErr(err)})
97 | return
98 | }
99 |
100 | buffer := make([]byte, 1048576)
101 | for {
102 | n, err := file.Read(buffer)
103 | if err != nil {
104 | if err == io.EOF {
105 | return
106 | }
107 | t.printf("Error reading file: %s", err)
108 | t.taskClient.Send(&pb.IOStreamData{Data: CreateErr(err)})
109 | return
110 | }
111 |
112 | if err := t.taskClient.Send(&pb.IOStreamData{Data: buffer[:n]}); err != nil {
113 | t.printf("Error sending file chunk: %s", err)
114 | t.taskClient.Send(&pb.IOStreamData{Data: CreateErr(err)})
115 | return
116 | }
117 | }
118 | }
119 |
120 | func (t *Task) upload() {
121 | if len(t.remoteData.Data) < 9 {
122 | const err string = "data is invalid"
123 | t.printf(err)
124 | t.taskClient.Send(&pb.IOStreamData{Data: CreateErr(errors.New(err))})
125 | return
126 | }
127 |
128 | fileSize := binary.BigEndian.Uint64(t.remoteData.Data[1:9])
129 | path := string(t.remoteData.Data[9:])
130 |
131 | file, err := os.Create(path)
132 | if err != nil {
133 | t.printf("Error creating file: %s", err)
134 | t.taskClient.Send(&pb.IOStreamData{Data: CreateErr(err)})
135 | return
136 | }
137 | defer file.Close()
138 |
139 | totalReceived := uint64(0)
140 |
141 | t.printf("receiving file: %s, size: %d", file.Name(), fileSize)
142 | for totalReceived < fileSize {
143 | if t.remoteData, err = t.taskClient.Recv(); err != nil {
144 | t.printf("Error receiving data: %s", err)
145 | t.taskClient.Send(&pb.IOStreamData{Data: CreateErr(err)})
146 | return
147 | }
148 |
149 | bytesWritten, err := file.Write(t.remoteData.Data)
150 | if err != nil {
151 | t.printf("Error writing to file: %s", err)
152 | t.taskClient.Send(&pb.IOStreamData{Data: CreateErr(err)})
153 | return
154 | }
155 |
156 | totalReceived += uint64(bytesWritten)
157 | }
158 | t.printf("received file %s.", file.Name())
159 | t.taskClient.Send(&pb.IOStreamData{Data: completeIdentifier}) // NZUP
160 | }
161 |
--------------------------------------------------------------------------------
/sync.py:
--------------------------------------------------------------------------------
1 | import requests
2 | import os
3 |
4 | # 环境变量
5 | GITHUB_TOKEN = os.getenv('GITHUB_TOKEN')
6 | SOURCE_REPO = 'nezhahq/agent'
7 | TARGET_REPO = 'amclubs/am-nezha-agent'
8 |
9 | # Headers for GitHub API
10 | headers = {'Authorization': f'token {GITHUB_TOKEN}'}
11 |
12 | def log(message):
13 | print(message)
14 |
15 | def check_response(response):
16 | if response.status_code not in [200, 201]:
17 | raise Exception(f"Request failed: {response.status_code} - {response.text}")
18 |
19 | try:
20 | # 获取目标仓库的所有发布
21 | target_releases_url = f'https://api.github.com/repos/{TARGET_REPO}/releases'
22 | response = requests.get(target_releases_url, headers=headers)
23 | check_response(response)
24 | target_releases = response.json()
25 | target_release_tags = {release['tag_name'] for release in target_releases}
26 |
27 | # 获取源仓库的所有发布
28 | source_releases_url = f'https://api.github.com/repos/{SOURCE_REPO}/releases'
29 | response = requests.get(source_releases_url, headers=headers)
30 | check_response(response)
31 | source_releases = response.json()
32 |
33 | for release in source_releases:
34 | tag_name = release['tag_name']
35 |
36 | # 检查是否已经存在相同的release
37 | if tag_name in target_release_tags:
38 | log(f"Release with tag {tag_name} already exists in target repository. Skipping.")
39 | continue
40 |
41 | release_name = release['name']
42 | release_body = release['body']
43 | draft = release['draft']
44 | prerelease = release['prerelease']
45 |
46 | # 创建一个新的 release 在目标仓库
47 | create_release_url = f'https://api.github.com/repos/{TARGET_REPO}/releases'
48 | release_data = {
49 | 'tag_name': tag_name,
50 | 'name': release_name,
51 | 'body': release_body,
52 | 'draft': draft,
53 | 'prerelease': prerelease
54 | }
55 | response = requests.post(create_release_url, json=release_data, headers=headers)
56 | check_response(response)
57 | new_release = response.json()
58 |
59 | # 上传资产到新的 release
60 | for asset in release['assets']:
61 | asset_url = asset['browser_download_url']
62 | asset_name = asset['name']
63 |
64 | # 下载资产
65 | asset_response = requests.get(asset_url, stream=True)
66 | check_response(asset_response)
67 | asset_path = os.path.join('/tmp', asset_name)
68 | with open(asset_path, 'wb') as f:
69 | for chunk in asset_response.iter_content(chunk_size=1024):
70 | if chunk:
71 | f.write(chunk)
72 |
73 | # 上传资产到新的 release
74 | upload_url = new_release['upload_url'].replace('{?name,label}', f'?name={asset_name}')
75 | with open(asset_path, 'rb') as f:
76 | headers.update({'Content-Type': 'application/octet-stream'})
77 | upload_response = requests.post(upload_url, headers=headers, data=f)
78 | check_response(upload_response)
79 |
80 | # 删除下载的文件
81 | os.remove(asset_path)
82 | log(f"Uploaded asset {asset_name} to release {release_name}")
83 |
84 | # 获取目标仓库的所有标签
85 | target_tags_url = f'https://api.github.com/repos/{TARGET_REPO}/tags'
86 | response = requests.get(target_tags_url, headers=headers)
87 | check_response(response)
88 | target_tags = response.json()
89 | target_tag_names = {tag['name'] for tag in target_tags}
90 |
91 | # 获取源仓库的所有标签
92 | source_tags_url = f'https://api.github.com/repos/{SOURCE_REPO}/tags'
93 | response = requests.get(source_tags_url, headers=headers)
94 | check_response(response)
95 | source_tags = response.json()
96 |
97 | for tag in source_tags:
98 | tag_name = tag['name']
99 |
100 | # 检查是否已经存在相同的tag
101 | if tag_name in target_tag_names:
102 | log(f"Tag {tag_name} already exists in target repository. Skipping.")
103 | continue
104 |
105 | tag_sha = tag['commit']['sha']
106 |
107 | # 创建轻量级标签
108 | ref_url = f'https://api.github.com/repos/{TARGET_REPO}/git/refs'
109 | ref_data = {
110 | 'ref': f'refs/tags/{tag_name}',
111 | 'sha': tag_sha
112 | }
113 | response = requests.post(ref_url, json=ref_data, headers=headers)
114 | check_response(response)
115 | log(f"Created tag {tag_name}")
116 |
117 | except Exception as e:
118 | log(f"Error: {e}")
119 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/nezhahq/agent
2 |
3 | go 1.20
4 |
5 | require (
6 | github.com/AlecAivazis/survey/v2 v2.3.7
7 | github.com/UserExistsError/conpty v0.1.4
8 | github.com/artdarek/go-unzip v1.0.0
9 | github.com/blang/semver v3.5.1+incompatible
10 | github.com/cloudflare/ahocorasick v0.0.0-20240916140611-054963ec9396
11 | github.com/creack/pty v1.1.24
12 | github.com/dean2021/goss v0.0.0-20230129073947-df90431348f1
13 | github.com/ebi-yade/altsvc-go v0.1.1
14 | github.com/ebitengine/purego v0.8.1
15 | github.com/hashicorp/go-uuid v1.0.3
16 | github.com/iamacarpet/go-winpty v1.0.4
17 | github.com/jaypipes/ghw v0.12.0
18 | github.com/json-iterator/go v1.1.12
19 | github.com/knadh/koanf/providers/env v1.0.0
20 | github.com/knadh/koanf/providers/file v1.1.2
21 | github.com/knadh/koanf/v2 v2.1.2
22 | github.com/nezhahq/go-github-selfupdate v0.0.0-20241205090552-0b56e412e750
23 | github.com/nezhahq/service v0.0.0-20241205090409-40f63a48da4e
24 | github.com/prometheus-community/pro-bing v0.4.1
25 | github.com/quic-go/quic-go v0.40.1
26 | github.com/refraction-networking/utls v1.6.3
27 | github.com/shirou/gopsutil/v4 v4.24.11
28 | github.com/tidwall/gjson v1.18.0
29 | github.com/urfave/cli/v2 v2.27.5
30 | golang.org/x/net v0.32.0
31 | golang.org/x/sys v0.28.0
32 | google.golang.org/grpc v1.64.1
33 | google.golang.org/protobuf v1.34.2
34 | sigs.k8s.io/yaml v1.4.0
35 | )
36 |
37 | require (
38 | gitee.com/naibahq/go-gitee v0.0.0-20240713052758-bc992e4c5b2c // indirect
39 | github.com/StackExchange/wmi v1.2.1 // indirect
40 | github.com/andybalholm/brotli v1.0.6 // indirect
41 | github.com/antihax/optional v1.0.0 // indirect
42 | github.com/cloudflare/circl v1.3.7 // indirect
43 | github.com/cpuguy83/go-md2man/v2 v2.0.5 // indirect
44 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
45 | github.com/fsnotify/fsnotify v1.7.0 // indirect
46 | github.com/ghodss/yaml v1.0.0 // indirect
47 | github.com/go-logr/logr v1.4.1 // indirect
48 | github.com/go-ole/go-ole v1.2.6 // indirect
49 | github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect
50 | github.com/go-viper/mapstructure/v2 v2.2.1 // indirect
51 | github.com/google/go-github v17.0.0+incompatible // indirect
52 | github.com/google/go-querystring v1.1.0 // indirect
53 | github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 // indirect
54 | github.com/google/uuid v1.6.0 // indirect
55 | github.com/inconshreveable/go-update v0.0.0-20160112193335-8152e7eb6ccf // indirect
56 | github.com/jaypipes/pcidb v1.0.0 // indirect
57 | github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
58 | github.com/klauspost/compress v1.17.4 // indirect
59 | github.com/knadh/koanf/maps v0.1.1 // indirect
60 | github.com/kr/pretty v0.3.1 // indirect
61 | github.com/lufia/plan9stats v0.0.0-20230110061619-bbe2e5e100de // indirect
62 | github.com/mattn/go-colorable v0.1.13 // indirect
63 | github.com/mattn/go-isatty v0.0.17 // indirect
64 | github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect
65 | github.com/mitchellh/copystructure v1.2.0 // indirect
66 | github.com/mitchellh/go-homedir v1.1.0 // indirect
67 | github.com/mitchellh/reflectwalk v1.0.2 // indirect
68 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
69 | github.com/modern-go/reflect2 v1.0.2 // indirect
70 | github.com/onsi/ginkgo/v2 v2.9.5 // indirect
71 | github.com/pkg/errors v0.9.1 // indirect
72 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
73 | github.com/power-devops/perfstat v0.0.0-20221212215047-62379fc7944b // indirect
74 | github.com/quic-go/qpack v0.4.0 // indirect
75 | github.com/quic-go/qtls-go1-20 v0.4.1 // indirect
76 | github.com/russross/blackfriday/v2 v2.1.0 // indirect
77 | github.com/tcnksm/go-gitconfig v0.1.2 // indirect
78 | github.com/tidwall/match v1.1.1 // indirect
79 | github.com/tidwall/pretty v1.2.0 // indirect
80 | github.com/tklauser/go-sysconf v0.3.12 // indirect
81 | github.com/tklauser/numcpus v0.6.1 // indirect
82 | github.com/ulikunitz/xz v0.5.11 // indirect
83 | github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect
84 | github.com/yusufpapurcu/wmi v1.2.4 // indirect
85 | go.uber.org/mock v0.4.0 // indirect
86 | golang.org/x/crypto v0.30.0 // indirect
87 | golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect
88 | golang.org/x/mod v0.17.0 // indirect
89 | golang.org/x/oauth2 v0.20.0 // indirect
90 | golang.org/x/sync v0.10.0 // indirect
91 | golang.org/x/term v0.27.0 // indirect
92 | golang.org/x/text v0.21.0 // indirect
93 | golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect
94 | google.golang.org/genproto/googleapis/rpc v0.0.0-20240528184218-531527333157 // indirect
95 | gopkg.in/yaml.v2 v2.4.0 // indirect
96 | howett.net/plist v1.0.0 // indirect
97 | )
98 |
--------------------------------------------------------------------------------
/pkg/monitor/gpu/gpu_windows.go:
--------------------------------------------------------------------------------
1 | //go:build windows
2 |
3 | package gpu
4 |
5 | import (
6 | "context"
7 | "errors"
8 | "fmt"
9 | "time"
10 | "unsafe"
11 |
12 | "github.com/jaypipes/ghw"
13 | "golang.org/x/sys/windows"
14 | )
15 |
16 | const (
17 | ERROR_SUCCESS = 0
18 | PDH_FMT_DOUBLE = 0x00000200
19 | PDH_MORE_DATA = 0x800007d2
20 | PDH_VAILD_DATA = 0x00000000
21 | PDH_NEW_DATA = 0x00000001
22 | PDH_NO_DATA = 0x800007d5
23 | )
24 |
25 | var (
26 | modPdh = windows.NewLazySystemDLL("pdh.dll")
27 |
28 | pdhOpenQuery = modPdh.NewProc("PdhOpenQuery")
29 | pdhCollectQueryData = modPdh.NewProc("PdhCollectQueryData")
30 | pdhGetFormattedCounterArrayW = modPdh.NewProc("PdhGetFormattedCounterArrayW")
31 | pdhAddEnglishCounterW = modPdh.NewProc("PdhAddEnglishCounterW")
32 | pdhCloseQuery = modPdh.NewProc("PdhCloseQuery")
33 | )
34 |
35 | type PDH_FMT_COUNTERVALUE_DOUBLE struct {
36 | CStatus uint32
37 | DoubleValue float64
38 | }
39 |
40 | type PDH_FMT_COUNTERVALUE_ITEM_DOUBLE struct {
41 | SzName *uint16
42 | FmtValue PDH_FMT_COUNTERVALUE_DOUBLE
43 | }
44 |
45 | func GetHost(_ context.Context) ([]string, error) {
46 | var gpuModel []string
47 | gi, err := ghw.GPU(ghw.WithDisableWarnings())
48 | if err != nil {
49 | return nil, err
50 | }
51 |
52 | for _, card := range gi.GraphicsCards {
53 | if card.DeviceInfo == nil {
54 | return nil, errors.New("Cannot find device info")
55 | }
56 | gpuModel = append(gpuModel, card.DeviceInfo.Product.Name)
57 | }
58 |
59 | return gpuModel, nil
60 | }
61 |
62 | func GetState(_ context.Context) ([]float64, error) {
63 | counter, err := newWin32PerformanceCounter("gpu_utilization", "\\GPU Engine(*engtype_3D)\\Utilization Percentage")
64 | if err != nil {
65 | return nil, err
66 | }
67 | defer pdhCloseQuery.Call(uintptr(counter.Query))
68 |
69 | values, err := getValue(8192, counter)
70 | if err != nil {
71 | return nil, err
72 | }
73 | tot := sumArray(values)
74 | if tot > 100 {
75 | tot = 100
76 | }
77 | return []float64{tot}, nil
78 | }
79 |
80 | // https://github.com/influxdata/telegraf/blob/master/plugins/inputs/win_perf_counters/performance_query.go
81 | func getCounterArrayValue(initialBufSize uint32, counter *win32PerformanceCounter) ([]float64, error) {
82 | for buflen := initialBufSize; buflen <= 100*1024*1024; buflen *= 2 {
83 | time.Sleep(10 * time.Millisecond) // GPU 查询必须设置间隔,否则数据不准
84 | s, _, err := pdhCollectQueryData.Call(uintptr(counter.Query))
85 | if s != 0 && err != nil {
86 | if s == PDH_NO_DATA {
87 | return nil, fmt.Errorf("%w: this counter has not data", err)
88 | }
89 | return nil, err
90 | }
91 | buf := make([]byte, buflen)
92 | size := buflen
93 | var itemCount uint32
94 | r, _, _ := pdhGetFormattedCounterArrayW.Call(uintptr(counter.Counter), PDH_FMT_DOUBLE, uintptr(unsafe.Pointer(&size)), uintptr(unsafe.Pointer(&itemCount)), uintptr(unsafe.Pointer(&buf[0])))
95 | if r == ERROR_SUCCESS {
96 | items := (*[1 << 20]PDH_FMT_COUNTERVALUE_ITEM_DOUBLE)(unsafe.Pointer(&buf[0]))[:itemCount:itemCount]
97 | values := make([]float64, 0, itemCount)
98 | for _, item := range items {
99 | if item.FmtValue.CStatus == PDH_VAILD_DATA || item.FmtValue.CStatus == PDH_NEW_DATA {
100 | val := item.FmtValue.DoubleValue
101 | values = append(values, val)
102 | }
103 | }
104 | return values, nil
105 | }
106 | if r != PDH_MORE_DATA {
107 | return nil, fmt.Errorf("pdhGetFormattedCounterArrayW failed with status 0x%X", r)
108 | }
109 | }
110 |
111 | return nil, errors.New("buffer limit reached")
112 | }
113 |
114 | func createQuery() (windows.Handle, error) {
115 | var query windows.Handle
116 | r, _, err := pdhOpenQuery.Call(0, 0, uintptr(unsafe.Pointer(&query)))
117 | if r != ERROR_SUCCESS {
118 | return 0, fmt.Errorf("pdhOpenQuery failed with status 0x%X: %v", r, err)
119 | }
120 | return query, nil
121 | }
122 |
123 | type win32PerformanceCounter struct {
124 | PostName string
125 | CounterName string
126 | Query windows.Handle
127 | Counter windows.Handle
128 | }
129 |
130 | func newWin32PerformanceCounter(postName, counterName string) (*win32PerformanceCounter, error) {
131 | query, err := createQuery()
132 | if err != nil {
133 | return nil, err
134 | }
135 | counter := win32PerformanceCounter{
136 | Query: query,
137 | PostName: postName,
138 | CounterName: counterName,
139 | }
140 | r, _, err := pdhAddEnglishCounterW.Call(
141 | uintptr(counter.Query),
142 | uintptr(unsafe.Pointer(windows.StringToUTF16Ptr(counter.CounterName))),
143 | 0,
144 | uintptr(unsafe.Pointer(&counter.Counter)),
145 | )
146 | if r != ERROR_SUCCESS {
147 | return nil, fmt.Errorf("pdhAddEnglishCounterW failed with status 0x%X: %v", r, err)
148 | }
149 | return &counter, nil
150 | }
151 |
152 | func getValue(initialBufSize uint32, counter *win32PerformanceCounter) ([]float64, error) {
153 | s, _, err := pdhCollectQueryData.Call(uintptr(counter.Query))
154 | if s != 0 && err != nil {
155 | if s == PDH_NO_DATA {
156 | return nil, fmt.Errorf("%w: this counter has not data", err)
157 | }
158 | return nil, err
159 | }
160 |
161 | return getCounterArrayValue(initialBufSize, counter)
162 | }
163 |
164 | func sumArray(arr []float64) float64 {
165 | var sum float64
166 | for _, value := range arr {
167 | sum += value
168 | }
169 | return sum
170 | }
171 |
--------------------------------------------------------------------------------
/model/config.go:
--------------------------------------------------------------------------------
1 | package model
2 |
3 | import (
4 | "errors"
5 | "fmt"
6 | "os"
7 | "path/filepath"
8 | "strings"
9 |
10 | "github.com/hashicorp/go-uuid"
11 | "github.com/knadh/koanf/providers/env"
12 | "github.com/knadh/koanf/providers/file"
13 | "github.com/knadh/koanf/v2"
14 | "sigs.k8s.io/yaml"
15 |
16 | "github.com/nezhahq/agent/pkg/util"
17 | )
18 |
19 | type AgentConfig struct {
20 | Debug bool `koanf:"debug" json:"debug"`
21 |
22 | Server string `koanf:"server" json:"server"` // 服务器地址
23 | ClientSecret string `koanf:"client_secret" json:"client_secret"` // 客户端密钥
24 | UUID string `koanf:"uuid" json:"uuid"`
25 |
26 | HardDrivePartitionAllowlist []string `koanf:"hard_drive_partition_allowlist" json:"hard_drive_partition_allowlist,omitempty"`
27 | NICAllowlist map[string]bool `koanf:"nic_allowlist" json:"nic_allowlist,omitempty"`
28 | DNS []string `koanf:"dns" json:"dns,omitempty"`
29 | GPU bool `koanf:"gpu" json:"gpu"` // 是否检查GPU
30 | Temperature bool `koanf:"temperature" json:"temperature"` // 是否检查温度
31 | SkipConnectionCount bool `koanf:"skip_connection_count" json:"skip_connection_count"` // 跳过连接数检查
32 | SkipProcsCount bool `koanf:"skip_procs_count" json:"skip_procs_count"` // 跳过进程数量检查
33 | DisableAutoUpdate bool `koanf:"disable_auto_update" json:"disable_auto_update"` // 关闭自动更新
34 | DisableForceUpdate bool `koanf:"disable_force_update" json:"disable_force_update"` // 关闭强制更新
35 | DisableCommandExecute bool `koanf:"disable_command_execute" json:"disable_command_execute"` // 关闭命令执行
36 | ReportDelay int `koanf:"report_delay" json:"report_delay"` // 报告间隔
37 | TLS bool `koanf:"tls" json:"tls"` // 是否使用TLS加密传输至服务端
38 | InsecureTLS bool `koanf:"insecure_tls" json:"insecure_tls"` // 是否禁用证书检查
39 | UseIPv6CountryCode bool `koanf:"use_ipv6_country_code" json:"use_ipv6_country_code"` // 默认优先展示IPv6旗帜
40 | UseGiteeToUpgrade bool `koanf:"use_gitee_to_upgrade" json:"use_gitee_to_upgrade"` // 强制从Gitee获取更新
41 | DisableNat bool `koanf:"disable_nat" json:"disable_nat"` // 关闭内网穿透
42 | DisableSendQuery bool `koanf:"disable_send_query" json:"disable_send_query"` // 关闭发送TCP/ICMP/HTTP请求
43 | IPReportPeriod uint32 `koanf:"ip_report_period" json:"ip_report_period"` // IP上报周期
44 | SelfUpdatePeriod uint32 `koanf:"self_update_period" json:"self_update_period"` // 自动更新周期
45 | CustomIPApi []string `koanf:"custom_ip_api" json:"custom_ip_api,omitempty"` // 自定义 IP API
46 |
47 | k *koanf.Koanf `json:"-"`
48 | filePath string `json:"-"`
49 | }
50 |
51 | // Read 从给定的文件目录加载配置文件
52 | func (c *AgentConfig) Read(path string) error {
53 | c.k = koanf.New("")
54 | c.filePath = path
55 | saveOnce := util.OnceValue(c.Save)
56 |
57 | if _, err := os.Stat(path); err == nil {
58 | err = c.k.Load(file.Provider(path), new(kubeyaml))
59 | if err != nil {
60 | return err
61 | }
62 | } else {
63 | defer saveOnce()
64 | }
65 |
66 | err := c.k.Load(env.Provider("NZ_", "", func(s string) string {
67 | return strings.ToLower(strings.TrimPrefix(s, "NZ_"))
68 | }), nil)
69 | if err != nil {
70 | return err
71 | }
72 |
73 | err = c.k.Unmarshal("", c)
74 | if err != nil {
75 | return err
76 | }
77 |
78 | if c.ReportDelay == 0 {
79 | c.ReportDelay = 1
80 | }
81 |
82 | if c.IPReportPeriod == 0 {
83 | c.IPReportPeriod = 1800
84 | } else if c.IPReportPeriod < 30 {
85 | c.IPReportPeriod = 30
86 | }
87 |
88 | if c.Server == "" {
89 | return errors.New("server address should not be empty")
90 | }
91 |
92 | if c.ClientSecret == "" {
93 | return errors.New("client_secret must be specified")
94 | }
95 |
96 | if c.ReportDelay < 1 || c.ReportDelay > 4 {
97 | return errors.New("report-delay ranges from 1-4")
98 | }
99 |
100 | if c.UUID == "" {
101 | if uuid, err := uuid.GenerateUUID(); err == nil {
102 | c.UUID = uuid
103 | return saveOnce()
104 | } else {
105 | return fmt.Errorf("generate UUID failed: %v", err)
106 | }
107 | }
108 |
109 | return nil
110 | }
111 |
112 | func (c *AgentConfig) Save() error {
113 | data, err := yaml.Marshal(c)
114 | if err != nil {
115 | return err
116 | }
117 |
118 | dir := filepath.Dir(c.filePath)
119 | if err := os.MkdirAll(dir, 0750); err != nil {
120 | return err
121 | }
122 |
123 | return os.WriteFile(c.filePath, data, 0600)
124 | }
125 |
126 | type kubeyaml struct{}
127 |
128 | // Unmarshal parses the given YAML bytes.
129 | func (k *kubeyaml) Unmarshal(b []byte) (map[string]interface{}, error) {
130 | var out map[string]interface{}
131 | if err := yaml.Unmarshal(b, &out); err != nil {
132 | return nil, err
133 | }
134 |
135 | return out, nil
136 | }
137 |
138 | // Marshal marshals the given config map to YAML bytes.
139 | func (k *kubeyaml) Marshal(o map[string]interface{}) ([]byte, error) {
140 | return yaml.Marshal(o)
141 | }
142 |
--------------------------------------------------------------------------------
/.github/workflows/sync.py:
--------------------------------------------------------------------------------
1 | import os
2 | import time
3 | import requests
4 | import hashlib
5 | from github import Github
6 |
7 |
8 | def get_github_latest_release():
9 | g = Github()
10 | repo = g.get_repo("nezhahq/agent")
11 | release = repo.get_latest_release()
12 | if release:
13 | print(f"Latest release tag is: {release.tag_name}")
14 | print(f"Latest release info is: {release.body}")
15 | files = []
16 | for asset in release.get_assets():
17 | url = asset.browser_download_url
18 | name = asset.name
19 |
20 | response = requests.get(url)
21 | if response.status_code == 200:
22 | with open(name, 'wb') as f:
23 | f.write(response.content)
24 | print(f"Downloaded {name}")
25 | else:
26 | print(f"Failed to download {name}")
27 | file_abs_path = get_abs_path(asset.name)
28 | files.append(file_abs_path)
29 | print('Checking file integrities')
30 | verify_checksum(get_abs_path("checksums.txt"))
31 | sync_to_gitee(release.tag_name, release.body, files)
32 | else:
33 | print("No releases found.")
34 |
35 |
36 | def delete_gitee_releases(latest_id, client, uri, token):
37 | get_data = {
38 | 'access_token': token
39 | }
40 |
41 | release_info = []
42 | release_response = client.get(uri, json=get_data)
43 | if release_response.status_code == 200:
44 | release_info = release_response.json()
45 | else:
46 | print(
47 | f"Request failed with status code {release_response.status_code}")
48 |
49 | release_ids = []
50 | for block in release_info:
51 | if 'id' in block:
52 | release_ids.append(block['id'])
53 |
54 | print(f'Current release ids: {release_ids}')
55 | release_ids.remove(latest_id)
56 |
57 | for id in release_ids:
58 | release_uri = f"{uri}/{id}"
59 | delete_data = {
60 | 'access_token': token
61 | }
62 | delete_response = client.delete(release_uri, json=delete_data)
63 | if delete_response.status_code == 204:
64 | print(f'Successfully deleted release #{id}.')
65 | else:
66 | raise ValueError(
67 | f"Request failed with status code {delete_response.status_code}")
68 |
69 |
70 | def sync_to_gitee(tag: str, body: str, files: slice):
71 | release_id = ""
72 | owner = "naibahq"
73 | repo = "agent"
74 | release_api_uri = f"https://gitee.com/api/v5/repos/{owner}/{repo}/releases"
75 | api_client = requests.Session()
76 | api_client.headers.update({
77 | 'Accept': 'application/json',
78 | 'Content-Type': 'application/json'
79 | })
80 |
81 | access_token = os.environ['GITEE_TOKEN']
82 | release_data = {
83 | 'access_token': access_token,
84 | 'tag_name': tag,
85 | 'name': tag,
86 | 'body': body,
87 | 'prerelease': False,
88 | 'target_commitish': 'main'
89 | }
90 | while True:
91 | try:
92 | release_api_response = api_client.post(
93 | release_api_uri, json=release_data, timeout=30)
94 | release_api_response.raise_for_status()
95 | break
96 | except requests.exceptions.Timeout as errt:
97 | print(f"Request timed out: {errt} Retrying in 60 seconds...")
98 | time.sleep(60)
99 | except requests.exceptions.RequestException as err:
100 | print(f"Request failed: {err}")
101 | break
102 | if release_api_response.status_code == 201:
103 | release_info = release_api_response.json()
104 | release_id = release_info.get('id')
105 | else:
106 | print(
107 | f"Request failed with status code {release_api_response.status_code}")
108 |
109 | print(f"Gitee release id: {release_id}")
110 | asset_api_uri = f"{release_api_uri}/{release_id}/attach_files"
111 |
112 | for file_path in files:
113 | success = False
114 |
115 | while not success:
116 | files = {
117 | 'file': open(file_path, 'rb')
118 | }
119 |
120 | asset_api_response = requests.post(
121 | asset_api_uri, params={'access_token': access_token}, files=files)
122 |
123 | if asset_api_response.status_code == 201:
124 | asset_info = asset_api_response.json()
125 | asset_name = asset_info.get('name')
126 | print(f"Successfully uploaded {asset_name}!")
127 | success = True
128 | else:
129 | print(
130 | f"Request failed with status code {asset_api_response.status_code}")
131 |
132 | # 仅保留最新 Release 以防超出 Gitee 仓库配额
133 | try:
134 | delete_gitee_releases(release_id, api_client,
135 | release_api_uri, access_token)
136 | except ValueError as e:
137 | print(e)
138 |
139 | api_client.close()
140 | print("Sync is completed!")
141 |
142 |
143 | def get_abs_path(path: str):
144 | wd = os.getcwd()
145 | return os.path.join(wd, path)
146 |
147 |
148 | def compute_sha256(file: str):
149 | sha256_hash = hashlib.sha256()
150 | buf_size = 65536
151 | with open(file, 'rb') as f:
152 | while True:
153 | data = f.read(buf_size)
154 | if not data:
155 | break
156 | sha256_hash.update(data)
157 | return sha256_hash.hexdigest()
158 |
159 |
160 | def verify_checksum(checksum_file: str):
161 | with open(checksum_file, 'r') as f:
162 | lines = f.readlines()
163 |
164 | for line in lines:
165 | checksum, file = line.strip().split()
166 | abs_path = get_abs_path(file)
167 | computed_hash = compute_sha256(abs_path)
168 |
169 | if checksum == computed_hash:
170 | print(f"{file}: OK")
171 | else:
172 | print(f"{file}: FAIL (expected {checksum}, got {computed_hash})")
173 | print("Will run the download process again")
174 | get_github_latest_release()
175 | break
176 |
177 |
178 | get_github_latest_release()
179 |
--------------------------------------------------------------------------------
/pkg/monitor/monitor.go:
--------------------------------------------------------------------------------
1 | package monitor
2 |
3 | import (
4 | "context"
5 | "runtime"
6 | "sync/atomic"
7 | "time"
8 |
9 | "github.com/shirou/gopsutil/v4/host"
10 | "github.com/shirou/gopsutil/v4/mem"
11 | "github.com/shirou/gopsutil/v4/process"
12 |
13 | "github.com/nezhahq/agent/model"
14 | "github.com/nezhahq/agent/pkg/logger"
15 | "github.com/nezhahq/agent/pkg/monitor/conn"
16 | "github.com/nezhahq/agent/pkg/monitor/cpu"
17 | "github.com/nezhahq/agent/pkg/monitor/disk"
18 | "github.com/nezhahq/agent/pkg/monitor/gpu"
19 | "github.com/nezhahq/agent/pkg/monitor/load"
20 | "github.com/nezhahq/agent/pkg/monitor/nic"
21 | "github.com/nezhahq/agent/pkg/monitor/temperature"
22 | )
23 |
24 | var (
25 | Version string
26 | agentConfig *model.AgentConfig
27 |
28 | printf = logger.DefaultLogger.Printf
29 | )
30 |
31 | var (
32 | netInSpeed, netOutSpeed, netInTransfer, netOutTransfer, lastUpdateNetStats uint64
33 | cachedBootTime time.Time
34 | temperatureStat []model.SensorTemperature
35 | )
36 |
37 | // 获取设备数据的最大尝试次数
38 | const maxDeviceDataFetchAttempts = 3
39 |
40 | const (
41 | CPU = iota + 1
42 | GPU
43 | Load
44 | Temperatures
45 | )
46 |
47 | // 获取主机数据的尝试次数,Key 为 Host 的属性名
48 | var hostDataFetchAttempts = map[uint8]uint8{
49 | CPU: 0,
50 | GPU: 0,
51 | }
52 |
53 | // 获取状态数据的尝试次数,Key 为 HostState 的属性名
54 | var statDataFetchAttempts = map[uint8]uint8{
55 | CPU: 0,
56 | GPU: 0,
57 | Load: 0,
58 | Temperatures: 0,
59 | }
60 |
61 | var (
62 | updateTempStatus = new(atomic.Bool)
63 | )
64 |
65 | func InitConfig(cfg *model.AgentConfig) {
66 | agentConfig = cfg
67 | }
68 |
69 | // GetHost 获取主机硬件信息
70 | func GetHost() *model.Host {
71 | var ret model.Host
72 |
73 | var cpuType string
74 | hi, err := host.Info()
75 | if err != nil {
76 | printf("host.Info error: %v", err)
77 | } else {
78 | if hi.VirtualizationRole == "guest" {
79 | cpuType = "Virtual"
80 | ret.Virtualization = hi.VirtualizationSystem
81 | } else {
82 | cpuType = "Physical"
83 | ret.Virtualization = ""
84 | }
85 | ret.Platform = hi.Platform
86 | ret.PlatformVersion = hi.PlatformVersion
87 | ret.Arch = hi.KernelArch
88 | ret.BootTime = hi.BootTime
89 | }
90 |
91 | ctxCpu := context.WithValue(context.Background(), cpu.CPUHostKey, cpuType)
92 | ret.CPU = tryHost(ctxCpu, CPU, cpu.GetHost)
93 |
94 | if agentConfig.GPU {
95 | ret.GPU = tryHost(context.Background(), GPU, gpu.GetHost)
96 | }
97 |
98 | ret.DiskTotal = getDiskTotal()
99 |
100 | mv, err := mem.VirtualMemory()
101 | if err != nil {
102 | printf("mem.VirtualMemory error: %v", err)
103 | } else {
104 | ret.MemTotal = mv.Total
105 | if runtime.GOOS != "windows" {
106 | ret.SwapTotal = mv.SwapTotal
107 | }
108 | }
109 |
110 | if runtime.GOOS == "windows" {
111 | ms, err := mem.SwapMemory()
112 | if err != nil {
113 | printf("mem.SwapMemory error: %v", err)
114 | } else {
115 | ret.SwapTotal = ms.Total
116 | }
117 | }
118 |
119 | cachedBootTime = time.Unix(int64(hi.BootTime), 0)
120 |
121 | ret.Version = Version
122 |
123 | return &ret
124 | }
125 |
126 | func GetState(skipConnectionCount bool, skipProcsCount bool) *model.HostState {
127 | var ret model.HostState
128 |
129 | cp := tryStat(context.Background(), CPU, cpu.GetState)
130 | if len(cp) > 0 {
131 | ret.CPU = cp[0]
132 | }
133 |
134 | vm, err := mem.VirtualMemory()
135 | if err != nil {
136 | printf("mem.VirtualMemory error: %v", err)
137 | } else {
138 | ret.MemUsed = vm.Total - vm.Available
139 | if runtime.GOOS != "windows" {
140 | ret.SwapUsed = vm.SwapTotal - vm.SwapFree
141 | }
142 | }
143 | if runtime.GOOS == "windows" {
144 | // gopsutil 在 Windows 下不能正确取 swap
145 | ms, err := mem.SwapMemory()
146 | if err != nil {
147 | printf("mem.SwapMemory error: %v", err)
148 | } else {
149 | ret.SwapUsed = ms.Used
150 | }
151 | }
152 |
153 | ret.DiskUsed = getDiskUsed()
154 |
155 | loadStat := tryStat(context.Background(), Load, load.GetState)
156 | ret.Load1 = loadStat.Load1
157 | ret.Load5 = loadStat.Load5
158 | ret.Load15 = loadStat.Load15
159 |
160 | var procs []int32
161 | if !skipProcsCount {
162 | procs, err = process.Pids()
163 | if err != nil {
164 | printf("process.Pids error: %v", err)
165 | } else {
166 | ret.ProcessCount = uint64(len(procs))
167 | }
168 | }
169 |
170 | if agentConfig.Temperature {
171 | go updateTemperatureStat()
172 | ret.Temperatures = temperatureStat
173 | }
174 |
175 | if agentConfig.GPU {
176 | ret.GPU = tryStat(context.Background(), GPU, gpu.GetState)
177 | }
178 |
179 | ret.NetInTransfer, ret.NetOutTransfer = netInTransfer, netOutTransfer
180 | ret.NetInSpeed, ret.NetOutSpeed = netInSpeed, netOutSpeed
181 | ret.Uptime = uint64(time.Since(cachedBootTime).Seconds())
182 |
183 | if !skipConnectionCount {
184 | ret.TcpConnCount, ret.UdpConnCount = getConns()
185 | }
186 |
187 | return &ret
188 | }
189 |
190 | // TrackNetworkSpeed NIC监控,统计流量与速度
191 | func TrackNetworkSpeed() {
192 | var innerNetInTransfer, innerNetOutTransfer uint64
193 |
194 | ctx := context.WithValue(context.Background(), nic.NICKey, agentConfig.NICAllowlist)
195 | nc, err := nic.GetState(ctx)
196 | if err != nil {
197 | return
198 | }
199 |
200 | innerNetInTransfer = nc[0]
201 | innerNetOutTransfer = nc[1]
202 |
203 | now := uint64(time.Now().Unix())
204 | diff := now - lastUpdateNetStats
205 | if diff > 0 {
206 | netInSpeed = (innerNetInTransfer - netInTransfer) / diff
207 | netOutSpeed = (innerNetOutTransfer - netOutTransfer) / diff
208 | }
209 | netInTransfer = innerNetInTransfer
210 | netOutTransfer = innerNetOutTransfer
211 | lastUpdateNetStats = now
212 | }
213 |
214 | func getDiskTotal() uint64 {
215 | ctx := context.WithValue(context.Background(), disk.DiskKey, agentConfig.HardDrivePartitionAllowlist)
216 | total, _ := disk.GetHost(ctx)
217 |
218 | return total
219 | }
220 |
221 | func getDiskUsed() uint64 {
222 | ctx := context.WithValue(context.Background(), disk.DiskKey, agentConfig.HardDrivePartitionAllowlist)
223 | used, _ := disk.GetState(ctx)
224 |
225 | return used
226 | }
227 |
228 | func getConns() (tcpConnCount, udpConnCount uint64) {
229 | connStat, err := conn.GetState(context.Background())
230 | if err != nil {
231 | return
232 | }
233 |
234 | if len(connStat) < 2 {
235 | return
236 | }
237 |
238 | return connStat[0], connStat[1]
239 | }
240 |
241 | func updateTemperatureStat() {
242 | if !updateTempStatus.CompareAndSwap(false, true) {
243 | return
244 | }
245 | defer updateTempStatus.Store(false)
246 |
247 | stat := tryStat(context.Background(), Temperatures, temperature.GetState)
248 | temperatureStat = stat
249 | }
250 |
251 | type hostStateFunc[T any] func(context.Context) (T, error)
252 |
253 | func tryHost[T any](ctx context.Context, typ uint8, f hostStateFunc[T]) T {
254 | var val T
255 |
256 | if hostDataFetchAttempts[typ] < maxDeviceDataFetchAttempts {
257 | v, err := f(ctx)
258 | if err != nil {
259 | hostDataFetchAttempts[typ]++
260 | printf("monitor error: %v, attempt: %d", err, hostDataFetchAttempts[typ])
261 | return val
262 | } else {
263 | val = v
264 | hostDataFetchAttempts[typ] = 0
265 | }
266 | }
267 | return val
268 | }
269 |
270 | func tryStat[T any](ctx context.Context, typ uint8, f hostStateFunc[T]) T {
271 | var val T
272 |
273 | if statDataFetchAttempts[typ] < maxDeviceDataFetchAttempts {
274 | v, err := f(ctx)
275 | if err != nil {
276 | statDataFetchAttempts[typ]++
277 | printf("monitor error: %v, attempt: %d", err, statDataFetchAttempts[typ])
278 | return val
279 | } else {
280 | val = v
281 | statDataFetchAttempts[typ] = 0
282 | }
283 | }
284 | return val
285 | }
286 |
--------------------------------------------------------------------------------
/pkg/utls/roundtripper.go:
--------------------------------------------------------------------------------
1 | // SPDX-FileCopyrightText: Copyright (c) 2016, Serene Han, Arlo Breault
2 | // SPDX-FileCopyrightText: Copyright (c) 2019-2020, The Tor Project, Inc
3 | // SPDX-License-Identifier: BSD-3-Clause
4 | // https://gitlab.torproject.org/tpo/anti-censorship/pluggable-transports/snowflake/-/blob/main/common/utls/roundtripper.go
5 |
6 | package utls
7 |
8 | import (
9 | "context"
10 | "crypto/tls"
11 | "errors"
12 | "fmt"
13 | "math/rand"
14 | "net"
15 | "net/http"
16 | "net/url"
17 | "sync"
18 | "time"
19 |
20 | utls "github.com/refraction-networking/utls"
21 | "golang.org/x/net/http2"
22 | "golang.org/x/net/proxy"
23 | )
24 |
25 | // NewUTLSHTTPRoundTripperWithProxy creates an instance of RoundTripper that dial to remote HTTPS endpoint with
26 | // an alternative version of TLS implementation that attempts to imitate browsers' fingerprint.
27 | // clientHelloID is the clientHello that uTLS attempts to imitate
28 | // uTlsConfig is the TLS Configuration template
29 | // backdropTransport is the transport that will be used for non-https traffic
30 | // returns a RoundTripper: its behaviour is documented at
31 | // https://gitlab.torproject.org/tpo/anti-censorship/pluggable-transports/snowflake/-/merge_requests/76#note_2777161
32 | func NewUTLSHTTPRoundTripperWithProxy(clientHelloID utls.ClientHelloID, uTlsConfig *utls.Config,
33 | backdropTransport http.RoundTripper, proxy *url.URL, header *http.Header) http.RoundTripper {
34 | rtImpl := &uTLSHTTPRoundTripperImpl{
35 | clientHelloID: clientHelloID,
36 | config: uTlsConfig,
37 | connectWithH1: map[string]bool{},
38 | backdropTransport: backdropTransport,
39 | pendingConn: map[pendingConnKey]*unclaimedConnection{},
40 | proxyAddr: proxy,
41 | headers: header,
42 | }
43 | rtImpl.init()
44 | return rtImpl
45 | }
46 |
47 | type uTLSHTTPRoundTripperImpl struct {
48 | clientHelloID utls.ClientHelloID
49 | config *utls.Config
50 |
51 | accessConnectWithH1 sync.Mutex
52 | connectWithH1 map[string]bool
53 |
54 | httpsH1Transport http.RoundTripper
55 | httpsH2Transport http.RoundTripper
56 | backdropTransport http.RoundTripper
57 |
58 | accessDialingConnection sync.Mutex
59 | pendingConn map[pendingConnKey]*unclaimedConnection
60 |
61 | proxyAddr *url.URL
62 |
63 | headers *http.Header
64 | }
65 |
66 | type pendingConnKey struct {
67 | isH2 bool
68 | dest string
69 | }
70 |
71 | var (
72 | errEAGAIN = errors.New("incorrect ALPN negotiated, try again with another ALPN")
73 | errEAGAINTooMany = errors.New("incorrect ALPN negotiated")
74 | errExpired = errors.New("connection have expired")
75 | )
76 |
77 | func (r *uTLSHTTPRoundTripperImpl) RoundTrip(req *http.Request) (*http.Response, error) {
78 | req.Header = *r.headers
79 |
80 | if req.URL.Scheme != "https" {
81 | return r.backdropTransport.RoundTrip(req)
82 | }
83 | for retryCount := 0; retryCount < 5; retryCount++ {
84 | effectivePort := req.URL.Port()
85 | if effectivePort == "" {
86 | effectivePort = "443"
87 | }
88 | if r.getShouldConnectWithH1(fmt.Sprintf("%v:%v", req.URL.Hostname(), effectivePort)) {
89 | resp, err := r.httpsH1Transport.RoundTrip(req)
90 | if errors.Is(err, errEAGAIN) {
91 | continue
92 | }
93 | return resp, err
94 | }
95 | resp, err := r.httpsH2Transport.RoundTrip(req)
96 | if errors.Is(err, errEAGAIN) {
97 | continue
98 | }
99 | return resp, err
100 | }
101 | return nil, errEAGAINTooMany
102 | }
103 |
104 | func (r *uTLSHTTPRoundTripperImpl) getShouldConnectWithH1(domainName string) bool {
105 | r.accessConnectWithH1.Lock()
106 | defer r.accessConnectWithH1.Unlock()
107 | if value, set := r.connectWithH1[domainName]; set {
108 | return value
109 | }
110 | return false
111 | }
112 |
113 | func (r *uTLSHTTPRoundTripperImpl) setShouldConnectWithH1(domainName string) {
114 | r.accessConnectWithH1.Lock()
115 | defer r.accessConnectWithH1.Unlock()
116 | r.connectWithH1[domainName] = true
117 | }
118 |
119 | func (r *uTLSHTTPRoundTripperImpl) clearShouldConnectWithH1(domainName string) {
120 | r.accessConnectWithH1.Lock()
121 | defer r.accessConnectWithH1.Unlock()
122 | r.connectWithH1[domainName] = false
123 | }
124 |
125 | func getPendingConnectionID(dest string, alpnIsH2 bool) pendingConnKey {
126 | return pendingConnKey{isH2: alpnIsH2, dest: dest}
127 | }
128 |
129 | func (r *uTLSHTTPRoundTripperImpl) putConn(addr string, alpnIsH2 bool, conn net.Conn) {
130 | connId := getPendingConnectionID(addr, alpnIsH2)
131 | r.pendingConn[connId] = newUnclaimedConnection(conn, time.Minute)
132 | }
133 |
134 | func (r *uTLSHTTPRoundTripperImpl) getConn(addr string, alpnIsH2 bool) net.Conn {
135 | connId := getPendingConnectionID(addr, alpnIsH2)
136 | if conn, ok := r.pendingConn[connId]; ok {
137 | delete(r.pendingConn, connId)
138 | if claimedConnection, err := conn.claimConnection(); err == nil {
139 | return claimedConnection
140 | }
141 | }
142 | return nil
143 | }
144 |
145 | func (r *uTLSHTTPRoundTripperImpl) dialOrGetTLSWithExpectedALPN(ctx context.Context, addr string, expectedH2 bool) (net.Conn, error) {
146 | r.accessDialingConnection.Lock()
147 | defer r.accessDialingConnection.Unlock()
148 |
149 | if r.getShouldConnectWithH1(addr) == expectedH2 {
150 | return nil, errEAGAIN
151 | }
152 |
153 | //Get a cached connection if possible to reduce preflight connection closed without sending data
154 | if gconn := r.getConn(addr, expectedH2); gconn != nil {
155 | return gconn, nil
156 | }
157 |
158 | conn, err := r.dialTLS(ctx, addr)
159 | if err != nil {
160 | return nil, err
161 | }
162 |
163 | protocol := conn.ConnectionState().NegotiatedProtocol
164 |
165 | protocolIsH2 := protocol == http2.NextProtoTLS
166 |
167 | if protocolIsH2 == expectedH2 {
168 | return conn, err
169 | }
170 |
171 | r.putConn(addr, protocolIsH2, conn)
172 |
173 | if protocolIsH2 {
174 | r.clearShouldConnectWithH1(addr)
175 | } else {
176 | r.setShouldConnectWithH1(addr)
177 | }
178 |
179 | return nil, errEAGAIN
180 | }
181 |
182 | // based on https://repo.or.cz/dnstt.git/commitdiff/d92a791b6864901f9263f7d73d97cfd30ac53b09..98bdffa1706dfc041d1e99b86c47f29d72ad3a0c
183 | // by dcf1
184 | func (r *uTLSHTTPRoundTripperImpl) dialTLS(ctx context.Context, addr string) (*utls.UConn, error) {
185 | config := r.config.Clone()
186 |
187 | host, _, err := net.SplitHostPort(addr)
188 | if err != nil {
189 | return nil, err
190 | }
191 | config.ServerName = host
192 |
193 | systemDialer := &net.Dialer{}
194 |
195 | var dialer proxy.ContextDialer
196 | dialer = systemDialer
197 |
198 | if r.proxyAddr != nil {
199 | proxyDialer, err := proxy.FromURL(r.proxyAddr, systemDialer)
200 | if err != nil {
201 | return nil, err
202 | }
203 | dialer = proxyDialer.(proxy.ContextDialer)
204 | }
205 |
206 | conn, err := dialer.DialContext(ctx, "tcp", addr)
207 | if err != nil {
208 | return nil, err
209 | }
210 | uconn := utls.UClient(conn, config, r.clientHelloID)
211 | if net.ParseIP(config.ServerName) != nil {
212 | err := uconn.RemoveSNIExtension()
213 | if err != nil {
214 | uconn.Close()
215 | return nil, err
216 | }
217 | }
218 |
219 | err = uconn.Handshake()
220 | if err != nil {
221 | return nil, err
222 | }
223 | return uconn, nil
224 | }
225 |
226 | func (r *uTLSHTTPRoundTripperImpl) init() {
227 | min := 1 << 13
228 | max := 1 << 14
229 |
230 | r.httpsH2Transport = &http2.Transport{
231 | DialTLS: func(network, addr string, cfg *tls.Config) (net.Conn, error) {
232 | return r.dialOrGetTLSWithExpectedALPN(context.Background(), addr, true)
233 | },
234 | MaxReadFrameSize: 16384,
235 | MaxDecoderHeaderTableSize: uint32(rand.Intn(max-min) + min),
236 | }
237 | r.httpsH1Transport = &http.Transport{
238 | DialTLSContext: func(ctx context.Context, network string, addr string) (net.Conn, error) {
239 | return r.dialOrGetTLSWithExpectedALPN(ctx, addr, false)
240 | },
241 | }
242 | }
243 |
244 | func newUnclaimedConnection(conn net.Conn, expireTime time.Duration) *unclaimedConnection {
245 | c := &unclaimedConnection{
246 | Conn: conn,
247 | }
248 | time.AfterFunc(expireTime, c.tick)
249 | return c
250 | }
251 |
252 | type unclaimedConnection struct {
253 | net.Conn
254 | claimed bool
255 | access sync.Mutex
256 | }
257 |
258 | func (c *unclaimedConnection) claimConnection() (net.Conn, error) {
259 | c.access.Lock()
260 | defer c.access.Unlock()
261 | if !c.claimed {
262 | c.claimed = true
263 | return c.Conn, nil
264 | }
265 | return nil, errExpired
266 | }
267 |
268 | func (c *unclaimedConnection) tick() {
269 | c.access.Lock()
270 | defer c.access.Unlock()
271 | if !c.claimed {
272 | c.claimed = true
273 | c.Conn.Close()
274 | c.Conn = nil
275 | }
276 | }
277 |
--------------------------------------------------------------------------------
/pkg/monitor/gpu/gpu_darwin.go:
--------------------------------------------------------------------------------
1 | //go:build darwin
2 |
3 | package gpu
4 |
5 | import (
6 | "context"
7 | "fmt"
8 | "unsafe"
9 |
10 | "github.com/ebitengine/purego"
11 |
12 | "github.com/nezhahq/agent/pkg/util"
13 | )
14 |
15 | type (
16 | CFStringEncoding = uint32
17 | CFIndex = int32
18 | CFTypeID = int32
19 | CFNumberType = CFIndex
20 | CFTypeRef = unsafe.Pointer
21 | CFStringRef = unsafe.Pointer
22 | CFDictionaryRef = unsafe.Pointer
23 |
24 | machPort = uint32
25 | ioIterator = uint32
26 | ioObject = uint32
27 | ioRegistryEntry = uint32
28 | ioService = uint32
29 | IOOptionBits = uint32
30 | )
31 |
32 | type (
33 | CFStringCreateWithCStringFunc = func(alloc uintptr, cStr string, encoding CFStringEncoding) CFStringRef
34 | CFGetTypeIDFunc = func(cf uintptr) CFTypeID
35 | CFStringGetTypeIDFunc = func() CFTypeID
36 | CFStringGetLengthFunc = func(theString uintptr) int32
37 | CFStringGetCStringFunc = func(cfStr uintptr, buffer *byte, size CFIndex, encoding CFStringEncoding) bool
38 | CFDictionaryGetTypeIDFunc = func() CFTypeID
39 | CFDictionaryGetValueFunc = func(dict, key uintptr) unsafe.Pointer
40 | CFDataGetTypeIDFunc = func() CFTypeID
41 | CFDataGetBytePtrFunc = func(theData uintptr) unsafe.Pointer
42 | CFDataGetLengthFunc = func(theData uintptr) CFIndex
43 | CFNumberGetValueFunc = func(number uintptr, theType CFNumberType, valuePtr uintptr) bool
44 | CFReleaseFunc = func(cf uintptr)
45 |
46 | IOServiceGetMatchingServicesFunc = func(mainPort machPort, matching uintptr, existing *ioIterator) ioService
47 | IOIteratorNextFunc = func(iterator ioIterator) ioObject
48 | IOServiceMatchingFunc = func(name string) CFDictionaryRef
49 | IORegistryEntrySearchCFPropertyFunc = func(entry ioRegistryEntry, plane string, key, allocator uintptr, options IOOptionBits) CFTypeRef
50 | IOObjectReleaseFunc = func(object ioObject) int
51 | )
52 |
53 | const (
54 | KERN_SUCCESS = 0
55 | MACH_PORT_NULL = 0
56 | IOSERVICE_GPU = "IOAccelerator"
57 | IOSERVICE_PCI = "IOPCIDevice"
58 |
59 | kIOServicePlane = "IOService"
60 | kIORegistryIterateRecursively = 1
61 | kCFStringEncodingUTF8 = 0x08000100
62 | kCFNumberIntType = 9
63 | )
64 |
65 | var (
66 | kCFAllocatorDefault uintptr = 0
67 | kIOMainPortDefault machPort = 0
68 | )
69 |
70 | var (
71 | coreFoundation, _ = purego.Dlopen("/System/Library/Frameworks/CoreFoundation.framework/CoreFoundation", purego.RTLD_LAZY|purego.RTLD_GLOBAL)
72 | ioKit, _ = purego.Dlopen("/System/Library/Frameworks/IOKit.framework/IOKit", purego.RTLD_LAZY|purego.RTLD_GLOBAL)
73 | )
74 |
75 | var (
76 | CFStringCreateWithCString CFStringCreateWithCStringFunc
77 | CFGetTypeID CFGetTypeIDFunc
78 | CFStringGetTypeID CFStringGetTypeIDFunc
79 | CFStringGetLength CFStringGetLengthFunc
80 | CFStringGetCString CFStringGetCStringFunc
81 | CFDictionaryGetTypeID CFDictionaryGetTypeIDFunc
82 | CFDictionaryGetValue CFDictionaryGetValueFunc
83 | CFDataGetTypeID CFDataGetTypeIDFunc
84 | CFDataGetBytePtr CFDataGetBytePtrFunc
85 | CFDataGetLength CFDataGetLengthFunc
86 | CFNumberGetValue CFNumberGetValueFunc
87 | CFRelease CFReleaseFunc
88 |
89 | IOServiceGetMatchingServices IOServiceGetMatchingServicesFunc
90 | IOIteratorNext IOIteratorNextFunc
91 | IOServiceMatching IOServiceMatchingFunc
92 | IORegistryEntrySearchCFProperty IORegistryEntrySearchCFPropertyFunc
93 | IOObjectRelease IOObjectReleaseFunc
94 | )
95 |
96 | var validVendors = []string{
97 | "AMD", "Intel", "NVIDIA", "Apple",
98 | }
99 |
100 | func init() {
101 | purego.RegisterLibFunc(&CFStringCreateWithCString, coreFoundation, "CFStringCreateWithCString")
102 | purego.RegisterLibFunc(&CFGetTypeID, coreFoundation, "CFGetTypeID")
103 | purego.RegisterLibFunc(&CFStringGetTypeID, coreFoundation, "CFStringGetTypeID")
104 | purego.RegisterLibFunc(&CFStringGetLength, coreFoundation, "CFStringGetLength")
105 | purego.RegisterLibFunc(&CFStringGetCString, coreFoundation, "CFStringGetCString")
106 | purego.RegisterLibFunc(&CFDictionaryGetTypeID, coreFoundation, "CFDictionaryGetTypeID")
107 | purego.RegisterLibFunc(&CFDictionaryGetValue, coreFoundation, "CFDictionaryGetValue")
108 | purego.RegisterLibFunc(&CFDataGetTypeID, coreFoundation, "CFDataGetTypeID")
109 | purego.RegisterLibFunc(&CFDataGetBytePtr, coreFoundation, "CFDataGetBytePtr")
110 | purego.RegisterLibFunc(&CFDataGetLength, coreFoundation, "CFDataGetLength")
111 | purego.RegisterLibFunc(&CFNumberGetValue, coreFoundation, "CFNumberGetValue")
112 | purego.RegisterLibFunc(&CFRelease, coreFoundation, "CFRelease")
113 |
114 | purego.RegisterLibFunc(&IOServiceGetMatchingServices, ioKit, "IOServiceGetMatchingServices")
115 | purego.RegisterLibFunc(&IOIteratorNext, ioKit, "IOIteratorNext")
116 | purego.RegisterLibFunc(&IOServiceMatching, ioKit, "IOServiceMatching")
117 | purego.RegisterLibFunc(&IORegistryEntrySearchCFProperty, ioKit, "IORegistryEntrySearchCFProperty")
118 | purego.RegisterLibFunc(&IOObjectRelease, ioKit, "IOObjectRelease")
119 | }
120 |
121 | func GetHost(_ context.Context) ([]string, error) {
122 | models, err := findDevices("model")
123 | if err != nil {
124 | return nil, err
125 | }
126 | return util.RemoveDuplicate(models), nil
127 | }
128 |
129 | func GetState(_ context.Context) ([]float64, error) {
130 | usage, err := findUtilization("PerformanceStatistics", "Device Utilization %")
131 | return []float64{float64(usage)}, err
132 | }
133 |
134 | func findDevices(key string) ([]string, error) {
135 | var iterator ioIterator
136 | var results []string
137 | done := false
138 |
139 | iv := IOServiceGetMatchingServices(kIOMainPortDefault, uintptr(IOServiceMatching(IOSERVICE_GPU)), &iterator)
140 | if iv != KERN_SUCCESS {
141 | return nil, fmt.Errorf("error retrieving GPU entry")
142 | }
143 |
144 | var service ioObject
145 | index := 0
146 |
147 | for {
148 | service = IOIteratorNext(iterator)
149 | if service == MACH_PORT_NULL {
150 | break
151 | }
152 |
153 | cfStr := CFStringCreateWithCString(kCFAllocatorDefault, key, kCFStringEncodingUTF8)
154 | result, _, _ := findProperties(service, uintptr(cfStr), 0)
155 | IOObjectRelease(service)
156 |
157 | if util.ContainsStr(validVendors, result) {
158 | results = append(results, result)
159 | index++
160 | } else if key == "model" && !done {
161 | IOObjectRelease(iterator)
162 | iv = IOServiceGetMatchingServices(kIOMainPortDefault, uintptr(IOServiceMatching(IOSERVICE_PCI)), &iterator)
163 | if iv != KERN_SUCCESS {
164 | return nil, fmt.Errorf("error retrieving GPU entry")
165 | }
166 | done = true
167 | }
168 | }
169 |
170 | IOObjectRelease(iterator)
171 | return results, nil
172 | }
173 |
174 | func findUtilization(key, dictKey string) (int, error) {
175 | var iterator ioIterator
176 | var err error
177 | result := 0
178 |
179 | iv := IOServiceGetMatchingServices(kIOMainPortDefault, uintptr(IOServiceMatching(IOSERVICE_GPU)), &iterator)
180 | if iv != KERN_SUCCESS {
181 | return 0, fmt.Errorf("error retrieving GPU entry")
182 | }
183 |
184 | // Only retrieving the utilization of a single GPU here
185 | var service ioObject
186 | for {
187 | service = IOIteratorNext(iterator)
188 | if service == MACH_PORT_NULL {
189 | break
190 | }
191 |
192 | cfStr := CFStringCreateWithCString(kCFAllocatorDefault, key, CFStringEncoding(kCFStringEncodingUTF8))
193 | cfDictStr := CFStringCreateWithCString(kCFAllocatorDefault, dictKey, CFStringEncoding(kCFStringEncodingUTF8))
194 |
195 | _, result, err = findProperties(service, uintptr(cfStr), uintptr(cfDictStr))
196 |
197 | CFRelease(uintptr(cfStr))
198 | CFRelease(uintptr(cfDictStr))
199 |
200 | if err != nil {
201 | IOObjectRelease(service)
202 | continue
203 | } else if result != 0 {
204 | break
205 | }
206 | }
207 |
208 | IOObjectRelease(service)
209 | IOObjectRelease(iterator)
210 |
211 | return result, err
212 | }
213 |
214 | func findProperties(service ioRegistryEntry, key, dictKey uintptr) (string, int, error) {
215 | properties := IORegistryEntrySearchCFProperty(service, kIOServicePlane, key, kCFAllocatorDefault, kIORegistryIterateRecursively)
216 | ptrValue := uintptr(properties)
217 | if properties != nil {
218 | switch CFGetTypeID(ptrValue) {
219 | // model
220 | case CFStringGetTypeID():
221 | length := CFStringGetLength(ptrValue) + 1 // null terminator
222 | buf := make([]byte, length-1)
223 | CFStringGetCString(ptrValue, &buf[0], length, uint32(kCFStringEncodingUTF8))
224 | CFRelease(ptrValue)
225 | return string(buf), 0, nil
226 | case CFDataGetTypeID():
227 | length := CFDataGetLength(ptrValue)
228 | bin := unsafe.String((*byte)(CFDataGetBytePtr(ptrValue)), length)
229 | CFRelease(ptrValue)
230 | return bin, 0, nil
231 | // PerformanceStatistics
232 | case CFDictionaryGetTypeID():
233 | cfValue := CFDictionaryGetValue(ptrValue, dictKey)
234 | if cfValue != nil {
235 | var value int
236 | if CFNumberGetValue(uintptr(cfValue), kCFNumberIntType, uintptr(unsafe.Pointer(&value))) {
237 | return "", value, nil
238 | } else {
239 | return "", 0, fmt.Errorf("failed to exec CFNumberGetValue")
240 | }
241 | } else {
242 | return "", 0, fmt.Errorf("failed to exec CFDictionaryGetValue")
243 | }
244 | }
245 | }
246 | return "", 0, fmt.Errorf("failed to exec IORegistryEntrySearchCFProperty")
247 | }
248 |
--------------------------------------------------------------------------------
/proto/nezha_grpc.pb.go:
--------------------------------------------------------------------------------
1 | // Code generated by protoc-gen-go-grpc. DO NOT EDIT.
2 | // versions:
3 | // - protoc-gen-go-grpc v1.3.0
4 | // - protoc v5.28.1
5 | // source: proto/nezha.proto
6 |
7 | package proto
8 |
9 | import (
10 | context "context"
11 | grpc "google.golang.org/grpc"
12 | codes "google.golang.org/grpc/codes"
13 | status "google.golang.org/grpc/status"
14 | )
15 |
16 | // This is a compile-time assertion to ensure that this generated file
17 | // is compatible with the grpc package it is being compiled against.
18 | // Requires gRPC-Go v1.32.0 or later.
19 | const _ = grpc.SupportPackageIsVersion7
20 |
21 | const (
22 | NezhaService_ReportSystemState_FullMethodName = "/proto.NezhaService/ReportSystemState"
23 | NezhaService_ReportSystemInfo_FullMethodName = "/proto.NezhaService/ReportSystemInfo"
24 | NezhaService_RequestTask_FullMethodName = "/proto.NezhaService/RequestTask"
25 | NezhaService_IOStream_FullMethodName = "/proto.NezhaService/IOStream"
26 | NezhaService_ReportGeoIP_FullMethodName = "/proto.NezhaService/ReportGeoIP"
27 | NezhaService_ReportSystemInfo2_FullMethodName = "/proto.NezhaService/ReportSystemInfo2"
28 | )
29 |
30 | // NezhaServiceClient is the client API for NezhaService service.
31 | //
32 | // For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
33 | type NezhaServiceClient interface {
34 | ReportSystemState(ctx context.Context, opts ...grpc.CallOption) (NezhaService_ReportSystemStateClient, error)
35 | ReportSystemInfo(ctx context.Context, in *Host, opts ...grpc.CallOption) (*Receipt, error)
36 | RequestTask(ctx context.Context, opts ...grpc.CallOption) (NezhaService_RequestTaskClient, error)
37 | IOStream(ctx context.Context, opts ...grpc.CallOption) (NezhaService_IOStreamClient, error)
38 | ReportGeoIP(ctx context.Context, in *GeoIP, opts ...grpc.CallOption) (*GeoIP, error)
39 | ReportSystemInfo2(ctx context.Context, in *Host, opts ...grpc.CallOption) (*Uint64Receipt, error)
40 | }
41 |
42 | type nezhaServiceClient struct {
43 | cc grpc.ClientConnInterface
44 | }
45 |
46 | func NewNezhaServiceClient(cc grpc.ClientConnInterface) NezhaServiceClient {
47 | return &nezhaServiceClient{cc}
48 | }
49 |
50 | func (c *nezhaServiceClient) ReportSystemState(ctx context.Context, opts ...grpc.CallOption) (NezhaService_ReportSystemStateClient, error) {
51 | stream, err := c.cc.NewStream(ctx, &NezhaService_ServiceDesc.Streams[0], NezhaService_ReportSystemState_FullMethodName, opts...)
52 | if err != nil {
53 | return nil, err
54 | }
55 | x := &nezhaServiceReportSystemStateClient{stream}
56 | return x, nil
57 | }
58 |
59 | type NezhaService_ReportSystemStateClient interface {
60 | Send(*State) error
61 | Recv() (*Receipt, error)
62 | grpc.ClientStream
63 | }
64 |
65 | type nezhaServiceReportSystemStateClient struct {
66 | grpc.ClientStream
67 | }
68 |
69 | func (x *nezhaServiceReportSystemStateClient) Send(m *State) error {
70 | return x.ClientStream.SendMsg(m)
71 | }
72 |
73 | func (x *nezhaServiceReportSystemStateClient) Recv() (*Receipt, error) {
74 | m := new(Receipt)
75 | if err := x.ClientStream.RecvMsg(m); err != nil {
76 | return nil, err
77 | }
78 | return m, nil
79 | }
80 |
81 | func (c *nezhaServiceClient) ReportSystemInfo(ctx context.Context, in *Host, opts ...grpc.CallOption) (*Receipt, error) {
82 | out := new(Receipt)
83 | err := c.cc.Invoke(ctx, NezhaService_ReportSystemInfo_FullMethodName, in, out, opts...)
84 | if err != nil {
85 | return nil, err
86 | }
87 | return out, nil
88 | }
89 |
90 | func (c *nezhaServiceClient) RequestTask(ctx context.Context, opts ...grpc.CallOption) (NezhaService_RequestTaskClient, error) {
91 | stream, err := c.cc.NewStream(ctx, &NezhaService_ServiceDesc.Streams[1], NezhaService_RequestTask_FullMethodName, opts...)
92 | if err != nil {
93 | return nil, err
94 | }
95 | x := &nezhaServiceRequestTaskClient{stream}
96 | return x, nil
97 | }
98 |
99 | type NezhaService_RequestTaskClient interface {
100 | Send(*TaskResult) error
101 | Recv() (*Task, error)
102 | grpc.ClientStream
103 | }
104 |
105 | type nezhaServiceRequestTaskClient struct {
106 | grpc.ClientStream
107 | }
108 |
109 | func (x *nezhaServiceRequestTaskClient) Send(m *TaskResult) error {
110 | return x.ClientStream.SendMsg(m)
111 | }
112 |
113 | func (x *nezhaServiceRequestTaskClient) Recv() (*Task, error) {
114 | m := new(Task)
115 | if err := x.ClientStream.RecvMsg(m); err != nil {
116 | return nil, err
117 | }
118 | return m, nil
119 | }
120 |
121 | func (c *nezhaServiceClient) IOStream(ctx context.Context, opts ...grpc.CallOption) (NezhaService_IOStreamClient, error) {
122 | stream, err := c.cc.NewStream(ctx, &NezhaService_ServiceDesc.Streams[2], NezhaService_IOStream_FullMethodName, opts...)
123 | if err != nil {
124 | return nil, err
125 | }
126 | x := &nezhaServiceIOStreamClient{stream}
127 | return x, nil
128 | }
129 |
130 | type NezhaService_IOStreamClient interface {
131 | Send(*IOStreamData) error
132 | Recv() (*IOStreamData, error)
133 | grpc.ClientStream
134 | }
135 |
136 | type nezhaServiceIOStreamClient struct {
137 | grpc.ClientStream
138 | }
139 |
140 | func (x *nezhaServiceIOStreamClient) Send(m *IOStreamData) error {
141 | return x.ClientStream.SendMsg(m)
142 | }
143 |
144 | func (x *nezhaServiceIOStreamClient) Recv() (*IOStreamData, error) {
145 | m := new(IOStreamData)
146 | if err := x.ClientStream.RecvMsg(m); err != nil {
147 | return nil, err
148 | }
149 | return m, nil
150 | }
151 |
152 | func (c *nezhaServiceClient) ReportGeoIP(ctx context.Context, in *GeoIP, opts ...grpc.CallOption) (*GeoIP, error) {
153 | out := new(GeoIP)
154 | err := c.cc.Invoke(ctx, NezhaService_ReportGeoIP_FullMethodName, in, out, opts...)
155 | if err != nil {
156 | return nil, err
157 | }
158 | return out, nil
159 | }
160 |
161 | func (c *nezhaServiceClient) ReportSystemInfo2(ctx context.Context, in *Host, opts ...grpc.CallOption) (*Uint64Receipt, error) {
162 | out := new(Uint64Receipt)
163 | err := c.cc.Invoke(ctx, NezhaService_ReportSystemInfo2_FullMethodName, in, out, opts...)
164 | if err != nil {
165 | return nil, err
166 | }
167 | return out, nil
168 | }
169 |
170 | // NezhaServiceServer is the server API for NezhaService service.
171 | // All implementations should embed UnimplementedNezhaServiceServer
172 | // for forward compatibility
173 | type NezhaServiceServer interface {
174 | ReportSystemState(NezhaService_ReportSystemStateServer) error
175 | ReportSystemInfo(context.Context, *Host) (*Receipt, error)
176 | RequestTask(NezhaService_RequestTaskServer) error
177 | IOStream(NezhaService_IOStreamServer) error
178 | ReportGeoIP(context.Context, *GeoIP) (*GeoIP, error)
179 | ReportSystemInfo2(context.Context, *Host) (*Uint64Receipt, error)
180 | }
181 |
182 | // UnimplementedNezhaServiceServer should be embedded to have forward compatible implementations.
183 | type UnimplementedNezhaServiceServer struct {
184 | }
185 |
186 | func (UnimplementedNezhaServiceServer) ReportSystemState(NezhaService_ReportSystemStateServer) error {
187 | return status.Errorf(codes.Unimplemented, "method ReportSystemState not implemented")
188 | }
189 | func (UnimplementedNezhaServiceServer) ReportSystemInfo(context.Context, *Host) (*Receipt, error) {
190 | return nil, status.Errorf(codes.Unimplemented, "method ReportSystemInfo not implemented")
191 | }
192 | func (UnimplementedNezhaServiceServer) RequestTask(NezhaService_RequestTaskServer) error {
193 | return status.Errorf(codes.Unimplemented, "method RequestTask not implemented")
194 | }
195 | func (UnimplementedNezhaServiceServer) IOStream(NezhaService_IOStreamServer) error {
196 | return status.Errorf(codes.Unimplemented, "method IOStream not implemented")
197 | }
198 | func (UnimplementedNezhaServiceServer) ReportGeoIP(context.Context, *GeoIP) (*GeoIP, error) {
199 | return nil, status.Errorf(codes.Unimplemented, "method ReportGeoIP not implemented")
200 | }
201 | func (UnimplementedNezhaServiceServer) ReportSystemInfo2(context.Context, *Host) (*Uint64Receipt, error) {
202 | return nil, status.Errorf(codes.Unimplemented, "method ReportSystemInfo2 not implemented")
203 | }
204 |
205 | // UnsafeNezhaServiceServer may be embedded to opt out of forward compatibility for this service.
206 | // Use of this interface is not recommended, as added methods to NezhaServiceServer will
207 | // result in compilation errors.
208 | type UnsafeNezhaServiceServer interface {
209 | mustEmbedUnimplementedNezhaServiceServer()
210 | }
211 |
212 | func RegisterNezhaServiceServer(s grpc.ServiceRegistrar, srv NezhaServiceServer) {
213 | s.RegisterService(&NezhaService_ServiceDesc, srv)
214 | }
215 |
216 | func _NezhaService_ReportSystemState_Handler(srv interface{}, stream grpc.ServerStream) error {
217 | return srv.(NezhaServiceServer).ReportSystemState(&nezhaServiceReportSystemStateServer{stream})
218 | }
219 |
220 | type NezhaService_ReportSystemStateServer interface {
221 | Send(*Receipt) error
222 | Recv() (*State, error)
223 | grpc.ServerStream
224 | }
225 |
226 | type nezhaServiceReportSystemStateServer struct {
227 | grpc.ServerStream
228 | }
229 |
230 | func (x *nezhaServiceReportSystemStateServer) Send(m *Receipt) error {
231 | return x.ServerStream.SendMsg(m)
232 | }
233 |
234 | func (x *nezhaServiceReportSystemStateServer) Recv() (*State, error) {
235 | m := new(State)
236 | if err := x.ServerStream.RecvMsg(m); err != nil {
237 | return nil, err
238 | }
239 | return m, nil
240 | }
241 |
242 | func _NezhaService_ReportSystemInfo_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
243 | in := new(Host)
244 | if err := dec(in); err != nil {
245 | return nil, err
246 | }
247 | if interceptor == nil {
248 | return srv.(NezhaServiceServer).ReportSystemInfo(ctx, in)
249 | }
250 | info := &grpc.UnaryServerInfo{
251 | Server: srv,
252 | FullMethod: NezhaService_ReportSystemInfo_FullMethodName,
253 | }
254 | handler := func(ctx context.Context, req interface{}) (interface{}, error) {
255 | return srv.(NezhaServiceServer).ReportSystemInfo(ctx, req.(*Host))
256 | }
257 | return interceptor(ctx, in, info, handler)
258 | }
259 |
260 | func _NezhaService_RequestTask_Handler(srv interface{}, stream grpc.ServerStream) error {
261 | return srv.(NezhaServiceServer).RequestTask(&nezhaServiceRequestTaskServer{stream})
262 | }
263 |
264 | type NezhaService_RequestTaskServer interface {
265 | Send(*Task) error
266 | Recv() (*TaskResult, error)
267 | grpc.ServerStream
268 | }
269 |
270 | type nezhaServiceRequestTaskServer struct {
271 | grpc.ServerStream
272 | }
273 |
274 | func (x *nezhaServiceRequestTaskServer) Send(m *Task) error {
275 | return x.ServerStream.SendMsg(m)
276 | }
277 |
278 | func (x *nezhaServiceRequestTaskServer) Recv() (*TaskResult, error) {
279 | m := new(TaskResult)
280 | if err := x.ServerStream.RecvMsg(m); err != nil {
281 | return nil, err
282 | }
283 | return m, nil
284 | }
285 |
286 | func _NezhaService_IOStream_Handler(srv interface{}, stream grpc.ServerStream) error {
287 | return srv.(NezhaServiceServer).IOStream(&nezhaServiceIOStreamServer{stream})
288 | }
289 |
290 | type NezhaService_IOStreamServer interface {
291 | Send(*IOStreamData) error
292 | Recv() (*IOStreamData, error)
293 | grpc.ServerStream
294 | }
295 |
296 | type nezhaServiceIOStreamServer struct {
297 | grpc.ServerStream
298 | }
299 |
300 | func (x *nezhaServiceIOStreamServer) Send(m *IOStreamData) error {
301 | return x.ServerStream.SendMsg(m)
302 | }
303 |
304 | func (x *nezhaServiceIOStreamServer) Recv() (*IOStreamData, error) {
305 | m := new(IOStreamData)
306 | if err := x.ServerStream.RecvMsg(m); err != nil {
307 | return nil, err
308 | }
309 | return m, nil
310 | }
311 |
312 | func _NezhaService_ReportGeoIP_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
313 | in := new(GeoIP)
314 | if err := dec(in); err != nil {
315 | return nil, err
316 | }
317 | if interceptor == nil {
318 | return srv.(NezhaServiceServer).ReportGeoIP(ctx, in)
319 | }
320 | info := &grpc.UnaryServerInfo{
321 | Server: srv,
322 | FullMethod: NezhaService_ReportGeoIP_FullMethodName,
323 | }
324 | handler := func(ctx context.Context, req interface{}) (interface{}, error) {
325 | return srv.(NezhaServiceServer).ReportGeoIP(ctx, req.(*GeoIP))
326 | }
327 | return interceptor(ctx, in, info, handler)
328 | }
329 |
330 | func _NezhaService_ReportSystemInfo2_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
331 | in := new(Host)
332 | if err := dec(in); err != nil {
333 | return nil, err
334 | }
335 | if interceptor == nil {
336 | return srv.(NezhaServiceServer).ReportSystemInfo2(ctx, in)
337 | }
338 | info := &grpc.UnaryServerInfo{
339 | Server: srv,
340 | FullMethod: NezhaService_ReportSystemInfo2_FullMethodName,
341 | }
342 | handler := func(ctx context.Context, req interface{}) (interface{}, error) {
343 | return srv.(NezhaServiceServer).ReportSystemInfo2(ctx, req.(*Host))
344 | }
345 | return interceptor(ctx, in, info, handler)
346 | }
347 |
348 | // NezhaService_ServiceDesc is the grpc.ServiceDesc for NezhaService service.
349 | // It's only intended for direct use with grpc.RegisterService,
350 | // and not to be introspected or modified (even as a copy)
351 | var NezhaService_ServiceDesc = grpc.ServiceDesc{
352 | ServiceName: "proto.NezhaService",
353 | HandlerType: (*NezhaServiceServer)(nil),
354 | Methods: []grpc.MethodDesc{
355 | {
356 | MethodName: "ReportSystemInfo",
357 | Handler: _NezhaService_ReportSystemInfo_Handler,
358 | },
359 | {
360 | MethodName: "ReportGeoIP",
361 | Handler: _NezhaService_ReportGeoIP_Handler,
362 | },
363 | {
364 | MethodName: "ReportSystemInfo2",
365 | Handler: _NezhaService_ReportSystemInfo2_Handler,
366 | },
367 | },
368 | Streams: []grpc.StreamDesc{
369 | {
370 | StreamName: "ReportSystemState",
371 | Handler: _NezhaService_ReportSystemState_Handler,
372 | ServerStreams: true,
373 | ClientStreams: true,
374 | },
375 | {
376 | StreamName: "RequestTask",
377 | Handler: _NezhaService_RequestTask_Handler,
378 | ServerStreams: true,
379 | ClientStreams: true,
380 | },
381 | {
382 | StreamName: "IOStream",
383 | Handler: _NezhaService_IOStream_Handler,
384 | ServerStreams: true,
385 | ClientStreams: true,
386 | },
387 | },
388 | Metadata: "proto/nezha.proto",
389 | }
390 |
--------------------------------------------------------------------------------
/go.sum:
--------------------------------------------------------------------------------
1 | cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
2 | gitee.com/naibahq/go-gitee v0.0.0-20240713052758-bc992e4c5b2c h1:rFMPP1jR4CIOcxU2MHTFHKtXLPqV+qD4VPjRnpA6Inw=
3 | gitee.com/naibahq/go-gitee v0.0.0-20240713052758-bc992e4c5b2c/go.mod h1:9gFPAuMAO9HJv5W73eoLV1NX71Ko5MhzGe+NwOJkm24=
4 | github.com/AlecAivazis/survey/v2 v2.3.7 h1:6I/u8FvytdGsgonrYsVn2t8t4QiRnh6QSTqkkhIiSjQ=
5 | github.com/AlecAivazis/survey/v2 v2.3.7/go.mod h1:xUTIdE4KCOIjsBAE1JYsUPoCqYdZ1reCfTwbto0Fduo=
6 | github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2 h1:+vx7roKuyA63nhn5WAunQHLTznkw5W8b1Xc0dNjp83s=
7 | github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2/go.mod h1:HBCaDeC1lPdgDeDbhX8XFpy1jqjK0IBG8W5K+xYqA0w=
8 | github.com/StackExchange/wmi v1.2.1 h1:VIkavFPXSjcnS+O8yTq7NI32k0R5Aj+v39y29VYDOSA=
9 | github.com/StackExchange/wmi v1.2.1/go.mod h1:rcmrprowKIVzvc+NUiLncP2uuArMWLCbu9SBzvHz7e8=
10 | github.com/UserExistsError/conpty v0.1.4 h1:+3FhJhiqhyEJa+K5qaK3/w6w+sN3Nh9O9VbJyBS02to=
11 | github.com/UserExistsError/conpty v0.1.4/go.mod h1:PDglKIkX3O/2xVk0MV9a6bCWxRmPVfxqZoTG/5sSd9I=
12 | github.com/andybalholm/brotli v1.0.6 h1:Yf9fFpf49Zrxb9NlQaluyE92/+X7UVHlhMNJN2sxfOI=
13 | github.com/andybalholm/brotli v1.0.6/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
14 | github.com/antihax/optional v1.0.0 h1:xK2lYat7ZLaVVcIuj82J8kIro4V6kDe0AUDFboUCwcg=
15 | github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
16 | github.com/artdarek/go-unzip v1.0.0 h1:Ja9wfhiXyl67z5JT37rWjTSb62KXDP+9jHRkdSREUvg=
17 | github.com/artdarek/go-unzip v1.0.0/go.mod h1:KhX4LV7e4UwWCTo7orBYnJ6LJ/dZTI6jXxUg69hO/C8=
18 | github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdnnjpJbkM4JQ=
19 | github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk=
20 | github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
21 | github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
22 | github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
23 | github.com/cloudflare/ahocorasick v0.0.0-20240916140611-054963ec9396 h1:W2HK1IdCnCGuLUeyizSCkwvBjdj0ZL7mxnJYQ3poyzI=
24 | github.com/cloudflare/ahocorasick v0.0.0-20240916140611-054963ec9396/go.mod h1:tGWUZLZp9ajsxUOnHmFFLnqnlKXsCn6GReG4jAD59H0=
25 | github.com/cloudflare/circl v1.3.7 h1:qlCDlTPz2n9fu58M0Nh1J/JzcFpfgkFHHX3O35r5vcU=
26 | github.com/cloudflare/circl v1.3.7/go.mod h1:sRTcRWXGLrKw6yIGJ+l7amYJFfAXbZG0kBSc8r4zxgA=
27 | github.com/cpuguy83/go-md2man/v2 v2.0.5 h1:ZtcqGrnekaHpVLArFSe4HK5DoKx1T0rq2DwVB0alcyc=
28 | github.com/cpuguy83/go-md2man/v2 v2.0.5/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
29 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
30 | github.com/creack/pty v1.1.17/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
31 | github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s=
32 | github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE=
33 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
34 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
35 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
36 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
37 | github.com/dean2021/goss v0.0.0-20230129073947-df90431348f1 h1:5UiJ324LiCdOF/3w/5IeXrKVjdnwHoalvLG2smb3wi4=
38 | github.com/dean2021/goss v0.0.0-20230129073947-df90431348f1/go.mod h1:NiLueuVb3hYcdF4ta+2ezcKJh6BEjhrBz9Hts6XJ5Sc=
39 | github.com/ebi-yade/altsvc-go v0.1.1 h1:HmZDNb5ZOPlkyXhi34LnRckawFCux7yPYw+dtInIixo=
40 | github.com/ebi-yade/altsvc-go v0.1.1/go.mod h1:K/U20bLcsOVrbTeDhqRjp+e3tgNT5iAqSiQzPoU0/Q0=
41 | github.com/ebitengine/purego v0.8.1 h1:sdRKd6plj7KYW33EH5As6YKfe8m9zbN9JMrOjNVF/BE=
42 | github.com/ebitengine/purego v0.8.1/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
43 | github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
44 | github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
45 | github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk=
46 | github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
47 | github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ=
48 | github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
49 | github.com/go-ole/go-ole v1.2.5/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
50 | github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=
51 | github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
52 | github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI=
53 | github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls=
54 | github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss=
55 | github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
56 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
57 | github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
58 | github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
59 | github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
60 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
61 | github.com/google/go-github v17.0.0+incompatible h1:N0LgJ1j65A7kfXrZnUDaYCs/Sf4rEjNlfyDHW9dolSY=
62 | github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ=
63 | github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
64 | github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
65 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
66 | github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 h1:yAJXTCF9TqKcTiHJAE8dj7HMvPfh66eeA2JYW7eFpSE=
67 | github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
68 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
69 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
70 | github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8=
71 | github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
72 | github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec h1:qv2VnGeEQHchGaZ/u7lxST/RaJw+cv273q79D81Xbog=
73 | github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec/go.mod h1:Q48J4R4DvxnHolD5P8pOtXigYlRuPLGl6moFx3ulM68=
74 | github.com/iamacarpet/go-winpty v1.0.4 h1:r42xaLLRZcUqjX6vHZeHos2haACfWkooOJTnFdogBfI=
75 | github.com/iamacarpet/go-winpty v1.0.4/go.mod h1:50yLtqN2hFb5sYO5Qm2LegB166oAEw/PZYRn+FPWj0Q=
76 | github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
77 | github.com/inconshreveable/go-update v0.0.0-20160112193335-8152e7eb6ccf h1:WfD7VjIE6z8dIvMsI4/s+1qr5EL+zoIGev1BQj1eoJ8=
78 | github.com/inconshreveable/go-update v0.0.0-20160112193335-8152e7eb6ccf/go.mod h1:hyb9oH7vZsitZCiBt0ZvifOrB+qc8PS5IiilCIb87rg=
79 | github.com/jaypipes/ghw v0.12.0 h1:xU2/MDJfWmBhJnujHY9qwXQLs3DBsf0/Xa9vECY0Tho=
80 | github.com/jaypipes/ghw v0.12.0/go.mod h1:jeJGbkRB2lL3/gxYzNYzEDETV1ZJ56OKr+CSeSEym+g=
81 | github.com/jaypipes/ghw v0.13.0 h1:log8MXuB8hzTNnSktqpXMHc0c/2k/WgjOMSUtnI1RV4=
82 | github.com/jaypipes/ghw v0.13.0/go.mod h1:In8SsaDqlb1oTyrbmTC14uy+fbBMvp+xdqX51MidlD8=
83 | github.com/jaypipes/pcidb v1.0.0 h1:vtZIfkiCUE42oYbJS0TAq9XSfSmcsgo9IdxSm9qzYU8=
84 | github.com/jaypipes/pcidb v1.0.0/go.mod h1:TnYUvqhPBzCKnH34KrIX22kAeEbDCSRJ9cqLRCuNDfk=
85 | github.com/jaypipes/pcidb v1.0.1 h1:WB2zh27T3nwg8AE8ei81sNRb9yWBii3JGNJtT7K9Oic=
86 | github.com/jaypipes/pcidb v1.0.1/go.mod h1:6xYUz/yYEyOkIkUt2t2J2folIuZ4Yg6uByCGFXMCeE4=
87 | github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
88 | github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
89 | github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
90 | github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs=
91 | github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
92 | github.com/klauspost/compress v1.17.4 h1:Ej5ixsIri7BrIjBkRZLTo6ghwrEtHFk7ijlczPW4fZ4=
93 | github.com/klauspost/compress v1.17.4/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM=
94 | github.com/knadh/koanf/maps v0.1.1 h1:G5TjmUh2D7G2YWf5SQQqSiHRJEjaicvU0KpypqB3NIs=
95 | github.com/knadh/koanf/maps v0.1.1/go.mod h1:npD/QZY3V6ghQDdcQzl1W4ICNVTkohC8E73eI2xW4yI=
96 | github.com/knadh/koanf/providers/env v1.0.0 h1:ufePaI9BnWH+ajuxGGiJ8pdTG0uLEUWC7/HDDPGLah0=
97 | github.com/knadh/koanf/providers/env v1.0.0/go.mod h1:mzFyRZueYhb37oPmC1HAv/oGEEuyvJDA98r3XAa8Gak=
98 | github.com/knadh/koanf/providers/file v1.1.2 h1:aCC36YGOgV5lTtAFz2qkgtWdeQsgfxUkxDOe+2nQY3w=
99 | github.com/knadh/koanf/providers/file v1.1.2/go.mod h1:/faSBcv2mxPVjFrXck95qeoyoZ5myJ6uxN8OOVNJJCI=
100 | github.com/knadh/koanf/v2 v2.1.2 h1:I2rtLRqXRy1p01m/utEtpZSSA6dcJbgGVuE27kW2PzQ=
101 | github.com/knadh/koanf/v2 v2.1.2/go.mod h1:Gphfaen0q1Fc1HTgJgSTC4oRX9R2R5ErYMZJy8fLJBo=
102 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
103 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
104 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
105 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
106 | github.com/lufia/plan9stats v0.0.0-20230110061619-bbe2e5e100de h1:V53FWzU6KAZVi1tPp5UIsMoUWJ2/PNwYIDXnu7QuBCE=
107 | github.com/lufia/plan9stats v0.0.0-20230110061619-bbe2e5e100de/go.mod h1:JKx41uQRwqlTZabZc+kILPrO/3jlKnQ2Z8b7YiVw5cE=
108 | github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
109 | github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
110 | github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
111 | github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
112 | github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
113 | github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng=
114 | github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
115 | github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
116 | github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d h1:5PJl274Y63IEHC+7izoQE9x6ikvDFZS2mDVS3drnohI=
117 | github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
118 | github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw=
119 | github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s=
120 | github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
121 | github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
122 | github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ=
123 | github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
124 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
125 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
126 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
127 | github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
128 | github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
129 | github.com/nezhahq/go-github-selfupdate v0.0.0-20241205090552-0b56e412e750 h1:T0APK/dSR1CgIYYIQmobxYh+gRfEUuD8ZCiflGL+8lI=
130 | github.com/nezhahq/go-github-selfupdate v0.0.0-20241205090552-0b56e412e750/go.mod h1:NVxyW3iSQ7YII8vvCIBTzk4HzsAa08Iibb68e0i0Nww=
131 | github.com/nezhahq/service v0.0.0-20241205090409-40f63a48da4e h1:8g5mO3T5anFNngq/2rYB4dPHi/K4Jff/mrE+iENjLOM=
132 | github.com/nezhahq/service v0.0.0-20241205090409-40f63a48da4e/go.mod h1:i6zO7Vzuv5+mdaCzHrvAC4U63W59uXmX9n6o7p4PJGk=
133 | github.com/onsi/ginkgo/v2 v2.9.5 h1:+6Hr4uxzP4XIUyAkg61dWBw8lb/gc4/X5luuxN/EC+Q=
134 | github.com/onsi/ginkgo/v2 v2.9.5/go.mod h1:tvAoo1QUJwNEU2ITftXTpR7R1RbCzoZUOs3RonqW57k=
135 | github.com/onsi/gomega v1.27.6 h1:ENqfyGeS5AX/rlXDd/ETokDz93u0YufY1Pgxuy/PvWE=
136 | github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
137 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
138 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
139 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
140 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
141 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
142 | github.com/power-devops/perfstat v0.0.0-20221212215047-62379fc7944b h1:0LFwY6Q3gMACTjAbMZBjXAqTOzOwFaj2Ld6cjeQ7Rig=
143 | github.com/power-devops/perfstat v0.0.0-20221212215047-62379fc7944b/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
144 | github.com/prometheus-community/pro-bing v0.4.1 h1:aMaJwyifHZO0y+h8+icUz0xbToHbia0wdmzdVZ+Kl3w=
145 | github.com/prometheus-community/pro-bing v0.4.1/go.mod h1:aLsw+zqCaDoa2RLVVSX3+UiCkBBXTMtZC3c7EkfWnAE=
146 | github.com/prometheus-community/pro-bing v0.5.0 h1:Fq+4BUXKIvsPtXUY8K+04ud9dkAuFozqGmRAyNUpffY=
147 | github.com/prometheus-community/pro-bing v0.5.0/go.mod h1:1joR9oXdMEAcAJJvhs+8vNDvTg5thfAZcRFhcUozG2g=
148 | github.com/quic-go/qpack v0.4.0 h1:Cr9BXA1sQS2SmDUWjSofMPNKmvF6IiIfDRmgU0w1ZCo=
149 | github.com/quic-go/qpack v0.4.0/go.mod h1:UZVnYIfi5GRk+zI9UMaCPsmZ2xKJP7XBUvVyT1Knj9A=
150 | github.com/quic-go/qtls-go1-20 v0.4.1 h1:D33340mCNDAIKBqXuAvexTNMUByrYmFYVfKfDN5nfFs=
151 | github.com/quic-go/qtls-go1-20 v0.4.1/go.mod h1:X9Nh97ZL80Z+bX/gUXMbipO6OxdiDi58b/fMC9mAL+k=
152 | github.com/quic-go/quic-go v0.40.1 h1:X3AGzUNFs0jVuO3esAGnTfvdgvL4fq655WaOi1snv1Q=
153 | github.com/quic-go/quic-go v0.40.1/go.mod h1:PeN7kuVJ4xZbxSv/4OX6S1USOX8MJvydwpTx31vx60c=
154 | github.com/refraction-networking/utls v1.6.3 h1:MFOfRN35sSx6K5AZNIoESsBuBxS2LCgRilRIdHb6fDc=
155 | github.com/refraction-networking/utls v1.6.3/go.mod h1:yil9+7qSl+gBwJqztoQseO6Pr3h62pQoY1lXiNR/FPs=
156 | github.com/refraction-networking/utls v1.6.7 h1:zVJ7sP1dJx/WtVuITug3qYUq034cDq9B2MR1K67ULZM=
157 | github.com/refraction-networking/utls v1.6.7/go.mod h1:BC3O4vQzye5hqpmDTWUqi4P5DDhzJfkV1tdqtawQIH0=
158 | github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
159 | github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
160 | github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
161 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
162 | github.com/shirou/gopsutil/v4 v4.24.10 h1:7VOzPtfw/5YDU+jLEoBwXwxJbQetULywoSV4RYY7HkM=
163 | github.com/shirou/gopsutil/v4 v4.24.10/go.mod h1:s4D/wg+ag4rG0WO7AiTj2BeYCRhym0vM7DHbZRxnIT8=
164 | github.com/shirou/gopsutil/v4 v4.24.11 h1:WaU9xqGFKvFfsUv94SXcUPD7rCkU0vr/asVdQOBZNj8=
165 | github.com/shirou/gopsutil/v4 v4.24.11/go.mod h1:s4D/wg+ag4rG0WO7AiTj2BeYCRhym0vM7DHbZRxnIT8=
166 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
167 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
168 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
169 | github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
170 | github.com/tcnksm/go-gitconfig v0.1.2 h1:iiDhRitByXAEyjgBqsKi9QU4o2TNtv9kPP3RgPgXBPw=
171 | github.com/tcnksm/go-gitconfig v0.1.2/go.mod h1:/8EhP4H7oJZdIPyT+/UIsG87kTzrzM4UsLGSItWYCpE=
172 | github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY=
173 | github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
174 | github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
175 | github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
176 | github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs=
177 | github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
178 | github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU=
179 | github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI=
180 | github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk=
181 | github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY=
182 | github.com/ulikunitz/xz v0.5.11 h1:kpFauv27b6ynzBNT/Xy+1k+fK4WswhN/6PN5WhFAGw8=
183 | github.com/ulikunitz/xz v0.5.11/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
184 | github.com/urfave/cli/v2 v2.27.5 h1:WoHEJLdsXr6dDWoJgMq/CboDmyY/8HMMH1fTECbih+w=
185 | github.com/urfave/cli/v2 v2.27.5/go.mod h1:3Sevf16NykTbInEnD0yKkjDAeZDS0A6bzhBH5hrMvTQ=
186 | github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4=
187 | github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM=
188 | github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
189 | github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
190 | github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
191 | go.uber.org/mock v0.4.0 h1:VcM4ZOtdbR4f6VXfiOpwpVJDL6lCReaZ6mw31wqh7KU=
192 | go.uber.org/mock v0.4.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc=
193 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
194 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
195 | golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A=
196 | golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70=
197 | golang.org/x/crypto v0.29.0/go.mod h1:+F4F4N5hv6v38hfeYwTdx20oUvLLc+QfrE9Ax9HtgRg=
198 | golang.org/x/crypto v0.30.0 h1:RwoQn3GkWiMkzlX562cLB7OxWvjH1L8xutO2WoJcRoY=
199 | golang.org/x/crypto v0.30.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
200 | golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g=
201 | golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k=
202 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
203 | golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA=
204 | golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
205 | golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
206 | golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
207 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
208 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
209 | golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
210 | golang.org/x/net v0.29.0 h1:5ORfpBpCs4HzDYoodCDBbwHzdR5UrLBZ3sOnUJmFoHo=
211 | golang.org/x/net v0.29.0/go.mod h1:gLkgy8jTGERgjzMic6DS9+SP0ajcu6Xu3Orq/SpETg0=
212 | golang.org/x/net v0.31.0 h1:68CPQngjLL0r2AlUKiSxtQFKvzRVbnzLwMUn5SzcLHo=
213 | golang.org/x/net v0.31.0/go.mod h1:P4fl1q7dY2hnZFxEk4pPSkDHF+QqjitcnDjUQyMM+pM=
214 | golang.org/x/net v0.32.0 h1:ZqPmj8Kzc+Y6e0+skZsuACbx+wzMgo5MQsJh9Qd6aYI=
215 | golang.org/x/net v0.32.0/go.mod h1:CwU0IoeOlnQQWJ6ioyFrfRuomB8GKF6KbYXZVyeXNfs=
216 | golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
217 | golang.org/x/oauth2 v0.20.0 h1:4mQdhULixXKP1rwYBW0vAijoXnkTG0BLCDRzfe1idMo=
218 | golang.org/x/oauth2 v0.20.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
219 | golang.org/x/oauth2 v0.23.0 h1:PbgcYx2W7i4LvjJWEbf0ngHV6qJYr86PkAV3bXdLEbs=
220 | golang.org/x/oauth2 v0.23.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
221 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
222 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
223 | golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
224 | golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
225 | golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
226 | golang.org/x/sync v0.9.0 h1:fEo0HyrW1GIgZdpbhCRO0PkJajUS5H9IFUztCgEo2jQ=
227 | golang.org/x/sync v0.9.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
228 | golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
229 | golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
230 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
231 | golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
232 | golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
233 | golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
234 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
235 | golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
236 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
237 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
238 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
239 | golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
240 | golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
241 | golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
242 | golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
243 | golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
244 | golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
245 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
246 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
247 | golang.org/x/term v0.24.0 h1:Mh5cbb+Zk2hqqXNO7S1iTjEphVL+jb8ZWaqh/g+JWkM=
248 | golang.org/x/term v0.24.0/go.mod h1:lOBK/LVxemqiMij05LGJ0tzNr8xlmwBRJ81PX6wVLH8=
249 | golang.org/x/term v0.26.0 h1:WEQa6V3Gja/BhNxg540hBip/kkaYtRg3cxg4oXSw4AU=
250 | golang.org/x/term v0.26.0/go.mod h1:Si5m1o57C5nBNQo5z1iq+XDijt21BDBDp2bK0QI8e3E=
251 | golang.org/x/term v0.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q=
252 | golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
253 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
254 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
255 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
256 | golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
257 | golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224=
258 | golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
259 | golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4=
260 | golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
261 | golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
262 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
263 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
264 | golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
265 | golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg=
266 | golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
267 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
268 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
269 | google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
270 | google.golang.org/genproto/googleapis/rpc v0.0.0-20240528184218-531527333157 h1:Zy9XzmMEflZ/MAaA7vNcoebnRAld7FsPW1EeBB7V0m8=
271 | google.golang.org/genproto/googleapis/rpc v0.0.0-20240528184218-531527333157/go.mod h1:EfXuqaE1J41VCDicxHzUDm+8rk+7ZdXzHV0IhO/I6s0=
272 | google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 h1:pPJltXNxVzT4pK9yD8vR9X75DaWYYmLGMsEvBfFQZzQ=
273 | google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU=
274 | google.golang.org/grpc v1.64.1 h1:LKtvyfbX3UGVPFcGqJ9ItpVWW6oN/2XqTxfAnwRRXiA=
275 | google.golang.org/grpc v1.64.1/go.mod h1:hiQF4LFZelK2WKaP6W0L92zGHtiQdZxk8CrSdvyjeP0=
276 | google.golang.org/grpc v1.68.1 h1:oI5oTa11+ng8r8XMMN7jAOmWfPZWbYpCFaMUTACxkM0=
277 | google.golang.org/grpc v1.68.1/go.mod h1:+q1XYFJjShcqn0QZHvCyeR4CXPA+llXIeUIfIe00waw=
278 | google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
279 | google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
280 | google.golang.org/protobuf v1.35.2 h1:8Ar7bF+apOIoThw1EdZl0p1oWvMqTHmpA2fRTyZO8io=
281 | google.golang.org/protobuf v1.35.2/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
282 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
283 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
284 | gopkg.in/yaml.v1 v1.0.0-20140924161607-9f9df34309c0/go.mod h1:WDnlLJ4WF5VGsH/HVa3CI79GS0ol3YnhVnKP89i0kNg=
285 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
286 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
287 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
288 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
289 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
290 | howett.net/plist v1.0.0 h1:7CrbWYbPPO/PyNy38b2EB/+gYbjCe2DXBxgtOOZbSQM=
291 | howett.net/plist v1.0.0/go.mod h1:lqaXoTrLY4hg8tnEzNru53gicrbv7rrk+2xJA/7hw9g=
292 | sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E=
293 | sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY=
294 |
--------------------------------------------------------------------------------
/cmd/agent/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "bytes"
5 | "context"
6 | "crypto/md5"
7 | "crypto/tls"
8 | "errors"
9 | "fmt"
10 | "io"
11 | "log"
12 | "math/rand"
13 | "net"
14 | "net/http"
15 | "net/url"
16 | "os"
17 | "path/filepath"
18 | "runtime"
19 | "strings"
20 | "sync/atomic"
21 | "time"
22 |
23 | "github.com/blang/semver"
24 | "github.com/ebi-yade/altsvc-go"
25 | "github.com/nezhahq/go-github-selfupdate/selfupdate"
26 | "github.com/nezhahq/service"
27 | ping "github.com/prometheus-community/pro-bing"
28 | "github.com/quic-go/quic-go/http3"
29 | utls "github.com/refraction-networking/utls"
30 | "github.com/shirou/gopsutil/v4/host"
31 | "github.com/urfave/cli/v2"
32 | "google.golang.org/grpc"
33 | "google.golang.org/grpc/credentials"
34 | "google.golang.org/grpc/credentials/insecure"
35 | "google.golang.org/grpc/resolver"
36 |
37 | "github.com/nezhahq/agent/cmd/agent/commands"
38 | "github.com/nezhahq/agent/model"
39 | fm "github.com/nezhahq/agent/pkg/fm"
40 | "github.com/nezhahq/agent/pkg/logger"
41 | "github.com/nezhahq/agent/pkg/monitor"
42 | "github.com/nezhahq/agent/pkg/processgroup"
43 | "github.com/nezhahq/agent/pkg/pty"
44 | "github.com/nezhahq/agent/pkg/util"
45 | utlsx "github.com/nezhahq/agent/pkg/utls"
46 | pb "github.com/nezhahq/agent/proto"
47 | )
48 |
49 | var (
50 | version = monitor.Version // 来自于 GoReleaser 的版本号
51 | arch string
52 | executablePath string
53 | defaultConfigPath = loadDefaultConfigPath()
54 | client pb.NezhaServiceClient
55 | initialized bool
56 | agentConfig model.AgentConfig
57 | prevDashboardBootTime uint64 // 面板上次启动时间
58 | geoipReported bool // 在面板重启后是否上报成功过 GeoIP
59 | lastReportHostInfo time.Time
60 | lastReportIPInfo time.Time
61 |
62 | hostStatus = new(atomic.Bool)
63 | ipStatus = new(atomic.Bool)
64 |
65 | dnsResolver = &net.Resolver{PreferGo: true}
66 | httpClient = &http.Client{
67 | CheckRedirect: func(req *http.Request, via []*http.Request) error {
68 | return http.ErrUseLastResponse
69 | },
70 | Timeout: time.Second * 30,
71 | }
72 | httpClient3 = &http.Client{
73 | CheckRedirect: func(req *http.Request, via []*http.Request) error {
74 | return http.ErrUseLastResponse
75 | },
76 | Timeout: time.Second * 30,
77 | Transport: &http3.RoundTripper{},
78 | }
79 | )
80 |
81 | var (
82 | println = logger.DefaultLogger.Println
83 | printf = logger.DefaultLogger.Printf
84 | )
85 |
86 | const (
87 | delayWhenError = time.Second * 10 // Agent 重连间隔
88 | networkTimeOut = time.Second * 5 // 普通网络超时
89 |
90 | minUpdateInterval = 30
91 | maxUpdateInterval = 90
92 |
93 | binaryName = "nezha-agent"
94 | )
95 |
96 | func setEnv() {
97 | resolver.SetDefaultScheme("passthrough")
98 | net.DefaultResolver.PreferGo = true // 使用 Go 内置的 DNS 解析器解析域名
99 | net.DefaultResolver.Dial = func(ctx context.Context, network, address string) (net.Conn, error) {
100 | d := net.Dialer{
101 | Timeout: time.Second * 5,
102 | }
103 | dnsServers := util.DNSServersAll
104 | if len(agentConfig.DNS) > 0 {
105 | dnsServers = agentConfig.DNS
106 | }
107 | index := int(time.Now().Unix()) % int(len(dnsServers))
108 | var conn net.Conn
109 | var err error
110 | for i := 0; i < len(dnsServers); i++ {
111 | conn, err = d.DialContext(ctx, "udp", dnsServers[util.RotateQueue1(index, i, len(dnsServers))])
112 | if err == nil {
113 | return conn, nil
114 | }
115 | }
116 | return nil, err
117 | }
118 | headers := util.BrowserHeaders()
119 | http.DefaultClient.Timeout = time.Second * 30
120 | httpClient.Transport = utlsx.NewUTLSHTTPRoundTripperWithProxy(
121 | utls.HelloChrome_Auto, new(utls.Config),
122 | http.DefaultTransport, nil, &headers,
123 | )
124 | }
125 |
126 | func loadDefaultConfigPath() string {
127 | var err error
128 | executablePath, err = os.Executable()
129 | if err != nil {
130 | return ""
131 | }
132 | return filepath.Join(filepath.Dir(executablePath), "config.yml")
133 | }
134 |
135 | func preRun(configPath string) error {
136 | // init
137 | setEnv()
138 |
139 | if configPath == "" {
140 | configPath = defaultConfigPath
141 | }
142 |
143 | // windows环境处理
144 | if runtime.GOOS == "windows" {
145 | hostArch, err := host.KernelArch()
146 | if err != nil {
147 | return err
148 | }
149 | switch hostArch {
150 | case "i386", "i686":
151 | hostArch = "386"
152 | case "x86_64":
153 | hostArch = "amd64"
154 | case "aarch64":
155 | hostArch = "arm64"
156 | }
157 | if arch != hostArch {
158 | return fmt.Errorf("与当前系统不匹配,当前运行 %s_%s, 需要下载 %s_%s", runtime.GOOS, arch, runtime.GOOS, hostArch)
159 | }
160 | }
161 |
162 | if err := agentConfig.Read(configPath); err != nil {
163 | return fmt.Errorf("init config failed: %v", err)
164 | }
165 |
166 | monitor.InitConfig(&agentConfig)
167 | monitor.CustomEndpoints = agentConfig.CustomIPApi
168 |
169 | return nil
170 | }
171 |
172 | func main() {
173 | app := &cli.App{
174 | Usage: "哪吒监控 Agent",
175 | Version: version,
176 | Flags: []cli.Flag{
177 | &cli.StringFlag{Name: "config", Aliases: []string{"c"}, Usage: "配置文件路径"},
178 | },
179 | Action: func(c *cli.Context) error {
180 | if path := c.String("config"); path != "" {
181 | if err := preRun(path); err != nil {
182 | return err
183 | }
184 | } else {
185 | if err := preRun(""); err != nil {
186 | return err
187 | }
188 | }
189 | runService("", "")
190 | return nil
191 | },
192 | Commands: []*cli.Command{
193 | {
194 | Name: "edit",
195 | Usage: "编辑配置文件",
196 | Flags: []cli.Flag{
197 | &cli.StringFlag{Name: "config", Aliases: []string{"c"}, Usage: "配置文件路径"},
198 | },
199 | Action: func(c *cli.Context) error {
200 | if path := c.String("config"); path != "" {
201 | commands.EditAgentConfig(path, &agentConfig)
202 | } else {
203 | commands.EditAgentConfig(defaultConfigPath, &agentConfig)
204 | }
205 | return nil
206 | },
207 | },
208 | {
209 | Name: "service",
210 | Usage: "服务操作",
211 | UsageText: "",
212 | Flags: []cli.Flag{
213 | &cli.StringFlag{Name: "config", Aliases: []string{"c"}, Usage: "配置文件路径"},
214 | },
215 | Action: func(c *cli.Context) error {
216 | if arg := c.Args().Get(0); arg != "" {
217 | if path := c.String("config"); path != "" {
218 | ap, _ := filepath.Abs(path)
219 | runService(arg, ap)
220 | } else {
221 | ap, _ := filepath.Abs(defaultConfigPath)
222 | runService(arg, ap)
223 | }
224 | return nil
225 | }
226 | return cli.Exit("必须指定一个参数", 1)
227 | },
228 | },
229 | },
230 | }
231 |
232 | if err := app.Run(os.Args); err != nil {
233 | log.Fatal(err)
234 | }
235 | }
236 |
237 | func run() {
238 | auth := model.AuthHandler{
239 | ClientSecret: agentConfig.ClientSecret,
240 | ClientUUID: agentConfig.UUID,
241 | }
242 |
243 | // 下载远程命令执行需要的终端
244 | if !agentConfig.DisableCommandExecute {
245 | go func() {
246 | if err := pty.DownloadDependency(); err != nil {
247 | printf("pty 下载依赖失败: %v", err)
248 | }
249 | }()
250 | }
251 |
252 | // 定时检查更新
253 | if _, err := semver.Parse(version); err == nil && !agentConfig.DisableAutoUpdate {
254 | doSelfUpdate(true)
255 | go func() {
256 | var interval time.Duration
257 | if agentConfig.SelfUpdatePeriod > 0 {
258 | interval = time.Duration(agentConfig.SelfUpdatePeriod) * time.Minute
259 | } else {
260 | interval = time.Duration(rand.Intn(maxUpdateInterval-minUpdateInterval)+minUpdateInterval) * time.Minute
261 | }
262 | for range time.Tick(interval) {
263 | doSelfUpdate(true)
264 | }
265 | }()
266 | }
267 |
268 | var err error
269 | var dashboardBootTimeReceipt *pb.Uint64Receipt
270 | var conn *grpc.ClientConn
271 |
272 | retry := func() {
273 | initialized = false
274 | println("Error to close connection ...")
275 | if conn != nil {
276 | conn.Close()
277 | }
278 | time.Sleep(delayWhenError)
279 | println("Try to reconnect ...")
280 | }
281 |
282 | for {
283 | var securityOption grpc.DialOption
284 | if agentConfig.TLS {
285 | if agentConfig.InsecureTLS {
286 | securityOption = grpc.WithTransportCredentials(credentials.NewTLS(&tls.Config{MinVersion: tls.VersionTLS12, InsecureSkipVerify: true}))
287 | } else {
288 | securityOption = grpc.WithTransportCredentials(credentials.NewTLS(&tls.Config{MinVersion: tls.VersionTLS12}))
289 | }
290 | } else {
291 | securityOption = grpc.WithTransportCredentials(insecure.NewCredentials())
292 | }
293 | conn, err = grpc.NewClient(agentConfig.Server, securityOption, grpc.WithPerRPCCredentials(&auth))
294 | if err != nil {
295 | printf("与面板建立连接失败: %v", err)
296 | retry()
297 | continue
298 | }
299 | client = pb.NewNezhaServiceClient(conn)
300 |
301 | timeOutCtx, cancel := context.WithTimeout(context.Background(), networkTimeOut)
302 | dashboardBootTimeReceipt, err = client.ReportSystemInfo2(timeOutCtx, monitor.GetHost().PB())
303 | if err != nil {
304 | printf("上报系统信息失败: %v", err)
305 | cancel()
306 | retry()
307 | continue
308 | }
309 | cancel()
310 | geoipReported = geoipReported && prevDashboardBootTime > 0 && dashboardBootTimeReceipt.GetData() == prevDashboardBootTime
311 | prevDashboardBootTime = dashboardBootTimeReceipt.GetData()
312 | initialized = true
313 |
314 | errCh := make(chan error)
315 |
316 | // 执行 Task
317 | tasks, err := client.RequestTask(context.Background())
318 | if err != nil {
319 | printf("请求任务失败: %v", err)
320 | retry()
321 | continue
322 | }
323 | go receiveTasksDaemon(tasks, errCh)
324 |
325 | reportState, err := client.ReportSystemState(context.Background())
326 | if err != nil {
327 | printf("上报状态信息失败: %v", err)
328 | retry()
329 | continue
330 | }
331 | go reportStateDaemon(reportState, errCh)
332 |
333 | for i := 0; i < 2; i++ {
334 | err = <-errCh
335 | if i == 0 {
336 | tasks.CloseSend()
337 | reportState.CloseSend()
338 | }
339 | printf("worker exit to main: %v", err)
340 | }
341 | close(errCh)
342 |
343 | retry()
344 | }
345 | }
346 |
347 | func runService(action string, path string) {
348 | winConfig := map[string]interface{}{
349 | "OnFailure": "restart",
350 | }
351 |
352 | args := []string{"-c", path}
353 | name := filepath.Base(executablePath)
354 | if path != defaultConfigPath && path != "" {
355 | hex := fmt.Sprintf("%x", md5.Sum([]byte(path)))[:7]
356 | name = fmt.Sprintf("%s-%s", name, hex)
357 | }
358 |
359 | svcConfig := &service.Config{
360 | Name: name,
361 | DisplayName: filepath.Base(executablePath),
362 | Arguments: args,
363 | Description: "哪吒监控 Agent",
364 | WorkingDirectory: filepath.Dir(executablePath),
365 | Option: winConfig,
366 | }
367 |
368 | prg := &commands.Program{
369 | Exit: make(chan struct{}),
370 | Run: run,
371 | }
372 | s, err := service.New(prg, svcConfig)
373 | if err != nil {
374 | printf("创建服务时出错,以普通模式运行: %v", err)
375 | run()
376 | return
377 | }
378 | prg.Service = s
379 |
380 | serviceLogger, err := s.Logger(nil)
381 | if err != nil {
382 | printf("获取 service logger 时出错: %+v", err)
383 | logger.InitDefaultLogger(agentConfig.Debug, service.ConsoleLogger)
384 | } else {
385 | logger.InitDefaultLogger(agentConfig.Debug, serviceLogger)
386 | }
387 |
388 | if action == "install" {
389 | initName := s.Platform()
390 | if err := agentConfig.Read(path); err != nil {
391 | log.Fatalf("init config failed: %v", err)
392 | }
393 | printf("Init system is: %s", initName)
394 | }
395 |
396 | if len(action) != 0 {
397 | err := service.Control(s, action)
398 | if err != nil {
399 | log.Fatal(err)
400 | }
401 | return
402 | }
403 |
404 | err = s.Run()
405 | if err != nil {
406 | logger.DefaultLogger.Error(err)
407 | }
408 | }
409 |
410 | func receiveTasksDaemon(tasks pb.NezhaService_RequestTaskClient, errCh chan<- error) {
411 | var task *pb.Task
412 | var err error
413 | for {
414 | task, err = tasks.Recv()
415 | if err != nil {
416 | errCh <- fmt.Errorf("receiveTasks exit: %v", err)
417 | return
418 | }
419 | go func(t *pb.Task) {
420 | defer func() {
421 | if err := recover(); err != nil {
422 | println("task panic", task, err)
423 | }
424 | }()
425 | result := doTask(t)
426 | if result != nil {
427 | if err := tasks.Send(result); err != nil {
428 | printf("send task result error: %v", err)
429 | }
430 | }
431 | }(task)
432 | }
433 | }
434 |
435 | func doTask(task *pb.Task) *pb.TaskResult {
436 | var result pb.TaskResult
437 | result.Id = task.GetId()
438 | result.Type = task.GetType()
439 | switch task.GetType() {
440 | case model.TaskTypeHTTPGet:
441 | handleHttpGetTask(task, &result)
442 | case model.TaskTypeICMPPing:
443 | handleIcmpPingTask(task, &result)
444 | case model.TaskTypeTCPPing:
445 | handleTcpPingTask(task, &result)
446 | case model.TaskTypeCommand:
447 | handleCommandTask(task, &result)
448 | case model.TaskTypeUpgrade:
449 | handleUpgradeTask(task, &result)
450 | case model.TaskTypeTerminalGRPC:
451 | handleTerminalTask(task)
452 | return nil
453 | case model.TaskTypeNAT:
454 | handleNATTask(task)
455 | return nil
456 | case model.TaskTypeFM:
457 | handleFMTask(task)
458 | return nil
459 | case model.TaskTypeKeepalive:
460 | default:
461 | printf("不支持的任务: %v", task)
462 | return nil
463 | }
464 | return &result
465 | }
466 |
467 | // reportStateDaemon 向server上报状态信息
468 | func reportStateDaemon(stateClient pb.NezhaService_ReportSystemStateClient, errCh chan<- error) {
469 | var err error
470 | for {
471 | lastReportHostInfo, lastReportIPInfo, err = reportState(stateClient, lastReportHostInfo, lastReportIPInfo)
472 | if err != nil {
473 | errCh <- fmt.Errorf("reportStateDaemon exit: %v", err)
474 | return
475 | }
476 | time.Sleep(time.Second * time.Duration(agentConfig.ReportDelay))
477 | }
478 | }
479 |
480 | func reportState(statClient pb.NezhaService_ReportSystemStateClient, host, ip time.Time) (time.Time, time.Time, error) {
481 | if initialized {
482 | monitor.TrackNetworkSpeed()
483 | if err := statClient.Send(monitor.GetState(agentConfig.SkipConnectionCount, agentConfig.SkipProcsCount).PB()); err != nil {
484 | return host, ip, err
485 | }
486 | _, err := statClient.Recv()
487 | if err != nil {
488 | return host, ip, err
489 | }
490 | }
491 | // 每10分钟重新获取一次硬件信息
492 | if host.Before(time.Now().Add(-10 * time.Minute)) {
493 | if reportHost() {
494 | host = time.Now()
495 | }
496 | }
497 | // 更新IP信息
498 | if time.Since(ip) > time.Second*time.Duration(agentConfig.IPReportPeriod) || !geoipReported {
499 | if reportGeoIP(agentConfig.UseIPv6CountryCode, !geoipReported) {
500 | ip = time.Now()
501 | geoipReported = true
502 | }
503 | }
504 | return host, ip, nil
505 | }
506 |
507 | func reportHost() bool {
508 | if !hostStatus.CompareAndSwap(false, true) {
509 | return false
510 | }
511 | defer hostStatus.Store(false)
512 |
513 | if client != nil && initialized {
514 | receipt, err := client.ReportSystemInfo2(context.Background(), monitor.GetHost().PB())
515 | if err == nil {
516 | geoipReported = receipt.GetData() == prevDashboardBootTime
517 | prevDashboardBootTime = receipt.GetData()
518 | }
519 | }
520 |
521 | return true
522 | }
523 |
524 | func reportGeoIP(use6, forceUpdate bool) bool {
525 | if !ipStatus.CompareAndSwap(false, true) {
526 | return false
527 | }
528 | defer ipStatus.Store(false)
529 |
530 | if client != nil && initialized {
531 | pbg := monitor.FetchIP(use6)
532 | if pbg == nil {
533 | return false
534 | }
535 | if !monitor.GeoQueryIPChanged && !forceUpdate {
536 | return true
537 | }
538 | geoip, err := client.ReportGeoIP(context.Background(), pbg)
539 | if err == nil {
540 | monitor.CachedCountryCode = geoip.GetCountryCode()
541 | monitor.GeoQueryIPChanged = false
542 | }
543 | }
544 |
545 | return true
546 | }
547 |
548 | // doSelfUpdate 执行更新检查 如果更新成功则会结束进程
549 | func doSelfUpdate(useLocalVersion bool) {
550 | v := semver.MustParse("0.1.0")
551 | if useLocalVersion {
552 | v = semver.MustParse(version)
553 | }
554 | printf("检查更新: %v", v)
555 | var latest *selfupdate.Release
556 | var err error
557 | if monitor.CachedCountryCode != "cn" && !agentConfig.UseGiteeToUpgrade {
558 | updater, erru := selfupdate.NewUpdater(selfupdate.Config{
559 | BinaryName: binaryName,
560 | })
561 | if erru != nil {
562 | printf("更新失败: %v", erru)
563 | return
564 | }
565 | latest, err = updater.UpdateSelf(v, "nezhahq/agent")
566 | } else {
567 | updater, erru := selfupdate.NewGiteeUpdater(selfupdate.Config{
568 | BinaryName: binaryName,
569 | })
570 | if erru != nil {
571 | printf("更新失败: %v", erru)
572 | return
573 | }
574 | latest, err = updater.UpdateSelf(v, "naibahq/agent")
575 | }
576 | if err != nil {
577 | printf("更新失败: %v", err)
578 | return
579 | }
580 | if !latest.Version.Equals(v) {
581 | printf("已经更新至: %v, 正在结束进程", latest.Version)
582 | os.Exit(1)
583 | }
584 | }
585 |
586 | func handleUpgradeTask(*pb.Task, *pb.TaskResult) {
587 | if agentConfig.DisableForceUpdate {
588 | return
589 | }
590 | doSelfUpdate(false)
591 | }
592 |
593 | func handleTcpPingTask(task *pb.Task, result *pb.TaskResult) {
594 | if agentConfig.DisableSendQuery {
595 | result.Data = "This server has disabled query sending"
596 | return
597 | }
598 |
599 | host, port, err := net.SplitHostPort(task.GetData())
600 | if err != nil {
601 | result.Data = err.Error()
602 | return
603 | }
604 | ipAddr, err := lookupIP(host)
605 | if err != nil {
606 | result.Data = err.Error()
607 | return
608 | }
609 | if strings.IndexByte(ipAddr, ':') != -1 {
610 | ipAddr = fmt.Sprintf("[%s]", ipAddr)
611 | }
612 | printf("TCP-Ping Task: Pinging %s:%s", ipAddr, port)
613 | start := time.Now()
614 | conn, err := net.DialTimeout("tcp", fmt.Sprintf("%s:%s", ipAddr, port), time.Second*10)
615 | if err != nil {
616 | result.Data = err.Error()
617 | } else {
618 | conn.Close()
619 | result.Delay = float32(time.Since(start).Microseconds()) / 1000.0
620 | result.Successful = true
621 | }
622 | }
623 |
624 | func handleIcmpPingTask(task *pb.Task, result *pb.TaskResult) {
625 | if agentConfig.DisableSendQuery {
626 | result.Data = "This server has disabled query sending"
627 | return
628 | }
629 |
630 | ipAddr, err := lookupIP(task.GetData())
631 | printf("ICMP-Ping Task: Pinging %s(%s)", task.GetData(), ipAddr)
632 | if err != nil {
633 | result.Data = err.Error()
634 | return
635 | }
636 | pinger, err := ping.NewPinger(ipAddr)
637 | if err == nil {
638 | pinger.SetPrivileged(true)
639 | pinger.Count = 5
640 | pinger.Timeout = time.Second * 20
641 | err = pinger.Run() // Blocks until finished.
642 | }
643 | if err == nil {
644 | stat := pinger.Statistics()
645 | if stat.PacketsRecv == 0 {
646 | result.Data = "pockets recv 0"
647 | return
648 | }
649 | result.Delay = float32(stat.AvgRtt.Microseconds()) / 1000.0
650 | result.Successful = true
651 | } else {
652 | result.Data = err.Error()
653 | }
654 | }
655 |
656 | func handleHttpGetTask(task *pb.Task, result *pb.TaskResult) {
657 | if agentConfig.DisableSendQuery {
658 | result.Data = "This server has disabled query sending"
659 | return
660 | }
661 | start := time.Now()
662 | taskUrl := task.GetData()
663 | resp, err := httpClient.Get(taskUrl)
664 | printf("HTTP-GET Task: %s", taskUrl)
665 | checkHttpResp(taskUrl, start, resp, err, result)
666 | }
667 |
668 | func checkHttpResp(taskUrl string, start time.Time, resp *http.Response, err error, result *pb.TaskResult) {
669 | if err == nil {
670 | defer resp.Body.Close()
671 | _, err = io.Copy(io.Discard, resp.Body)
672 | }
673 | if err == nil {
674 | // 检查 HTTP Response 状态
675 | result.Delay = float32(time.Since(start).Microseconds()) / 1000.0
676 | if resp.StatusCode > 399 || resp.StatusCode < 200 {
677 | err = errors.New("\n应用错误:" + resp.Status)
678 | }
679 | }
680 | if err == nil {
681 | // 检查 SSL 证书信息
682 | if resp.TLS != nil && len(resp.TLS.PeerCertificates) > 0 {
683 | c := resp.TLS.PeerCertificates[0]
684 | result.Data = c.Issuer.CommonName + "|" + c.NotAfter.String()
685 | }
686 | altSvc := resp.Header.Get("Alt-Svc")
687 | if altSvc != "" {
688 | checkAltSvc(start, altSvc, taskUrl, result)
689 | } else {
690 | result.Successful = true
691 | }
692 | } else {
693 | // HTTP 请求失败
694 | result.Data = err.Error()
695 | }
696 | }
697 |
698 | func checkAltSvc(start time.Time, altSvcStr string, taskUrl string, result *pb.TaskResult) {
699 | altSvcList, err := altsvc.Parse(altSvcStr)
700 | if err != nil {
701 | result.Data = err.Error()
702 | result.Successful = false
703 | return
704 | }
705 |
706 | parsedUrl, _ := url.Parse(taskUrl)
707 | originalHost := parsedUrl.Hostname()
708 | originalPort := parsedUrl.Port()
709 | if originalPort == "" {
710 | switch parsedUrl.Scheme {
711 | case "http":
712 | originalPort = "80"
713 | case "https":
714 | originalPort = "443"
715 | }
716 | }
717 |
718 | altAuthorityHost := ""
719 | altAuthorityPort := ""
720 | altAuthorityProtocol := ""
721 | for _, altSvc := range altSvcList {
722 | altAuthorityPort = altSvc.AltAuthority.Port
723 | if altSvc.AltAuthority.Host != "" {
724 | altAuthorityHost = altSvc.AltAuthority.Host
725 | altAuthorityProtocol = altSvc.ProtocolID
726 | break
727 | }
728 | }
729 | if altAuthorityHost == "" {
730 | altAuthorityHost = originalHost
731 | }
732 | if altAuthorityHost == originalHost && altAuthorityPort == originalPort {
733 | result.Successful = true
734 | return
735 | }
736 |
737 | altAuthorityUrl := "https://" + altAuthorityHost + ":" + altAuthorityPort + "/"
738 | req, _ := http.NewRequest("GET", altAuthorityUrl, nil)
739 | req.Host = originalHost
740 |
741 | client := httpClient
742 | if strings.HasPrefix(altAuthorityProtocol, "h3") {
743 | client = httpClient3
744 | }
745 | resp, err := client.Do(req)
746 |
747 | checkHttpResp(altAuthorityUrl, start, resp, err, result)
748 | }
749 |
750 | func handleCommandTask(task *pb.Task, result *pb.TaskResult) {
751 | if agentConfig.DisableCommandExecute {
752 | result.Data = "此 Agent 已禁止命令执行"
753 | return
754 | }
755 | startedAt := time.Now()
756 | endCh := make(chan struct{})
757 | pg, err := processgroup.NewProcessExitGroup()
758 | if err != nil {
759 | // 进程组创建失败,直接退出
760 | result.Data = err.Error()
761 | return
762 | }
763 | timeout := time.NewTimer(time.Hour * 2)
764 | cmd := processgroup.NewCommand(task.GetData())
765 | var b bytes.Buffer
766 | cmd.Stdout = &b
767 | cmd.Env = os.Environ()
768 | if err = cmd.Start(); err != nil {
769 | result.Data = err.Error()
770 | return
771 | }
772 | pg.AddProcess(cmd)
773 | go func() {
774 | select {
775 | case <-timeout.C:
776 | result.Data = "任务执行超时\n"
777 | close(endCh)
778 | pg.Dispose()
779 | case <-endCh:
780 | timeout.Stop()
781 | }
782 | }()
783 | if err = cmd.Wait(); err != nil {
784 | result.Data += fmt.Sprintf("%s\n%s", b.String(), err.Error())
785 | } else {
786 | close(endCh)
787 | result.Data = b.String()
788 | result.Successful = true
789 | }
790 | pg.Dispose()
791 | result.Delay = float32(time.Since(startedAt).Seconds())
792 | }
793 |
794 | type WindowSize struct {
795 | Cols uint32
796 | Rows uint32
797 | }
798 |
799 | func handleTerminalTask(task *pb.Task) {
800 | if agentConfig.DisableCommandExecute {
801 | println("此 Agent 已禁止命令执行")
802 | return
803 | }
804 | var terminal model.TerminalTask
805 | err := util.Json.Unmarshal([]byte(task.GetData()), &terminal)
806 | if err != nil {
807 | printf("Terminal 任务解析错误: %v", err)
808 | return
809 | }
810 |
811 | remoteIO, err := client.IOStream(context.Background())
812 | if err != nil {
813 | printf("Terminal IOStream失败: %v", err)
814 | return
815 | }
816 |
817 | // 发送 StreamID
818 | if err := remoteIO.Send(&pb.IOStreamData{Data: append([]byte{
819 | 0xff, 0x05, 0xff, 0x05,
820 | }, []byte(terminal.StreamID)...)}); err != nil {
821 | printf("Terminal 发送StreamID失败: %v", err)
822 | return
823 | }
824 |
825 | go ioStreamKeepAlive(remoteIO)
826 |
827 | tty, err := pty.Start()
828 | if err != nil {
829 | printf("Terminal pty.Start失败 %v", err)
830 | return
831 | }
832 |
833 | defer func() {
834 | err := tty.Close()
835 | errCloseSend := remoteIO.CloseSend()
836 | println("terminal exit", terminal.StreamID, err, errCloseSend)
837 | }()
838 | println("terminal init", terminal.StreamID)
839 |
840 | go func() {
841 | buf := make([]byte, 10240)
842 | for {
843 | read, err := tty.Read(buf)
844 | if err != nil {
845 | remoteIO.Send(&pb.IOStreamData{Data: []byte(err.Error())})
846 | remoteIO.CloseSend()
847 | return
848 | }
849 | remoteIO.Send(&pb.IOStreamData{Data: buf[:read]})
850 | }
851 | }()
852 |
853 | for {
854 | var remoteData *pb.IOStreamData
855 | if remoteData, err = remoteIO.Recv(); err != nil {
856 | return
857 | }
858 | if len(remoteData.Data) == 0 {
859 | continue
860 | }
861 | switch remoteData.Data[0] {
862 | case 0:
863 | tty.Write(remoteData.Data[1:])
864 | case 1:
865 | decoder := util.Json.NewDecoder(strings.NewReader(string(remoteData.Data[1:])))
866 | var resizeMessage WindowSize
867 | err := decoder.Decode(&resizeMessage)
868 | if err != nil {
869 | continue
870 | }
871 | tty.Setsize(resizeMessage.Cols, resizeMessage.Rows)
872 | }
873 | }
874 | }
875 |
876 | func handleNATTask(task *pb.Task) {
877 | if agentConfig.DisableNat {
878 | println("This server has disabled NAT traversal")
879 | return
880 | }
881 |
882 | var nat model.TaskNAT
883 | err := util.Json.Unmarshal([]byte(task.GetData()), &nat)
884 | if err != nil {
885 | printf("NAT 任务解析错误: %v", err)
886 | return
887 | }
888 |
889 | remoteIO, err := client.IOStream(context.Background())
890 | if err != nil {
891 | printf("NAT IOStream失败: %v", err)
892 | return
893 | }
894 |
895 | // 发送 StreamID
896 | if err := remoteIO.Send(&pb.IOStreamData{Data: append([]byte{
897 | 0xff, 0x05, 0xff, 0x05,
898 | }, []byte(nat.StreamID)...)}); err != nil {
899 | printf("NAT 发送StreamID失败: %v", err)
900 | return
901 | }
902 |
903 | go ioStreamKeepAlive(remoteIO)
904 |
905 | conn, err := net.Dial("tcp", nat.Host)
906 | if err != nil {
907 | printf("NAT Dial %s 失败:%s", nat.Host, err)
908 | return
909 | }
910 |
911 | defer func() {
912 | err := conn.Close()
913 | errCloseSend := remoteIO.CloseSend()
914 | println("NAT exit", nat.StreamID, err, errCloseSend)
915 | }()
916 | println("NAT init", nat.StreamID)
917 |
918 | go func() {
919 | buf := make([]byte, 10240)
920 | for {
921 | read, err := conn.Read(buf)
922 | if err != nil {
923 | remoteIO.Send(&pb.IOStreamData{Data: []byte(err.Error())})
924 | remoteIO.CloseSend()
925 | return
926 | }
927 | remoteIO.Send(&pb.IOStreamData{Data: buf[:read]})
928 | }
929 | }()
930 |
931 | for {
932 | var remoteData *pb.IOStreamData
933 | if remoteData, err = remoteIO.Recv(); err != nil {
934 | return
935 | }
936 | conn.Write(remoteData.Data)
937 | }
938 | }
939 |
940 | func handleFMTask(task *pb.Task) {
941 | if agentConfig.DisableCommandExecute {
942 | println("此 Agent 已禁止命令执行")
943 | return
944 | }
945 | var fmTask model.TaskFM
946 | err := util.Json.Unmarshal([]byte(task.GetData()), &fmTask)
947 | if err != nil {
948 | printf("FM 任务解析错误: %v", err)
949 | return
950 | }
951 |
952 | remoteIO, err := client.IOStream(context.Background())
953 | if err != nil {
954 | printf("FM IOStream失败: %v", err)
955 | return
956 | }
957 |
958 | // 发送 StreamID
959 | if err := remoteIO.Send(&pb.IOStreamData{Data: append([]byte{
960 | 0xff, 0x05, 0xff, 0x05,
961 | }, []byte(fmTask.StreamID)...)}); err != nil {
962 | printf("FM 发送StreamID失败: %v", err)
963 | return
964 | }
965 |
966 | go ioStreamKeepAlive(remoteIO)
967 |
968 | defer func() {
969 | errCloseSend := remoteIO.CloseSend()
970 | println("FM exit", fmTask.StreamID, nil, errCloseSend)
971 | }()
972 | println("FM init", fmTask.StreamID)
973 |
974 | fmc := fm.NewFMClient(remoteIO, printf)
975 | for {
976 | var remoteData *pb.IOStreamData
977 | if remoteData, err = remoteIO.Recv(); err != nil {
978 | return
979 | }
980 | if len(remoteData.Data) == 0 {
981 | continue
982 | }
983 | fmc.DoTask(remoteData)
984 | }
985 | }
986 |
987 | func lookupIP(hostOrIp string) (string, error) {
988 | if net.ParseIP(hostOrIp) == nil {
989 | ips, err := dnsResolver.LookupIPAddr(context.Background(), hostOrIp)
990 | if err != nil {
991 | return "", err
992 | }
993 | if len(ips) == 0 {
994 | return "", fmt.Errorf("无法解析 %s", hostOrIp)
995 | }
996 | return ips[0].IP.String(), nil
997 | }
998 | return hostOrIp, nil
999 | }
1000 |
1001 | func ioStreamKeepAlive(stream pb.NezhaService_IOStreamClient) {
1002 | for {
1003 | if err := stream.Send(&pb.IOStreamData{Data: []byte{}}); err != nil {
1004 | printf("IOStream KeepAlive 失败: %v", err)
1005 | return
1006 | }
1007 | time.Sleep(time.Second * 30)
1008 | }
1009 | }
1010 |
--------------------------------------------------------------------------------