@yuuki824/kanshi 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +34 -0
- package/Dockerfile +53 -0
- package/LICENSE +661 -0
- package/README.md +219 -0
- package/bin/kanshi.js +63 -0
- package/docker-compose.yml +84 -0
- package/go.mod +3 -0
- package/internal/access/access.go +275 -0
- package/internal/access/listeners.go +229 -0
- package/internal/config/config.go +256 -0
- package/internal/config/defaults_other.go +5 -0
- package/internal/config/defaults_windows.go +4 -0
- package/internal/config/envfile.go +147 -0
- package/internal/dockerstats/dial.go +32 -0
- package/internal/dockerstats/dial_other.go +13 -0
- package/internal/dockerstats/dial_windows.go +62 -0
- package/internal/dockerstats/dockerstats.go +626 -0
- package/internal/roots/roots.go +175 -0
- package/internal/roots/roots_linux.go +132 -0
- package/internal/roots/roots_windows.go +74 -0
- package/internal/server/access.go +155 -0
- package/internal/server/server.go +523 -0
- package/internal/server/static.go +165 -0
- package/internal/storage/cache.go +283 -0
- package/internal/storage/dir_linux.go +177 -0
- package/internal/storage/dir_windows.go +244 -0
- package/internal/storage/statat_fast.go +21 -0
- package/internal/storage/statat_other.go +11 -0
- package/internal/storage/storage.go +598 -0
- package/internal/storage/sysnum_amd64.go +7 -0
- package/internal/storage/sysnum_generic.go +7 -0
- package/internal/storage/walk.go +342 -0
- package/internal/vitals/vitals.go +246 -0
- package/internal/vitals/vitals_linux.go +548 -0
- package/internal/vitals/vitals_windows.go +286 -0
- package/main.go +129 -0
- package/main_other.go +11 -0
- package/main_windows.go +29 -0
- package/package.json +49 -0
- package/web/app.js +835 -0
- package/web/apple-touch-icon.png +0 -0
- package/web/icon.svg +6 -0
- package/web/index.html +151 -0
- package/web/style.css +356 -0
- package/web/treemap.js +217 -0
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
package vitals
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"fmt"
|
|
5
|
+
"syscall"
|
|
6
|
+
"time"
|
|
7
|
+
"unsafe"
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
var (
|
|
11
|
+
ntdll = syscall.NewLazyDLL("ntdll.dll")
|
|
12
|
+
procNtQuerySystemInformation = ntdll.NewProc("NtQuerySystemInformation")
|
|
13
|
+
|
|
14
|
+
kernel32 = syscall.NewLazyDLL("kernel32.dll")
|
|
15
|
+
procGlobalMemoryStatusEx = kernel32.NewProc("GlobalMemoryStatusEx")
|
|
16
|
+
procK32GetPerformanceInfo = kernel32.NewProc("K32GetPerformanceInfo")
|
|
17
|
+
procGetTickCount64 = kernel32.NewProc("GetTickCount64")
|
|
18
|
+
|
|
19
|
+
iphlpapi = syscall.NewLazyDLL("iphlpapi.dll")
|
|
20
|
+
procGetIfTable2 = iphlpapi.NewProc("GetIfTable2")
|
|
21
|
+
procFreeMibTable = iphlpapi.NewProc("FreeMibTable")
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
const (
|
|
25
|
+
systemProcessorPerformanceInformation = 8
|
|
26
|
+
statusInfoLengthMismatch = 0xC0000004
|
|
27
|
+
ioctlDiskPerformance = 0x70020
|
|
28
|
+
ifTypeSoftwareLoopback = 24
|
|
29
|
+
devicesEvery = time.Minute
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
type sysState struct {
|
|
33
|
+
perf []processorPerformance
|
|
34
|
+
prevCPU, curCPU []cpuTimes
|
|
35
|
+
cpuAt time.Time
|
|
36
|
+
|
|
37
|
+
disks []syscall.Handle
|
|
38
|
+
disksAt time.Time
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/* ── CPU ────────────────────────────────────────────────────────────────── */
|
|
42
|
+
|
|
43
|
+
// processorPerformance is SYSTEM_PROCESSOR_PERFORMANCE_INFORMATION. Kernel
|
|
44
|
+
// time includes idle time.
|
|
45
|
+
type processorPerformance struct {
|
|
46
|
+
IdleTime, KernelTime, UserTime, DpcTime, InterruptTime int64
|
|
47
|
+
InterruptCount uint32
|
|
48
|
+
_ uint32
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
type cpuTimes struct{ busy, total uint64 }
|
|
52
|
+
|
|
53
|
+
func (r *Reader) readCPU(dst []cpuTimes) []cpuTimes {
|
|
54
|
+
s := &r.sys
|
|
55
|
+
if s.perf == nil {
|
|
56
|
+
s.perf = make([]processorPerformance, 64)
|
|
57
|
+
}
|
|
58
|
+
for {
|
|
59
|
+
var n uint32
|
|
60
|
+
size := uint32(len(s.perf)) * uint32(unsafe.Sizeof(s.perf[0]))
|
|
61
|
+
st, _, _ := procNtQuerySystemInformation.Call(systemProcessorPerformanceInformation,
|
|
62
|
+
uintptr(unsafe.Pointer(&s.perf[0])), uintptr(size), uintptr(unsafe.Pointer(&n)))
|
|
63
|
+
if st == statusInfoLengthMismatch {
|
|
64
|
+
s.perf = make([]processorPerformance, 2*len(s.perf))
|
|
65
|
+
continue
|
|
66
|
+
}
|
|
67
|
+
if st != 0 {
|
|
68
|
+
return dst[:0]
|
|
69
|
+
}
|
|
70
|
+
dst = dst[:0]
|
|
71
|
+
for _, p := range s.perf[:n/uint32(unsafe.Sizeof(s.perf[0]))] {
|
|
72
|
+
total := uint64(p.KernelTime + p.UserTime)
|
|
73
|
+
dst = append(dst, cpuTimes{busy: total - uint64(p.IdleTime), total: total})
|
|
74
|
+
}
|
|
75
|
+
return dst
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
func (r *Reader) primeCPU() {
|
|
80
|
+
r.sys.prevCPU = r.readCPU(r.sys.prevCPU)
|
|
81
|
+
r.sys.cpuAt = time.Now()
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// cpuCores mirrors the Linux version: a delta against the previous call, with
|
|
85
|
+
// a short blocking window when that call was too recent to divide by.
|
|
86
|
+
func (r *Reader) cpuCores() []float64 {
|
|
87
|
+
s := &r.sys
|
|
88
|
+
s.curCPU = r.readCPU(s.curCPU)
|
|
89
|
+
if len(s.curCPU) == 0 {
|
|
90
|
+
return nil
|
|
91
|
+
}
|
|
92
|
+
if len(s.prevCPU) == 0 || time.Since(s.cpuAt) < minDT {
|
|
93
|
+
s.prevCPU, s.curCPU = s.curCPU, s.prevCPU
|
|
94
|
+
s.cpuAt = time.Now()
|
|
95
|
+
time.Sleep(250 * time.Millisecond)
|
|
96
|
+
if s.curCPU = r.readCPU(s.curCPU); len(s.curCPU) == 0 {
|
|
97
|
+
return nil
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
prev, now := s.prevCPU, s.curCPU
|
|
101
|
+
out := make([]float64, len(now))
|
|
102
|
+
if len(prev) == len(now) {
|
|
103
|
+
for i := range now {
|
|
104
|
+
out[i] = busyPercent(float64(now[i].busy)-float64(prev[i].busy),
|
|
105
|
+
float64(now[i].total)-float64(prev[i].total))
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
s.prevCPU, s.curCPU = now, prev
|
|
109
|
+
s.cpuAt = time.Now()
|
|
110
|
+
return out
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/* ── memory ─────────────────────────────────────────────────────────────── */
|
|
114
|
+
|
|
115
|
+
type memoryStatusEx struct {
|
|
116
|
+
Length, MemoryLoad uint32
|
|
117
|
+
TotalPhys, AvailPhys uint64
|
|
118
|
+
TotalPageFile, AvailPageFile uint64
|
|
119
|
+
TotalVirtual, AvailVirtual, AvailExtra uint64
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
type performanceInformation struct {
|
|
123
|
+
Size uint32
|
|
124
|
+
CommitTotal, CommitLimit, CommitPeak uintptr
|
|
125
|
+
PhysicalTotal, PhysicalAvailable, SystemCache uintptr
|
|
126
|
+
KernelTotal, KernelPaged, KernelNonpaged, PageSize uintptr
|
|
127
|
+
HandleCount, ProcessCount, ThreadCount uint32
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// memory reports RAM. Windows has no swap partition in the Linux sense — its
|
|
131
|
+
// page file backs committed memory rather than overflowing RAM — so swap is
|
|
132
|
+
// reported as absent and the dashboard leaves that meter out.
|
|
133
|
+
func (r *Reader) memory() (Memory, Swap) {
|
|
134
|
+
ms := memoryStatusEx{Length: uint32(unsafe.Sizeof(memoryStatusEx{}))}
|
|
135
|
+
if ok, _, _ := procGlobalMemoryStatusEx.Call(uintptr(unsafe.Pointer(&ms))); ok == 0 {
|
|
136
|
+
return Memory{}, Swap{}
|
|
137
|
+
}
|
|
138
|
+
var cached uint64
|
|
139
|
+
pi := performanceInformation{Size: uint32(unsafe.Sizeof(performanceInformation{}))}
|
|
140
|
+
if ok, _, _ := procK32GetPerformanceInfo.Call(uintptr(unsafe.Pointer(&pi)), uintptr(pi.Size)); ok != 0 {
|
|
141
|
+
cached = uint64(pi.SystemCache) * uint64(pi.PageSize)
|
|
142
|
+
}
|
|
143
|
+
return memoryFigures(ms.TotalPhys, ms.AvailPhys, cached), Swap{}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/* ── network ────────────────────────────────────────────────────────────── */
|
|
147
|
+
|
|
148
|
+
// mibIfRow2 is MIB_IF_ROW2. Go's own alignment rules reproduce the C layout;
|
|
149
|
+
// a test pins the size and the offsets that are read.
|
|
150
|
+
type mibIfRow2 struct {
|
|
151
|
+
InterfaceLuid uint64
|
|
152
|
+
InterfaceIndex uint32
|
|
153
|
+
InterfaceGUID [16]byte
|
|
154
|
+
Alias [257]uint16
|
|
155
|
+
Description [257]uint16
|
|
156
|
+
PhysicalAddressLength uint32
|
|
157
|
+
PhysicalAddress [32]byte
|
|
158
|
+
PermanentPhysicalAddress [32]byte
|
|
159
|
+
Mtu uint32
|
|
160
|
+
Type uint32
|
|
161
|
+
TunnelType uint32
|
|
162
|
+
MediaType uint32
|
|
163
|
+
PhysicalMediumType uint32
|
|
164
|
+
AccessType uint32
|
|
165
|
+
DirectionType uint32
|
|
166
|
+
InterfaceAndOperStatusFlags uint8
|
|
167
|
+
OperStatus uint32
|
|
168
|
+
AdminStatus uint32
|
|
169
|
+
MediaConnectState uint32
|
|
170
|
+
NetworkGUID [16]byte
|
|
171
|
+
ConnectionType uint32
|
|
172
|
+
TransmitLinkSpeed uint64
|
|
173
|
+
ReceiveLinkSpeed uint64
|
|
174
|
+
InOctets uint64
|
|
175
|
+
InUcastPkts uint64
|
|
176
|
+
InNUcastPkts uint64
|
|
177
|
+
InDiscards uint64
|
|
178
|
+
InErrors uint64
|
|
179
|
+
InUnknownProtos uint64
|
|
180
|
+
InUcastOctets uint64
|
|
181
|
+
InMulticastOctets uint64
|
|
182
|
+
InBroadcastOctets uint64
|
|
183
|
+
OutOctets uint64
|
|
184
|
+
OutUcastPkts uint64
|
|
185
|
+
OutNUcastPkts uint64
|
|
186
|
+
OutDiscards uint64
|
|
187
|
+
OutErrors uint64
|
|
188
|
+
OutUcastOctets uint64
|
|
189
|
+
OutMulticastOctets uint64
|
|
190
|
+
OutBroadcastOctets uint64
|
|
191
|
+
OutQLen uint64
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const (
|
|
195
|
+
ifHardwareInterface = 1 << 0
|
|
196
|
+
ifFilterInterface = 1 << 1
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
// netCounters sums the physical adapters only. Hyper-V switches, WSL, VPN
|
|
200
|
+
// tunnels and the filter drivers stacked on a NIC all report the same bytes
|
|
201
|
+
// again, the same double counting the Linux side avoids via sysfs.
|
|
202
|
+
func (r *Reader) netCounters() (rx, tx uint64, ok bool) {
|
|
203
|
+
// The table lives in memory Windows allocated, outside the Go heap, and
|
|
204
|
+
// is handed back with FreeMibTable.
|
|
205
|
+
var table unsafe.Pointer
|
|
206
|
+
if st, _, _ := procGetIfTable2.Call(uintptr(unsafe.Pointer(&table))); st != 0 || table == nil {
|
|
207
|
+
return 0, 0, false
|
|
208
|
+
}
|
|
209
|
+
defer procFreeMibTable.Call(uintptr(table))
|
|
210
|
+
n := *(*uint32)(table)
|
|
211
|
+
rows := unsafe.Slice((*mibIfRow2)(unsafe.Add(table, 8)), n)
|
|
212
|
+
for i := range rows {
|
|
213
|
+
row := &rows[i]
|
|
214
|
+
flags := row.InterfaceAndOperStatusFlags
|
|
215
|
+
if flags&ifHardwareInterface == 0 || flags&ifFilterInterface != 0 || row.Type == ifTypeSoftwareLoopback {
|
|
216
|
+
continue
|
|
217
|
+
}
|
|
218
|
+
rx += row.InOctets
|
|
219
|
+
tx += row.OutOctets
|
|
220
|
+
}
|
|
221
|
+
return rx, tx, true
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/* ── disk ───────────────────────────────────────────────────────────────── */
|
|
225
|
+
|
|
226
|
+
type diskPerformance struct {
|
|
227
|
+
BytesRead, BytesWritten int64
|
|
228
|
+
ReadTime, WriteTime, IdleTime int64
|
|
229
|
+
ReadCount, WriteCount, QueueDepth, SplitCount uint32
|
|
230
|
+
QueryTime int64
|
|
231
|
+
StorageDeviceNumber uint32
|
|
232
|
+
StorageManagerName [8]uint16
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// diskCounters sums IOCTL_DISK_PERFORMANCE over every physical drive. Opening
|
|
236
|
+
// a drive with no access rights is enough for this call, so it needs no
|
|
237
|
+
// administrator token.
|
|
238
|
+
func (r *Reader) diskCounters() (read, write uint64, ok bool) {
|
|
239
|
+
s := &r.sys
|
|
240
|
+
if s.disksAt.IsZero() || time.Since(s.disksAt) > devicesEvery {
|
|
241
|
+
s.reopenDisks()
|
|
242
|
+
}
|
|
243
|
+
for _, h := range s.disks {
|
|
244
|
+
var perf diskPerformance
|
|
245
|
+
var n uint32
|
|
246
|
+
if err := syscall.DeviceIoControl(h, ioctlDiskPerformance, nil, 0,
|
|
247
|
+
(*byte)(unsafe.Pointer(&perf)), uint32(unsafe.Sizeof(perf)), &n, nil); err != nil {
|
|
248
|
+
continue
|
|
249
|
+
}
|
|
250
|
+
read += uint64(perf.BytesRead)
|
|
251
|
+
write += uint64(perf.BytesWritten)
|
|
252
|
+
ok = true
|
|
253
|
+
}
|
|
254
|
+
return read, write, ok
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
func (s *sysState) reopenDisks() {
|
|
258
|
+
for _, h := range s.disks {
|
|
259
|
+
syscall.CloseHandle(h)
|
|
260
|
+
}
|
|
261
|
+
s.disks = s.disks[:0]
|
|
262
|
+
s.disksAt = time.Now()
|
|
263
|
+
// Drive numbers can have gaps after a disk is removed, so every slot is
|
|
264
|
+
// tried rather than stopping at the first miss.
|
|
265
|
+
for i := 0; i < 32; i++ {
|
|
266
|
+
p, _ := syscall.UTF16PtrFromString(fmt.Sprintf(`\\.\PhysicalDrive%d`, i))
|
|
267
|
+
h, err := syscall.CreateFile(p, 0, syscall.FILE_SHARE_READ|syscall.FILE_SHARE_WRITE, nil,
|
|
268
|
+
syscall.OPEN_EXISTING, 0, 0)
|
|
269
|
+
if err == nil {
|
|
270
|
+
s.disks = append(s.disks, h)
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/* ── load, temperature, uptime ──────────────────────────────────────────── */
|
|
276
|
+
|
|
277
|
+
// Windows keeps no load average, and its only CPU temperature source (the
|
|
278
|
+
// ACPI thermal zone over WMI) needs an administrator and is wrong on most
|
|
279
|
+
// desktop boards. Both are left null; the dashboard hides them.
|
|
280
|
+
func (r *Reader) loadAvg() *[3]float64 { return nil }
|
|
281
|
+
func (r *Reader) temperature() *float64 { return nil }
|
|
282
|
+
|
|
283
|
+
func (r *Reader) uptime() float64 {
|
|
284
|
+
ms, _, _ := procGetTickCount64.Call()
|
|
285
|
+
return float64(ms) / 1000
|
|
286
|
+
}
|
package/main.go
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
// Command kanshi serves an at-a-glance dashboard for a single machine.
|
|
2
|
+
package main
|
|
3
|
+
|
|
4
|
+
import (
|
|
5
|
+
"context"
|
|
6
|
+
"embed"
|
|
7
|
+
"flag"
|
|
8
|
+
"fmt"
|
|
9
|
+
"io/fs"
|
|
10
|
+
"net/http"
|
|
11
|
+
"os"
|
|
12
|
+
"os/signal"
|
|
13
|
+
"runtime"
|
|
14
|
+
"runtime/debug"
|
|
15
|
+
"strconv"
|
|
16
|
+
"syscall"
|
|
17
|
+
"time"
|
|
18
|
+
|
|
19
|
+
"github.com/rene-roid/kanshi/internal/access"
|
|
20
|
+
"github.com/rene-roid/kanshi/internal/config"
|
|
21
|
+
"github.com/rene-roid/kanshi/internal/server"
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
// version is stamped by the release build with -ldflags "-X main.version=…".
|
|
25
|
+
var version = "dev"
|
|
26
|
+
|
|
27
|
+
// The frontend is baked into the binary so the runtime image can be scratch —
|
|
28
|
+
// nothing to copy, nothing to keep in sync with the code that serves it.
|
|
29
|
+
//
|
|
30
|
+
//go:embed web
|
|
31
|
+
var embedded embed.FS
|
|
32
|
+
|
|
33
|
+
func main() {
|
|
34
|
+
var flags config.Flags
|
|
35
|
+
flag.StringVar(&flags.Access, "access", "", "who can reach the dashboard: local, lan, tailscale, all, or a mix like lan,tailscale (default local)")
|
|
36
|
+
flag.IntVar(&flags.Port, "port", 0, "port to listen on (default 8100)")
|
|
37
|
+
flag.StringVar(&flags.File, "config", "", "settings file to read (default: "+config.FileName+" next to the program, then in the user config directory)")
|
|
38
|
+
open := flag.Bool("open", ownConsole(), "open the dashboard in the browser once it is listening")
|
|
39
|
+
healthcheck := flag.Bool("healthcheck", false, "probe a running instance and exit; used by the container HEALTHCHECK")
|
|
40
|
+
showVersion := flag.Bool("version", false, "print the version and exit")
|
|
41
|
+
flag.Parse()
|
|
42
|
+
|
|
43
|
+
if *showVersion {
|
|
44
|
+
fmt.Println("kanshi", version)
|
|
45
|
+
return
|
|
46
|
+
}
|
|
47
|
+
cfg := config.Load(flags)
|
|
48
|
+
if *healthcheck {
|
|
49
|
+
os.Exit(probe(cfg))
|
|
50
|
+
}
|
|
51
|
+
resourceDefaults()
|
|
52
|
+
|
|
53
|
+
mode, err := access.Parse(cfg.Access)
|
|
54
|
+
if err != nil {
|
|
55
|
+
// A typo here should narrow access, never widen it.
|
|
56
|
+
logf("warning: %v; falling back to local only", err)
|
|
57
|
+
mode = access.Mode{}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
web, err := webFS(cfg)
|
|
61
|
+
if err != nil {
|
|
62
|
+
fail(err)
|
|
63
|
+
}
|
|
64
|
+
srv, err := server.New(cfg, web, version, logf)
|
|
65
|
+
if err != nil {
|
|
66
|
+
fail(err)
|
|
67
|
+
}
|
|
68
|
+
if *open {
|
|
69
|
+
srv.OnReady = func() { openBrowser("http://localhost:" + strconv.Itoa(cfg.Port)) }
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
|
|
73
|
+
defer stop()
|
|
74
|
+
if err := srv.Run(ctx, mode); err != nil {
|
|
75
|
+
fail(err)
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
func logf(format string, args ...any) {
|
|
80
|
+
fmt.Fprintf(os.Stderr, "kanshi: "+format+"\n", args...)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
func fail(err error) {
|
|
84
|
+
logf("%v", err)
|
|
85
|
+
if ownConsole() {
|
|
86
|
+
// Double-clicked on Windows: the console closes with the process, so
|
|
87
|
+
// hold it open long enough to read why.
|
|
88
|
+
fmt.Fprint(os.Stderr, "Press Enter to close this window.")
|
|
89
|
+
_, _ = fmt.Scanln()
|
|
90
|
+
}
|
|
91
|
+
os.Exit(1)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// resourceDefaults gives a bare binary the same conservative budget the
|
|
95
|
+
// Compose file sets: two scheduler threads are plenty for a 5-second poll and
|
|
96
|
+
// one walk, and a soft memory limit makes an unusually large walk collect
|
|
97
|
+
// harder rather than grow. Either can be overridden with GOMAXPROCS and
|
|
98
|
+
// GOMEMLIMIT.
|
|
99
|
+
func resourceDefaults() {
|
|
100
|
+
if os.Getenv("GOMAXPROCS") == "" {
|
|
101
|
+
runtime.GOMAXPROCS(min(2, runtime.NumCPU()))
|
|
102
|
+
}
|
|
103
|
+
if os.Getenv("GOMEMLIMIT") == "" {
|
|
104
|
+
debug.SetMemoryLimit(128 << 20)
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
func webFS(cfg config.Config) (fs.FS, error) {
|
|
109
|
+
if cfg.WebDir != "" {
|
|
110
|
+
return os.DirFS(cfg.WebDir), nil
|
|
111
|
+
}
|
|
112
|
+
return fs.Sub(embedded, "web")
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// probe is the container health check. It runs in the same binary because the
|
|
116
|
+
// runtime image is scratch: there is no shell and no curl to call instead.
|
|
117
|
+
// Loopback is listened on in every access mode, so that is what it asks.
|
|
118
|
+
func probe(cfg config.Config) int {
|
|
119
|
+
client := &http.Client{Timeout: 4 * time.Second}
|
|
120
|
+
resp, err := client.Get(fmt.Sprintf("http://127.0.0.1:%d/healthz", cfg.Port))
|
|
121
|
+
if err != nil {
|
|
122
|
+
return 1
|
|
123
|
+
}
|
|
124
|
+
defer resp.Body.Close()
|
|
125
|
+
if resp.StatusCode != http.StatusOK {
|
|
126
|
+
return 1
|
|
127
|
+
}
|
|
128
|
+
return 0
|
|
129
|
+
}
|
package/main_other.go
ADDED
package/main_windows.go
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
package main
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"syscall"
|
|
5
|
+
"unsafe"
|
|
6
|
+
)
|
|
7
|
+
|
|
8
|
+
var (
|
|
9
|
+
kernel32 = syscall.NewLazyDLL("kernel32.dll")
|
|
10
|
+
procGetConsoleProcessList = kernel32.NewProc("GetConsoleProcessList")
|
|
11
|
+
shell32 = syscall.NewLazyDLL("shell32.dll")
|
|
12
|
+
procShellExecuteW = shell32.NewProc("ShellExecuteW")
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
// ownConsole reports that kanshi.exe has a console window to itself, which is
|
|
16
|
+
// what happens when it is double-clicked in Explorer rather than started from
|
|
17
|
+
// a terminal (where the shell shares the console).
|
|
18
|
+
func ownConsole() bool {
|
|
19
|
+
var ids [2]uint32
|
|
20
|
+
n, _, _ := procGetConsoleProcessList.Call(uintptr(unsafe.Pointer(&ids[0])), 2)
|
|
21
|
+
return n == 1
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
func openBrowser(url string) {
|
|
25
|
+
verb, _ := syscall.UTF16PtrFromString("open")
|
|
26
|
+
target, _ := syscall.UTF16PtrFromString(url)
|
|
27
|
+
const swShowNormal = 1
|
|
28
|
+
procShellExecuteW.Call(0, uintptr(unsafe.Pointer(verb)), uintptr(unsafe.Pointer(target)), 0, 0, swShowNormal)
|
|
29
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@yuuki824/kanshi",
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"description": "Launch the Kanshi homeserver dashboard with npx.",
|
|
5
|
+
"license": "AGPL-3.0-only",
|
|
6
|
+
"bin": {
|
|
7
|
+
"kanshi": "bin/kanshi.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"main.go",
|
|
11
|
+
"main_other.go",
|
|
12
|
+
"main_windows.go",
|
|
13
|
+
"go.mod",
|
|
14
|
+
"internal",
|
|
15
|
+
"!internal/**/*_test.go",
|
|
16
|
+
"web",
|
|
17
|
+
"bin",
|
|
18
|
+
"Dockerfile",
|
|
19
|
+
"docker-compose.yml",
|
|
20
|
+
"README.md",
|
|
21
|
+
"LICENSE",
|
|
22
|
+
".env.example"
|
|
23
|
+
],
|
|
24
|
+
"engines": {
|
|
25
|
+
"node": ">=18"
|
|
26
|
+
},
|
|
27
|
+
"scripts": {
|
|
28
|
+
"pack:check": "npm pack --dry-run",
|
|
29
|
+
"test": "node --check bin/kanshi.js"
|
|
30
|
+
},
|
|
31
|
+
"publishConfig": {
|
|
32
|
+
"access": "public"
|
|
33
|
+
},
|
|
34
|
+
"keywords": [
|
|
35
|
+
"docker",
|
|
36
|
+
"go",
|
|
37
|
+
"dashboard",
|
|
38
|
+
"monitoring",
|
|
39
|
+
"tailscale",
|
|
40
|
+
"homeserver",
|
|
41
|
+
"windows",
|
|
42
|
+
"linux"
|
|
43
|
+
],
|
|
44
|
+
"repository": {
|
|
45
|
+
"type": "git",
|
|
46
|
+
"url": "git+https://github.com/rene-roid/kanshi.git"
|
|
47
|
+
},
|
|
48
|
+
"homepage": "https://github.com/rene-roid/kanshi#readme"
|
|
49
|
+
}
|