paseo-plugin-machine-status 0.1.1
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/LICENSE +21 -0
- package/README.md +367 -0
- package/client/charts.tsx +335 -0
- package/client/cleanup.tsx +169 -0
- package/client/docker.tsx +494 -0
- package/client/menu.tsx +181 -0
- package/client/pill-card.tsx +195 -0
- package/client/pill.tsx +155 -0
- package/client/processes.tsx +321 -0
- package/client/services.tsx +251 -0
- package/client/status.tsx +514 -0
- package/client/ui.tsx +263 -0
- package/index.client.tsx +31 -0
- package/index.server.ts +41 -0
- package/package.json +45 -0
- package/paseo-plugin.json +4 -0
- package/server/cleanup.ts +222 -0
- package/server/docker.ts +260 -0
- package/server/exec.ts +62 -0
- package/server/explain-rules.ts +188 -0
- package/server/explain.ts +95 -0
- package/server/metrics.ts +312 -0
- package/server/processes.ts +165 -0
- package/server/services.ts +428 -0
- package/shared/contracts.ts +170 -0
- package/shared/docker.ts +95 -0
- package/shared/explain.ts +24 -0
- package/shared/format.ts +39 -0
- package/shared/pill.ts +63 -0
- package/shared/services-classify.ts +190 -0
- package/shared/services.ts +52 -0
- package/shared/viz.ts +159 -0
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
import { useRpc, type PluginSurfaceProps } from "@getpaseo/plugin/client";
|
|
2
|
+
import { useQuery } from "@tanstack/react-query";
|
|
3
|
+
import React, { useState } from "react";
|
|
4
|
+
import { Linking, Pressable, ScrollView, Text, View } from "react-native";
|
|
5
|
+
import { readServices, type PortRow, type ServiceRow } from "../shared/services";
|
|
6
|
+
import { ActionButton, Card, useChromeStyles, type Chrome } from "./ui";
|
|
7
|
+
import { paletteFor, STATUS, type VizPalette } from "../shared/viz";
|
|
8
|
+
|
|
9
|
+
const SOURCE_DOT: Record<ServiceRow["source"], "agent" | "plain"> = {
|
|
10
|
+
agent: "agent",
|
|
11
|
+
"agent-child": "agent",
|
|
12
|
+
docker: "plain",
|
|
13
|
+
listener: "plain",
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export function ServicesTab({ theme, layout, host }: PluginSurfaceProps) {
|
|
17
|
+
const chrome: Chrome = { theme, compact: layout.compact };
|
|
18
|
+
const styles = useChromeStyles(chrome);
|
|
19
|
+
const palette = paletteFor(theme.colors.surface0);
|
|
20
|
+
const read = useRpc(readServices);
|
|
21
|
+
|
|
22
|
+
// Collection shells out to netstat, lsof, ps and docker, so it only runs while
|
|
23
|
+
// this tab is the one on screen.
|
|
24
|
+
const services = useQuery({
|
|
25
|
+
queryKey: ["machine-status", "services", host.id],
|
|
26
|
+
queryFn: () => read({}),
|
|
27
|
+
refetchInterval: 5_000,
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
const data = services.data;
|
|
31
|
+
const [showSystem, setShowSystem] = useState(false);
|
|
32
|
+
|
|
33
|
+
// The OS listens on plenty of ports that have nothing to do with development:
|
|
34
|
+
// AirPlay on 5000 and 7000, Handoff on an ephemeral port, Remote Desktop on
|
|
35
|
+
// 3283. They are classified, not deleted, so the toggle can bring them back.
|
|
36
|
+
const allPorts = data?.ports ?? [];
|
|
37
|
+
const systemPorts = allPorts.filter((row) => row.relevance === "system");
|
|
38
|
+
const ports = showSystem ? allPorts : allPorts.filter((row) => row.relevance === "dev");
|
|
39
|
+
|
|
40
|
+
const allServices = data?.services ?? [];
|
|
41
|
+
const shownServices = showSystem
|
|
42
|
+
? allServices
|
|
43
|
+
: allServices.filter((service) => service.relevance === "dev");
|
|
44
|
+
|
|
45
|
+
return (
|
|
46
|
+
<ScrollView style={styles.screen} contentContainerStyle={styles.content}>
|
|
47
|
+
{services.error ? (
|
|
48
|
+
<Card chrome={chrome} title="Ports unavailable">
|
|
49
|
+
<Text style={styles.danger}>{(services.error as Error).message}</Text>
|
|
50
|
+
</Card>
|
|
51
|
+
) : null}
|
|
52
|
+
|
|
53
|
+
<View style={styles.band}>
|
|
54
|
+
<View style={{ flex: 1.25 }}>
|
|
55
|
+
<Card
|
|
56
|
+
chrome={chrome}
|
|
57
|
+
title="Open ports"
|
|
58
|
+
subtitle={data ? `${ports.length} of ${allPorts.length} listening` : undefined}
|
|
59
|
+
>
|
|
60
|
+
<View style={[styles.rowWrap, { alignItems: "center" }]}>
|
|
61
|
+
<Legend chrome={chrome} palette={palette} color={STATUS.warning} label="reachable" />
|
|
62
|
+
<Legend chrome={chrome} palette={palette} color={palette.neutral} label="loopback only" />
|
|
63
|
+
<View style={{ flex: 1 }} />
|
|
64
|
+
<Pressable
|
|
65
|
+
accessibilityRole="switch"
|
|
66
|
+
accessibilityState={{ checked: showSystem }}
|
|
67
|
+
accessibilityLabel="Show system ports"
|
|
68
|
+
onPress={() => setShowSystem((value) => !value)}
|
|
69
|
+
style={({ pressed }) => [
|
|
70
|
+
styles.chip,
|
|
71
|
+
{
|
|
72
|
+
borderColor: showSystem ? chrome.theme.colors.accent : palette.borderStrong,
|
|
73
|
+
opacity: pressed ? 0.7 : 1,
|
|
74
|
+
},
|
|
75
|
+
]}
|
|
76
|
+
>
|
|
77
|
+
<Text style={showSystem ? styles.value : styles.muted}>
|
|
78
|
+
{showSystem ? "Hide" : "Show"} system ({systemPorts.length})
|
|
79
|
+
</Text>
|
|
80
|
+
</Pressable>
|
|
81
|
+
</View>
|
|
82
|
+
|
|
83
|
+
{!layout.compact ? (
|
|
84
|
+
<View style={[styles.processRow, { paddingVertical: 2 }]}>
|
|
85
|
+
<Text style={[styles.axis, { width: 62 }]}>PORT</Text>
|
|
86
|
+
<Text style={[styles.axis, { width: 44 }]}>PROTO</Text>
|
|
87
|
+
<Text style={[styles.axis, { width: 96 }]}>ADDRESS</Text>
|
|
88
|
+
<Text style={[styles.axis, { flex: 1 }]}>PROCESS</Text>
|
|
89
|
+
<Text style={[styles.axis, { width: 58, textAlign: "right" }]}>PID</Text>
|
|
90
|
+
<View style={{ width: 74 }} />
|
|
91
|
+
</View>
|
|
92
|
+
) : null}
|
|
93
|
+
|
|
94
|
+
{ports.map((row) => (
|
|
95
|
+
<PortLine key={row.id} chrome={chrome} palette={palette} row={row} />
|
|
96
|
+
))}
|
|
97
|
+
|
|
98
|
+
{data && ports.length === 0 ? (
|
|
99
|
+
<Text style={styles.muted}>
|
|
100
|
+
{allPorts.length === 0
|
|
101
|
+
? "Nothing is listening."
|
|
102
|
+
: "No development ports. Everything listening belongs to the OS."}
|
|
103
|
+
</Text>
|
|
104
|
+
) : null}
|
|
105
|
+
</Card>
|
|
106
|
+
</View>
|
|
107
|
+
|
|
108
|
+
<View style={{ flex: 1 }}>
|
|
109
|
+
<Card
|
|
110
|
+
chrome={chrome}
|
|
111
|
+
title="Running apps"
|
|
112
|
+
subtitle={data ? `${shownServices.length} services` : undefined}
|
|
113
|
+
>
|
|
114
|
+
{shownServices.map((service) => (
|
|
115
|
+
<View key={service.id} style={styles.processRow}>
|
|
116
|
+
<View
|
|
117
|
+
style={{
|
|
118
|
+
width: 8,
|
|
119
|
+
height: 8,
|
|
120
|
+
borderRadius: 4,
|
|
121
|
+
backgroundColor:
|
|
122
|
+
SOURCE_DOT[service.source] === "agent" ? palette.series[0] : STATUS.good,
|
|
123
|
+
}}
|
|
124
|
+
/>
|
|
125
|
+
<View style={{ flex: 1, gap: 2 }}>
|
|
126
|
+
<Text style={styles.value} numberOfLines={1}>
|
|
127
|
+
{service.name}
|
|
128
|
+
</Text>
|
|
129
|
+
<Text style={styles.muted} numberOfLines={1}>
|
|
130
|
+
{service.subtitle}
|
|
131
|
+
</Text>
|
|
132
|
+
</View>
|
|
133
|
+
<Text style={[styles.muted, { width: 78, textAlign: "right" }]} numberOfLines={1}>
|
|
134
|
+
{service.ports.length > 0 ? service.ports.map((port) => `:${port}`).join(" ") : "—"}
|
|
135
|
+
</Text>
|
|
136
|
+
</View>
|
|
137
|
+
))}
|
|
138
|
+
|
|
139
|
+
{data && shownServices.length === 0 ? (
|
|
140
|
+
<Text style={styles.muted}>No services detected.</Text>
|
|
141
|
+
) : null}
|
|
142
|
+
|
|
143
|
+
{(data?.notes ?? []).map((note) => (
|
|
144
|
+
<Text key={note} style={styles.muted}>
|
|
145
|
+
{note}
|
|
146
|
+
</Text>
|
|
147
|
+
))}
|
|
148
|
+
</Card>
|
|
149
|
+
</View>
|
|
150
|
+
</View>
|
|
151
|
+
</ScrollView>
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function Legend({
|
|
156
|
+
chrome,
|
|
157
|
+
color,
|
|
158
|
+
label,
|
|
159
|
+
}: {
|
|
160
|
+
chrome: Chrome;
|
|
161
|
+
palette: VizPalette;
|
|
162
|
+
color: string;
|
|
163
|
+
label: string;
|
|
164
|
+
}) {
|
|
165
|
+
const styles = useChromeStyles(chrome);
|
|
166
|
+
return (
|
|
167
|
+
<View style={{ flexDirection: "row", alignItems: "center", gap: 6 }}>
|
|
168
|
+
<View style={{ width: 8, height: 8, borderRadius: 4, backgroundColor: color }} />
|
|
169
|
+
<Text style={styles.muted}>{label}</Text>
|
|
170
|
+
</View>
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function PortLine({
|
|
175
|
+
chrome,
|
|
176
|
+
palette,
|
|
177
|
+
row,
|
|
178
|
+
}: {
|
|
179
|
+
chrome: Chrome;
|
|
180
|
+
palette: VizPalette;
|
|
181
|
+
row: PortRow;
|
|
182
|
+
}) {
|
|
183
|
+
const styles = useChromeStyles(chrome);
|
|
184
|
+
const exposed = row.scope !== "local";
|
|
185
|
+
const label = row.attribution?.label ?? (row.owner === "other" ? "another user" : "unknown");
|
|
186
|
+
|
|
187
|
+
const openIt = () => {
|
|
188
|
+
if (row.url) void Linking.openURL(row.url).catch(() => undefined);
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
if (chrome.compact) {
|
|
192
|
+
return (
|
|
193
|
+
<View style={[styles.processRow, { flexDirection: "column", alignItems: "stretch", gap: 4 }]}>
|
|
194
|
+
<View style={styles.row}>
|
|
195
|
+
<View style={{ flexDirection: "row", alignItems: "center", gap: 8 }}>
|
|
196
|
+
<Dot color={exposed ? STATUS.warning : palette.neutral} />
|
|
197
|
+
<Text style={styles.valueStrong}>{row.port}</Text>
|
|
198
|
+
<Text style={styles.muted}>{row.protocol.toUpperCase()}</Text>
|
|
199
|
+
</View>
|
|
200
|
+
{row.url ? (
|
|
201
|
+
<ActionButton chrome={chrome} variant="ghost" label="Open" onPress={openIt} />
|
|
202
|
+
) : null}
|
|
203
|
+
</View>
|
|
204
|
+
<Text style={styles.muted} numberOfLines={1}>
|
|
205
|
+
{row.address} · {label}
|
|
206
|
+
{row.pid === null ? "" : ` · PID ${row.pid}`}
|
|
207
|
+
</Text>
|
|
208
|
+
{row.attribution?.detail ? (
|
|
209
|
+
<Text style={styles.muted} numberOfLines={1}>
|
|
210
|
+
{row.attribution.detail}
|
|
211
|
+
</Text>
|
|
212
|
+
) : null}
|
|
213
|
+
</View>
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
return (
|
|
218
|
+
<View style={styles.processRow}>
|
|
219
|
+
<View style={{ width: 62, flexDirection: "row", alignItems: "center", gap: 8 }}>
|
|
220
|
+
<Dot color={exposed ? STATUS.warning : palette.neutral} />
|
|
221
|
+
<Text style={styles.valueStrong}>{row.port}</Text>
|
|
222
|
+
</View>
|
|
223
|
+
<Text style={[styles.muted, { width: 44 }]}>{row.protocol.toUpperCase()}</Text>
|
|
224
|
+
<Text style={[styles.muted, { width: 96 }]} numberOfLines={1}>
|
|
225
|
+
{row.address}
|
|
226
|
+
</Text>
|
|
227
|
+
<View style={{ flex: 1, gap: 2 }}>
|
|
228
|
+
<Text style={styles.value} numberOfLines={1}>
|
|
229
|
+
{label}
|
|
230
|
+
</Text>
|
|
231
|
+
{row.attribution?.detail ? (
|
|
232
|
+
<Text style={styles.muted} numberOfLines={1}>
|
|
233
|
+
{row.attribution.detail}
|
|
234
|
+
</Text>
|
|
235
|
+
) : null}
|
|
236
|
+
</View>
|
|
237
|
+
<Text style={[styles.muted, { width: 58, textAlign: "right" }]}>{row.pid ?? "—"}</Text>
|
|
238
|
+
<View style={{ width: 74, alignItems: "flex-end" }}>
|
|
239
|
+
{row.url ? (
|
|
240
|
+
<ActionButton chrome={chrome} variant="ghost" label="Open" onPress={openIt} />
|
|
241
|
+
) : (
|
|
242
|
+
<Text style={styles.muted}>—</Text>
|
|
243
|
+
)}
|
|
244
|
+
</View>
|
|
245
|
+
</View>
|
|
246
|
+
);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function Dot({ color }: { color: string }) {
|
|
250
|
+
return <View style={{ width: 8, height: 8, borderRadius: 4, backgroundColor: color }} />;
|
|
251
|
+
}
|