@@ -46,18 +33,21 @@ const UpgradeOnboardingModal_v0_14_5_Content = () => {
-
- Drag & Drop Files into Terminal - Drag files from Finder or your
- file manager into a terminal to paste their quoted path
+ Quake Mode — global hotkey (
+
app:globalhotkey) now toggles a Wave window visible and invisible
-
- New opt-in
app:showsplitbuttons setting adds split buttons to block
- headers
+ Drag & Drop Files into Terminal
+ to paste their quoted path
- - Toggle the widgets sidebar from the View menu
+ -
+ New
app:showsplitbuttons setting adds split buttons to block headers
+
+ - Toggle the widgets sidebar on and off from the View menu
- F2 to rename the active tab
- Mouse back/forward buttons now navigate in web widgets
-
- [bugfix]{" "}Config files that didn't exist yet couldn't be
+ [bugfix] Config files that didn't exist yet couldn't be
created or edited from the Settings widget
diff --git a/frontend/app/view/processviewer/processviewer.tsx b/frontend/app/view/processviewer/processviewer.tsx
index a9d79c6ee..d7f284bcf 100644
--- a/frontend/app/view/processviewer/processviewer.tsx
+++ b/frontend/app/view/processviewer/processviewer.tsx
@@ -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 (
(gone)
- {showStatus &&
}
-
- {showThreads &&
}
+ {visibleKeys.has("status") &&
}
+ {visibleKeys.has("user") &&
}
+ {visibleKeys.has("threads") &&
}
@@ -637,11 +641,13 @@ const ProcessRow = React.memo(function ProcessRow({
{proc.pid}
{proc.command}
- {showStatus && (
+ {visibleKeys.has("status") && (
{proc.status}
)}
-
{proc.user}
- {showThreads && (
+ {visibleKeys.has("user") && (
+
{proc.user}
+ )}
+ {visibleKeys.has("threads") && (
{proc.numthreads === -1 ? "-" : proc.numthreads >= 1 ? proc.numthreads : ""}
diff --git a/package-lock.json b/package-lock.json
index 667722b00..9e4a67a93 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -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": [
diff --git a/pkg/wconfig/defaultconfig/widgets.json b/pkg/wconfig/defaultconfig/widgets.json
index 2d0524b7d..eb978d644 100644
--- a/pkg/wconfig/defaultconfig/widgets.json
+++ b/pkg/wconfig/defaultconfig/widgets.json
@@ -40,5 +40,15 @@
"view": "sysinfo"
}
}
+ },
+ "defwidget@processviewer": {
+ "display:order": -1,
+ "icon": "list-tree",
+ "label": "processes",
+ "blockdef": {
+ "meta": {
+ "view": "processviewer"
+ }
+ }
}
}
diff --git a/pkg/wshrpc/wshremote/processviewer.go b/pkg/wshrpc/wshremote/processviewer.go
index f4248d51f..dc6bb0ee6 100644
--- a/pkg/wshrpc/wshremote/processviewer.go
+++ b/pkg/wshrpc/wshremote/processviewer.go
@@ -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]