├── .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 | naiba 8 | UUBulb 9 | AM科技 10 | Leon 11 | zhangnew 12 | AEnjoy 13 | :D 14 | Darc Z. 15 | xream 16 | xykt 17 | 卖女孩的小火柴 18 | Chisato22 19 | 葉鲜森(KEVI_) 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 | --------------------------------------------------------------------------------