mirror of
https://github.com/wavetermdev/waveterm
synced 2026-04-21 14:37:16 +00:00
Lots of fixes, big and small for processviewer (frontend and backend) (#3224)
The big fix is not spawning a goroutine per process. other fixes are more minor, but improve the quality and clean up some edge cases.
This commit is contained in:
parent
4a55b7ed0f
commit
158e404d80
9 changed files with 125 additions and 99 deletions
|
|
@ -156,6 +156,7 @@ tasks:
|
|||
- tsunami/go.mod
|
||||
- tsunami/go.sum
|
||||
- tsunami/**/*.go
|
||||
- package.json
|
||||
|
||||
build:schema:
|
||||
desc: Build the schema for configuration.
|
||||
|
|
@ -185,6 +186,7 @@ tasks:
|
|||
- "pkg/**/*.json"
|
||||
- "pkg/**/*.sh"
|
||||
- tsunami/**/*.go
|
||||
- package.json
|
||||
generates:
|
||||
- dist/bin/wavesrv.*
|
||||
|
||||
|
|
@ -289,6 +291,7 @@ tasks:
|
|||
sources:
|
||||
- "cmd/wsh/**/*.go"
|
||||
- "pkg/**/*.go"
|
||||
- package.json
|
||||
generates:
|
||||
- "dist/bin/wsh*"
|
||||
|
||||
|
|
|
|||
|
|
@ -107,6 +107,14 @@ Chords are shown with a + between the keys. You have 2 seconds to hit the 2nd ch
|
|||
| <Kbd k="Shift:PageUp"/> | Scroll up one page |
|
||||
| <Kbd k="Shift:PageDown"/>| Scroll down one page |
|
||||
|
||||
## Process Viewer Keybindings
|
||||
|
||||
| Key | Function |
|
||||
| ----------------------- | ------------------------------------- |
|
||||
| <Kbd k="Space"/> | Pause / resume live updates |
|
||||
| <Kbd k="Cmd:f"/> | Open process filter / search |
|
||||
| <Kbd k="Escape"/> | Close search bar |
|
||||
|
||||
## Customizeable Systemwide Global Hotkey
|
||||
|
||||
Wave allows setting a custom global hotkey to focus your most recent window from anywhere in your computer. For more information on this, see [the config docs](./config#customizable-systemwide-global-hotkey).
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ sidebar_position: 200
|
|||
Wave v0.14.5 introduces a new Process Viewer widget, Quake Mode for global hotkey, and several quality-of-life improvements.
|
||||
|
||||
- **Process Viewer** - New widget that displays running processes on local and remote machines, with CPU and memory usage, sortable columns, and the ability to send signals to processes
|
||||
- **Quake Mode** - The global hotkey (`app:globalhotkey`) now triggers a dedicated quake mode that drops a Wave window down from the top of the screen, similar to classic quake-style terminals
|
||||
- **Quake Mode** - The global hotkey (`app:globalhotkey`) now toggles a Wave window visible and invisible
|
||||
- **[bugfix] Settings Widget** - Fixed a bug where config files that didn't exist yet couldn't be created or edited from the Settings widget UI
|
||||
- **Drag & Drop Files into Terminal** - Drag files from Finder (macOS) or your file manager into a terminal block to paste their quoted path ([#746](https://github.com/wavetermdev/waveterm/issues/746))
|
||||
- New opt-in `app:showsplitbuttons` setting adds horizontal/vertical split buttons to block headers
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ title: "Widgets"
|
|||
|
||||
import { Kbd } from "@site/src/components/kbd";
|
||||
import { PlatformProvider, PlatformSelectorButton } from "@site/src/components/platformcontext";
|
||||
import { VersionBadge } from "@site/src/components/versionbadge";
|
||||
|
||||
<PlatformProvider>
|
||||
|
||||
|
|
@ -138,4 +139,10 @@ You can also save by pressing <Kbd k="Cmd:s" />.
|
|||
To exit **edit mode** without saving, click the cancel button to the right of the header.
|
||||
You can also exit without saving by pressing <Kbd k="Cmd:r" />.
|
||||
|
||||
### Process Viewer <VersionBadge version="v0.14.5" />
|
||||
|
||||
The Process Viewer shows a live list of running processes on any connected host. It is similar to `top` or `htop`, displaying PID, command, CPU%, and memory usage. On Linux it also shows process status and thread count.
|
||||
|
||||
Columns are sortable by clicking their headers. Right-clicking a row lets you send Unix signals (SIGTERM, SIGKILL, etc.) or copy the PID. You can filter the list by pressing <Kbd k="Cmd:f"/> and typing a search term. Press <Kbd k="Space"/> to pause live updates (useful when inspecting a specific process); press it again to resume.
|
||||
|
||||
</PlatformProvider>
|
||||
|
|
|
|||
|
|
@ -6,8 +6,8 @@ const UpgradeOnboardingModal_v0_14_5_Content = () => {
|
|||
<div className="flex flex-col items-start gap-6 w-full mb-4 unselectable">
|
||||
<div className="text-secondary leading-relaxed">
|
||||
<p className="mb-0">
|
||||
Wave v0.14.5 introduces a new Process Viewer widget, Quake Mode for the global hotkey, and several
|
||||
quality-of-life improvements.
|
||||
Wave v0.14.5 introduces a new Process Viewer widget, several quality-of-life improvements, and a
|
||||
fix for creating new config files from the Settings widget.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
|
@ -24,19 +24,6 @@ const UpgradeOnboardingModal_v0_14_5_Content = () => {
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex w-full items-start gap-4">
|
||||
<div className="flex-shrink-0">
|
||||
<i className="text-[24px] text-accent fa-solid fa-terminal"></i>
|
||||
</div>
|
||||
<div className="flex flex-col items-start gap-2 flex-1">
|
||||
<div className="text-foreground text-base font-semibold leading-[18px]">Quake Mode</div>
|
||||
<div className="text-secondary leading-5">
|
||||
The global hotkey (<code>app:globalhotkey</code>) now triggers a dedicated quake mode that
|
||||
drops a Wave window down from the top of the screen, similar to classic quake-style terminals.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex w-full items-start gap-4">
|
||||
<div className="flex-shrink-0">
|
||||
<i className="text-[24px] text-accent fa-solid fa-wrench"></i>
|
||||
|
|
@ -46,18 +33,21 @@ const UpgradeOnboardingModal_v0_14_5_Content = () => {
|
|||
<div className="text-secondary leading-5">
|
||||
<ul className="list-disc list-outside space-y-1 pl-5">
|
||||
<li>
|
||||
<strong>Drag & Drop Files into Terminal</strong> - Drag files from Finder or your
|
||||
file manager into a terminal to paste their quoted path
|
||||
<strong>Quake Mode</strong> — global hotkey (
|
||||
<code>app:globalhotkey</code>) now toggles a Wave window visible and invisible
|
||||
</li>
|
||||
<li>
|
||||
New opt-in <code>app:showsplitbuttons</code> setting adds split buttons to block
|
||||
headers
|
||||
<strong>Drag & Drop Files into Terminal</strong>
|
||||
to paste their quoted path
|
||||
</li>
|
||||
<li>Toggle the widgets sidebar from the View menu</li>
|
||||
<li>
|
||||
New <code>app:showsplitbuttons</code> setting adds split buttons to block headers
|
||||
</li>
|
||||
<li>Toggle the widgets sidebar on and off from the View menu</li>
|
||||
<li>F2 to rename the active tab</li>
|
||||
<li>Mouse back/forward buttons now navigate in web widgets</li>
|
||||
<li>
|
||||
<strong>[bugfix]</strong>{" "}Config files that didn't exist yet couldn't be
|
||||
<strong>[bugfix]</strong> Config files that didn't exist yet couldn't be
|
||||
created or edited from the Settings widget
|
||||
</li>
|
||||
</ul>
|
||||
|
|
|
|||
|
|
@ -311,6 +311,10 @@ export class ProcessViewerViewModel implements ViewModel {
|
|||
this.cancelPoll = null;
|
||||
this.startKeepAlive();
|
||||
} else {
|
||||
if (this.cancelPoll) {
|
||||
this.cancelPoll();
|
||||
}
|
||||
this.cancelPoll = null;
|
||||
this.startPolling();
|
||||
}
|
||||
}
|
||||
|
|
@ -470,7 +474,7 @@ const Columns: ColDef[] = [
|
|||
{ key: "pid", label: "PID", width: "70px", align: "right" },
|
||||
{ key: "command", label: "Command", width: "minmax(120px, 4fr)" },
|
||||
{ key: "status", label: "Status", width: "75px", hideOnPlatform: ["windows", "darwin"] },
|
||||
{ key: "user", label: "User", width: "80px" },
|
||||
{ key: "user", label: "User", width: "80px", hideOnPlatform: ["windows"] },
|
||||
{ key: "threads", label: "NT", tooltip: "Num Threads", width: "40px", align: "right", hideOnPlatform: ["windows"] },
|
||||
{ key: "cpu", label: "CPU%", width: "70px", align: "right" },
|
||||
{ key: "mem", label: "Memory", width: "90px", align: "right" },
|
||||
|
|
@ -603,9 +607,9 @@ const ProcessRow = React.memo(function ProcessRow({
|
|||
onSelect: (pid: number) => void;
|
||||
onContextMenu: (pid: number, e: React.MouseEvent) => void;
|
||||
}) {
|
||||
const cols = getColumns(platform);
|
||||
const visibleKeys = new Set(cols.map((c) => c.key));
|
||||
const gridTemplate = getGridTemplate(platform);
|
||||
const showStatus = platform !== "windows" && platform !== "darwin";
|
||||
const showThreads = platform !== "windows";
|
||||
if (proc.gone) {
|
||||
return (
|
||||
<div
|
||||
|
|
@ -618,9 +622,9 @@ const ProcessRow = React.memo(function ProcessRow({
|
|||
{proc.pid}
|
||||
</div>
|
||||
<div className="px-2 flex items-center truncate text-muted italic">(gone)</div>
|
||||
{showStatus && <div className="px-2 flex items-center truncate" />}
|
||||
<div className="px-2 flex items-center truncate" />
|
||||
{showThreads && <div className="px-2 flex items-center truncate" />}
|
||||
{visibleKeys.has("status") && <div className="px-2 flex items-center truncate" />}
|
||||
{visibleKeys.has("user") && <div className="px-2 flex items-center truncate" />}
|
||||
{visibleKeys.has("threads") && <div className="px-2 flex items-center truncate" />}
|
||||
<div className="px-2 flex items-center truncate" />
|
||||
<div className="px-2 flex items-center truncate" />
|
||||
</div>
|
||||
|
|
@ -637,11 +641,13 @@ const ProcessRow = React.memo(function ProcessRow({
|
|||
{proc.pid}
|
||||
</div>
|
||||
<div className="px-2 flex items-center truncate">{proc.command}</div>
|
||||
{showStatus && (
|
||||
{visibleKeys.has("status") && (
|
||||
<div className="px-2 flex items-center truncate text-secondary text-[11px]">{proc.status}</div>
|
||||
)}
|
||||
<div className="px-2 flex items-center truncate text-secondary">{proc.user}</div>
|
||||
{showThreads && (
|
||||
{visibleKeys.has("user") && (
|
||||
<div className="px-2 flex items-center truncate text-secondary">{proc.user}</div>
|
||||
)}
|
||||
{visibleKeys.has("threads") && (
|
||||
<div className="px-2 flex items-center truncate justify-end text-secondary font-mono text-[11px]">
|
||||
{proc.numthreads === -1 ? "-" : proc.numthreads >= 1 ? proc.numthreads : ""}
|
||||
</div>
|
||||
|
|
|
|||
4
package-lock.json
generated
4
package-lock.json
generated
|
|
@ -1,12 +1,12 @@
|
|||
{
|
||||
"name": "waveterm",
|
||||
"version": "0.14.5-beta.0",
|
||||
"version": "0.14.5-beta.1",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "waveterm",
|
||||
"version": "0.14.5-beta.0",
|
||||
"version": "0.14.5-beta.1",
|
||||
"hasInstallScript": true,
|
||||
"license": "Apache-2.0",
|
||||
"workspaces": [
|
||||
|
|
|
|||
|
|
@ -40,5 +40,15 @@
|
|||
"view": "sysinfo"
|
||||
}
|
||||
}
|
||||
},
|
||||
"defwidget@processviewer": {
|
||||
"display:order": -1,
|
||||
"icon": "list-tree",
|
||||
"label": "processes",
|
||||
"blockdef": {
|
||||
"meta": {
|
||||
"view": "processviewer"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ import (
|
|||
const (
|
||||
ProcCacheIdleTimeout = 60 * time.Second
|
||||
ProcCachePollInterval = 1 * time.Second
|
||||
ProcCacheMinSleep = 500 * time.Millisecond
|
||||
ProcViewerMaxLimit = 500
|
||||
)
|
||||
|
||||
|
|
@ -153,17 +154,48 @@ func (s *procCacheState) getWidgetPidOrder(widgetId string) ([]int32, int) {
|
|||
return entry.pids, entry.totalCount
|
||||
}
|
||||
|
||||
// updateCacheAndCheckIdle stores the latest snapshot, signals the first-ready channel if needed,
|
||||
// and checks whether the loop has been idle long enough to shut down.
|
||||
// Returns true if the loop should exit (idle timeout reached), false to continue.
|
||||
func (s *procCacheState) updateCacheAndCheckIdle(result *wshrpc.ProcessListResponse, firstDone *bool, firstReadyCh chan struct{}) bool {
|
||||
s.lock.Lock()
|
||||
defer s.lock.Unlock()
|
||||
if result != nil {
|
||||
s.cached = result
|
||||
}
|
||||
if !*firstDone {
|
||||
*firstDone = true
|
||||
close(firstReadyCh)
|
||||
s.ready = nil
|
||||
}
|
||||
if time.Since(s.lastRequest) < ProcCacheIdleTimeout {
|
||||
return false
|
||||
}
|
||||
s.cached = nil
|
||||
s.running = false
|
||||
s.lastCPUSamples = nil
|
||||
s.lastCPUEpoch = 0
|
||||
s.uidCache = nil
|
||||
s.widgetPidOrders = nil
|
||||
return true
|
||||
}
|
||||
|
||||
func (s *procCacheState) runLoop(firstReadyCh chan struct{}) {
|
||||
firstDone := false
|
||||
defer func() {
|
||||
panichandler.PanicHandler("procCache.runLoop", recover())
|
||||
if panichandler.PanicHandler("procCache.runLoop", recover()) == nil {
|
||||
return
|
||||
}
|
||||
s.lock.Lock()
|
||||
defer s.lock.Unlock()
|
||||
s.running = false
|
||||
if !firstDone {
|
||||
close(firstReadyCh)
|
||||
s.ready = nil
|
||||
}
|
||||
}()
|
||||
|
||||
numCPU := runtime.NumCPU()
|
||||
if numCPU < 1 {
|
||||
numCPU = 1
|
||||
}
|
||||
|
||||
firstDone := false
|
||||
numCPU := max(runtime.NumCPU(), 1)
|
||||
|
||||
for {
|
||||
iterStart := time.Now()
|
||||
|
|
@ -178,36 +210,21 @@ func (s *procCacheState) runLoop(firstReadyCh chan struct{}) {
|
|||
}
|
||||
}
|
||||
|
||||
s.lock.Lock()
|
||||
s.cached = result
|
||||
idleFor := time.Since(s.lastRequest)
|
||||
if !firstDone {
|
||||
firstDone = true
|
||||
close(firstReadyCh)
|
||||
s.ready = nil
|
||||
}
|
||||
if idleFor >= ProcCacheIdleTimeout {
|
||||
s.cached = nil
|
||||
s.running = false
|
||||
s.lastCPUSamples = nil
|
||||
s.lastCPUEpoch = 0
|
||||
s.uidCache = nil
|
||||
s.widgetPidOrders = nil
|
||||
s.lock.Unlock()
|
||||
if s.updateCacheAndCheckIdle(result, &firstDone, firstReadyCh) {
|
||||
return
|
||||
}
|
||||
s.lock.Unlock()
|
||||
|
||||
elapsed := time.Since(iterStart)
|
||||
if sleep := ProcCachePollInterval - elapsed; sleep > 0 {
|
||||
time.Sleep(sleep)
|
||||
}
|
||||
time.Sleep(max(ProcCacheMinSleep, ProcCachePollInterval-elapsed))
|
||||
}
|
||||
}
|
||||
|
||||
// lookupUID resolves a uid to a username, using the per-run cache to avoid
|
||||
// repeated syscalls for the same uid.
|
||||
func (s *procCacheState) lookupUID(uid uint32) string {
|
||||
if runtime.GOOS == "windows" {
|
||||
return ""
|
||||
}
|
||||
if s.uidCache == nil {
|
||||
s.uidCache = make(map[uint32]string)
|
||||
}
|
||||
|
|
@ -239,33 +256,25 @@ func (s *procCacheState) collectSnapshot(numCPU int) *wshrpc.ProcessListResponse
|
|||
s.lastCPUSamples = make(map[int32]cpuSample, len(procs))
|
||||
}
|
||||
|
||||
snap, _ := procinfo.MakeGlobalSnapshot()
|
||||
snap, err := procinfo.MakeGlobalSnapshot()
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
hasCPU := s.lastCPUEpoch > 1 // first epoch has no previous sample to diff against
|
||||
|
||||
// Build per-pid procinfo in parallel, then compute CPU% sequentially.
|
||||
type pidInfo struct {
|
||||
pid int32
|
||||
info *procinfo.ProcInfo
|
||||
}
|
||||
rawInfos := make([]pidInfo, len(procs))
|
||||
var wg sync.WaitGroup
|
||||
for i, p := range procs {
|
||||
i, p := i, p
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer func() {
|
||||
panichandler.PanicHandler("collectSnapshot:GetProcInfo", recover())
|
||||
wg.Done()
|
||||
}()
|
||||
pi, err := procinfo.GetProcInfo(ctx, snap, p.Pid)
|
||||
if err != nil {
|
||||
pi = nil
|
||||
}
|
||||
rawInfos[i] = pidInfo{pid: p.Pid, info: pi}
|
||||
}()
|
||||
pi, err := procinfo.GetProcInfo(ctx, snap, p.Pid)
|
||||
if err != nil {
|
||||
pi = nil
|
||||
}
|
||||
rawInfos[i] = pidInfo{pid: p.Pid, info: pi}
|
||||
}
|
||||
wg.Wait()
|
||||
|
||||
// Sample CPU times and compute CPU% sequentially to keep epoch accounting simple.
|
||||
cpuPcts := make(map[int32]float64, len(procs))
|
||||
|
|
@ -295,10 +304,11 @@ func (s *procCacheState) collectSnapshot(numCPU int) *wshrpc.ProcessListResponse
|
|||
}
|
||||
}
|
||||
|
||||
// Compute total memory for MemPct.
|
||||
// Compute total memory for MemPct and summary.
|
||||
vmStat, _ := mem.VirtualMemoryWithContext(ctx)
|
||||
var totalMem uint64
|
||||
if vm, err := mem.VirtualMemoryWithContext(ctx); err == nil {
|
||||
totalMem = vm.Total
|
||||
if vmStat != nil {
|
||||
totalMem = vmStat.Total
|
||||
}
|
||||
|
||||
var cpuSum float64
|
||||
|
|
@ -331,16 +341,7 @@ func (s *procCacheState) collectSnapshot(numCPU int) *wshrpc.ProcessListResponse
|
|||
infos = append(infos, info)
|
||||
}
|
||||
|
||||
summaryCh := make(chan wshrpc.ProcessSummary, 1)
|
||||
go func() {
|
||||
defer func() {
|
||||
if err := panichandler.PanicHandler("buildProcessSummary", recover()); err != nil {
|
||||
summaryCh <- wshrpc.ProcessSummary{Total: len(procs)}
|
||||
}
|
||||
}()
|
||||
summaryCh <- buildProcessSummary(ctx, len(procs), numCPU, cpuSum)
|
||||
}()
|
||||
summary := <-summaryCh
|
||||
summary := buildProcessSummary(ctx, len(procs), numCPU, cpuSum, vmStat)
|
||||
|
||||
return &wshrpc.ProcessListResponse{
|
||||
Processes: infos,
|
||||
|
|
@ -351,6 +352,10 @@ func (s *procCacheState) collectSnapshot(numCPU int) *wshrpc.ProcessListResponse
|
|||
}
|
||||
}
|
||||
|
||||
func bound(v, lo, hi int) int {
|
||||
return max(lo, min(v, hi))
|
||||
}
|
||||
|
||||
func computeCPUPct(t1, t2, elapsedSec float64) float64 {
|
||||
delta := (t2 - t1) / elapsedSec * 100
|
||||
if delta < 0 {
|
||||
|
|
@ -359,17 +364,17 @@ func computeCPUPct(t1, t2, elapsedSec float64) float64 {
|
|||
return delta
|
||||
}
|
||||
|
||||
func buildProcessSummary(ctx context.Context, total int, numCPU int, cpuSum float64) wshrpc.ProcessSummary {
|
||||
func buildProcessSummary(ctx context.Context, total int, numCPU int, cpuSum float64, vmStat *mem.VirtualMemoryStat) wshrpc.ProcessSummary {
|
||||
summary := wshrpc.ProcessSummary{Total: total, NumCPU: numCPU, CpuSum: cpuSum}
|
||||
if avg, err := load.AvgWithContext(ctx); err == nil {
|
||||
summary.Load1 = avg.Load1
|
||||
summary.Load5 = avg.Load5
|
||||
summary.Load15 = avg.Load15
|
||||
}
|
||||
if vm, err := mem.VirtualMemoryWithContext(ctx); err == nil {
|
||||
summary.MemTotal = vm.Total
|
||||
summary.MemUsed = vm.Used
|
||||
summary.MemFree = vm.Free
|
||||
if vmStat != nil {
|
||||
summary.MemTotal = vmStat.Total
|
||||
summary.MemUsed = vmStat.Used
|
||||
summary.MemFree = vmStat.Free
|
||||
}
|
||||
return summary
|
||||
}
|
||||
|
|
@ -544,10 +549,7 @@ func (impl *ServerImpl) RemoteProcessListCommand(ctx context.Context, data wshrp
|
|||
for _, p := range raw.Processes {
|
||||
pidMap[p.Pid] = p
|
||||
}
|
||||
start := data.Start
|
||||
if start >= len(pidOrder) {
|
||||
start = len(pidOrder)
|
||||
}
|
||||
start := bound(data.Start, 0, len(pidOrder))
|
||||
window := pidOrder[start:]
|
||||
if limit > 0 && len(window) > limit {
|
||||
window = window[:limit]
|
||||
|
|
|
|||
Loading…
Reference in a new issue