| 1 | package main |
| 2 | |
| 3 | import ( |
| 4 | "runtime" |
| 5 | "strconv" |
| 6 | "strings" |
| 7 | ) |
| 8 | |
| 9 | // devinfo.go collects coarse machine facts attached to crash reports: OS version, |
| 10 | // CPU model, core count, RAM. Nothing here identifies a user or machine. |
| 11 | |
| 12 | type deviceInfo struct { |
| 13 | OSVersion string `json:"osVersion,omitempty"` |
| 14 | OSBuild int `json:"osBuild,omitempty"` |
| 15 | OSRevision int `json:"osRevision,omitempty"` |
| 16 | DistroID string `json:"distroId,omitempty"` |
| 17 | DistroVersion string `json:"distroVersion,omitempty"` |
| 18 | KernelVersion string `json:"kernelVersion,omitempty"` |
| 19 | SessionType string `json:"sessionType,omitempty"` |
| 20 | CPU string `json:"cpu,omitempty"` |
| 21 | Cores int `json:"cores"` |
| 22 | RAMGB int `json:"ramGb,omitempty"` |
| 23 | } |
| 24 | |
| 25 | type platformEnvironment struct { |
| 26 | DistroID string |
| 27 | DistroVersion string |
| 28 | KernelVersion string |
| 29 | SessionType string |
| 30 | } |
| 31 | |
| 32 | const gib = 1 << 30 |
| 33 | |
| 34 | func collectDeviceInfo() deviceInfo { |
| 35 | osBuild, osRevision := platformOSBuild() |
| 36 | environment := platformEnvironmentInfo() |
| 37 | return deviceInfo{ |
| 38 | OSVersion: boundedPlatformField(platformOSVersion(), 128), |
| 39 | OSBuild: osBuild, |
| 40 | OSRevision: osRevision, |
| 41 | DistroID: environment.DistroID, |
| 42 | DistroVersion: environment.DistroVersion, |
| 43 | KernelVersion: environment.KernelVersion, |
| 44 | SessionType: environment.SessionType, |
| 45 | CPU: boundedPlatformField(platformCPU(), 128), |
| 46 | Cores: runtime.NumCPU(), |
| 47 | RAMGB: int((platformRAMBytes() + gib/2) / gib), |
| 48 | } |
| 49 | } |
| 50 | |
| 51 | func boundedPlatformField(value string, limit int) string { |
| 52 | value = strings.TrimSpace(value) |
| 53 | value = strings.Map(func(r rune) rune { |
| 54 | if r < 0x20 || r == 0x7f { |
| 55 | return -1 |
| 56 | } |
| 57 | return r |
| 58 | }, value) |
| 59 | if len(value) > limit { |
| 60 | value = value[:limit] |
| 61 | } |
| 62 | return value |
| 63 | } |
| 64 | |
| 65 | func parseOSRelease(osRelease string) map[string]string { |
| 66 | result := make(map[string]string) |
| 67 | for line := range strings.SplitSeq(osRelease, "\n") { |
| 68 | line = strings.TrimSpace(line) |
| 69 | if line == "" || strings.HasPrefix(line, "#") { |
| 70 | continue |
| 71 | } |
| 72 | key, value, ok := strings.Cut(line, "=") |
| 73 | if !ok { |
| 74 | continue |
| 75 | } |
| 76 | key = strings.TrimSpace(key) |
| 77 | value = strings.TrimSpace(value) |
| 78 | if len(value) >= 2 && ((value[0] == '"' && value[len(value)-1] == '"') || (value[0] == '\'' && value[len(value)-1] == '\'')) { |
| 79 | value = value[1 : len(value)-1] |
| 80 | value = strings.ReplaceAll(value, `\"`, `"`) |
| 81 | value = strings.ReplaceAll(value, `\\`, `\`) |
| 82 | } |
| 83 | result[key] = value |
| 84 | } |
| 85 | return result |
| 86 | } |
| 87 | |
| 88 | func parseCPUModel(cpuinfo string) string { |
| 89 | for line := range strings.SplitSeq(cpuinfo, "\n") { |
| 90 | if name, ok := strings.CutPrefix(line, "model name"); ok { |
| 91 | if _, v, ok := strings.Cut(name, ":"); ok { |
| 92 | return strings.TrimSpace(v) |
| 93 | } |
| 94 | } |
| 95 | } |
| 96 | return "" |
| 97 | } |
| 98 | |
| 99 | func parseMemTotalBytes(meminfo string) uint64 { |
| 100 | for line := range strings.SplitSeq(meminfo, "\n") { |
| 101 | if rest, ok := strings.CutPrefix(line, "MemTotal:"); ok { |
| 102 | kb, err := strconv.ParseUint(strings.TrimSuffix(strings.TrimSpace(rest), " kB"), 10, 64) |
| 103 | if err != nil { |
| 104 | return 0 |
| 105 | } |
| 106 | return kb * 1024 |
| 107 | } |
| 108 | } |
| 109 | return 0 |
| 110 | } |
| 111 | |
| 112 | func parseOSReleasePrettyName(osRelease string) string { |
| 113 | return parseOSRelease(osRelease)["PRETTY_NAME"] |
| 114 | } |
| 115 | |
| 116 | func linuxSessionType(getenv func(string) string) string { |
| 117 | if getenv("SSH_CONNECTION") != "" || getenv("XRDP_SESSION") != "" { |
| 118 | return "remote" |
| 119 | } |
| 120 | switch strings.ToLower(strings.TrimSpace(getenv("XDG_SESSION_TYPE"))) { |
| 121 | case "wayland": |
| 122 | return "wayland" |
| 123 | case "x11": |
| 124 | return "x11" |
| 125 | } |
| 126 | if getenv("WAYLAND_DISPLAY") != "" { |
| 127 | return "wayland" |
| 128 | } |
| 129 | if getenv("DISPLAY") != "" { |
| 130 | return "x11" |
| 131 | } |
| 132 | return "unknown" |
| 133 | } |
| 134 |