mirror of
https://gitee.com/mateos/mateclaw.git
synced 2026-09-14 03:33:43 +08:00
321 lines
14 KiB
Java
321 lines
14 KiB
Java
package vip.mate.tool.browser;
|
|
|
|
import cn.hutool.http.HttpUtil;
|
|
import lombok.extern.slf4j.Slf4j;
|
|
import org.springframework.stereotype.Service;
|
|
|
|
import java.io.BufferedReader;
|
|
import java.io.IOException;
|
|
import java.io.InputStreamReader;
|
|
import java.nio.charset.StandardCharsets;
|
|
import java.nio.file.Files;
|
|
import java.nio.file.Path;
|
|
import java.util.ArrayList;
|
|
import java.util.LinkedHashMap;
|
|
import java.util.List;
|
|
import java.util.Locale;
|
|
import java.util.Map;
|
|
import java.util.concurrent.TimeUnit;
|
|
|
|
/**
|
|
* Diagnoses why the browser tool might fail to launch on this host.
|
|
*
|
|
* <p>Runs a dry inventory — detecting system browsers, Playwright cache, Node runtime,
|
|
* required shared libraries on Linux, container / root context — without actually
|
|
* launching a session. Produces a structured report with actionable next steps.
|
|
*/
|
|
@Slf4j
|
|
@Service
|
|
public class BrowserDiagnosticsService {
|
|
|
|
private static final boolean IS_WINDOWS = System.getProperty("os.name", "")
|
|
.toLowerCase(Locale.ROOT).contains("win");
|
|
private static final boolean IS_LINUX = System.getProperty("os.name", "")
|
|
.toLowerCase(Locale.ROOT).contains("linux");
|
|
|
|
/** Shared libraries Chromium needs on Linux. Missing any is a hard block. */
|
|
private static final List<String> REQUIRED_LINUX_LIBS = List.of(
|
|
"libnss3", "libgbm", "libasound", "libxkbcommon", "libx11", "libxcomposite",
|
|
"libxdamage", "libxrandr", "libxfixes", "libatk", "libcups", "libpango"
|
|
);
|
|
|
|
private final BrowserProperties props;
|
|
|
|
public BrowserDiagnosticsService(BrowserProperties props) {
|
|
this.props = props;
|
|
}
|
|
|
|
public Report run() {
|
|
List<Finding> findings = new ArrayList<>();
|
|
findings.add(inspectEnvironment());
|
|
findings.add(inspectConfiguredCdp());
|
|
findings.add(inspectConfiguredPath());
|
|
findings.add(inspectEnvPath());
|
|
findings.add(inspectSystemBrowsers());
|
|
findings.add(inspectPlaywrightCache());
|
|
if (IS_LINUX) {
|
|
findings.add(inspectLinuxLibs());
|
|
}
|
|
|
|
String overall = deriveOverall(findings);
|
|
List<String> advice = deriveAdvice(findings);
|
|
return new Report(overall, findings, advice);
|
|
}
|
|
|
|
// ==================== Individual probes ====================
|
|
|
|
private Finding inspectEnvironment() {
|
|
Map<String, Object> data = new LinkedHashMap<>();
|
|
data.put("os", System.getProperty("os.name"));
|
|
data.put("arch", System.getProperty("os.arch"));
|
|
data.put("user", System.getProperty("user.name"));
|
|
data.put("container", BrowserLauncher.isRunningInContainer());
|
|
data.put("root", BrowserLauncher.isRunningAsRoot());
|
|
return new Finding("environment", Status.INFO, "Runtime environment", data, null);
|
|
}
|
|
|
|
private Finding inspectConfiguredCdp() {
|
|
String url = props.getCdpUrl();
|
|
if (url == null || url.isBlank()) {
|
|
return new Finding("config.cdp-url", Status.INFO, "mateclaw.browser.cdp-url not set", Map.of(), null);
|
|
}
|
|
Map<String, Object> data = new LinkedHashMap<>();
|
|
data.put("url", url);
|
|
try {
|
|
String resp = HttpUtil.get(stripTrailing(url) + "/json/version", 2000);
|
|
if (resp != null && resp.contains("webSocketDebuggerUrl")) {
|
|
data.put("reachable", true);
|
|
return new Finding("config.cdp-url", Status.OK,
|
|
"CDP endpoint reachable", data, null);
|
|
}
|
|
data.put("reachable", false);
|
|
data.put("response", resp);
|
|
return new Finding("config.cdp-url", Status.ERROR,
|
|
"CDP endpoint did not return a valid /json/version payload", data,
|
|
"Ensure Chrome was started with --remote-debugging-port=" + port(url) + " and /json/version is reachable.");
|
|
} catch (Exception e) {
|
|
data.put("error", e.getMessage());
|
|
return new Finding("config.cdp-url", Status.ERROR,
|
|
"CDP endpoint unreachable: " + e.getMessage(), data,
|
|
"Start Chrome with --remote-debugging-port or clear mateclaw.browser.cdp-url.");
|
|
}
|
|
}
|
|
|
|
private Finding inspectConfiguredPath() {
|
|
String path = props.getChromePath();
|
|
if (path == null || path.isBlank()) {
|
|
return new Finding("config.chrome-path", Status.INFO, "mateclaw.browser.chrome-path not set", Map.of(), null);
|
|
}
|
|
Path p = Path.of(path);
|
|
if (!Files.exists(p)) {
|
|
return new Finding("config.chrome-path", Status.ERROR,
|
|
"Configured chrome-path does not exist: " + path, Map.of("path", path),
|
|
"Install Chrome at that path, or clear mateclaw.browser.chrome-path.");
|
|
}
|
|
if (!Files.isExecutable(p)) {
|
|
return new Finding("config.chrome-path", Status.ERROR,
|
|
"Configured chrome-path is not executable: " + path, Map.of("path", path),
|
|
"chmod +x the binary, or point to the real chrome executable.");
|
|
}
|
|
return new Finding("config.chrome-path", Status.OK, "Configured chrome-path is valid",
|
|
Map.of("path", path), null);
|
|
}
|
|
|
|
private Finding inspectEnvPath() {
|
|
String env = System.getenv("CHROME_PATH");
|
|
if (env == null || env.isBlank()) {
|
|
return new Finding("env.CHROME_PATH", Status.INFO, "CHROME_PATH not set", Map.of(), null);
|
|
}
|
|
Path p = Path.of(env);
|
|
if (!Files.exists(p)) {
|
|
return new Finding("env.CHROME_PATH", Status.WARN,
|
|
"CHROME_PATH points to a missing file: " + env, Map.of("path", env),
|
|
"Fix CHROME_PATH or unset it to let auto-detection run.");
|
|
}
|
|
return new Finding("env.CHROME_PATH", Status.OK, "CHROME_PATH resolves to a real file",
|
|
Map.of("path", env), null);
|
|
}
|
|
|
|
private Finding inspectSystemBrowsers() {
|
|
List<Map<String, Object>> found = new ArrayList<>();
|
|
for (Path candidate : BrowserLauncher.systemBrowserCandidates()) {
|
|
if (Files.exists(candidate)) {
|
|
Map<String, Object> entry = new LinkedHashMap<>();
|
|
entry.put("path", candidate.toString());
|
|
entry.put("executable", Files.isExecutable(candidate));
|
|
found.add(entry);
|
|
}
|
|
}
|
|
if (found.isEmpty()) {
|
|
return new Finding("system.browsers", Status.WARN,
|
|
"No system Chrome / Edge / Brave found on well-known paths",
|
|
Map.of("scanned", BrowserLauncher.systemBrowserCandidates().stream().map(Path::toString).toList()),
|
|
installBrowserAdvice());
|
|
}
|
|
return new Finding("system.browsers", Status.OK,
|
|
"Found " + found.size() + " system browser(s)", Map.of("found", found), null);
|
|
}
|
|
|
|
private Finding inspectPlaywrightCache() {
|
|
Path cacheDir = playwrightCacheDir();
|
|
Map<String, Object> data = new LinkedHashMap<>();
|
|
data.put("cacheDir", cacheDir.toString());
|
|
if (!Files.isDirectory(cacheDir)) {
|
|
return new Finding("playwright.cache", Status.WARN,
|
|
"Playwright browser cache not found (bundled chromium unavailable)", data,
|
|
"Run `mvn exec:java -e -Dexec.mainClass=\"com.microsoft.playwright.CLI\" -Dexec.args=\"install chromium\"` " +
|
|
"or rely on system Chrome (recommended).");
|
|
}
|
|
try (var stream = Files.list(cacheDir)) {
|
|
List<String> entries = stream.map(p -> p.getFileName().toString()).filter(n -> n.contains("chromium")).toList();
|
|
data.put("chromiumBuilds", entries);
|
|
if (entries.isEmpty()) {
|
|
return new Finding("playwright.cache", Status.WARN,
|
|
"Playwright cache has no chromium build", data,
|
|
"Run playwright install chromium or use system Chrome.");
|
|
}
|
|
return new Finding("playwright.cache", Status.OK,
|
|
"Playwright bundled chromium available (" + entries.size() + " build(s))", data, null);
|
|
} catch (IOException e) {
|
|
data.put("error", e.getMessage());
|
|
return new Finding("playwright.cache", Status.WARN,
|
|
"Failed to read Playwright cache: " + e.getMessage(), data, null);
|
|
}
|
|
}
|
|
|
|
private Finding inspectLinuxLibs() {
|
|
// Pick the first available system browser to ldd-check.
|
|
Path binary = BrowserLauncher.systemBrowserCandidates().stream()
|
|
.filter(Files::exists).findFirst().orElse(null);
|
|
if (binary == null) {
|
|
return new Finding("linux.libs", Status.INFO, "No system browser to ldd-check", Map.of(), null);
|
|
}
|
|
try {
|
|
Process p = new ProcessBuilder("ldd", binary.toString())
|
|
.redirectErrorStream(true).start();
|
|
StringBuilder out = new StringBuilder();
|
|
try (BufferedReader r = new BufferedReader(new InputStreamReader(p.getInputStream(), StandardCharsets.UTF_8))) {
|
|
String line;
|
|
while ((line = r.readLine()) != null) {
|
|
out.append(line).append('\n');
|
|
}
|
|
}
|
|
p.waitFor(5, TimeUnit.SECONDS);
|
|
String dump = out.toString();
|
|
List<String> missing = new ArrayList<>();
|
|
for (String line : dump.split("\n")) {
|
|
if (line.contains("not found")) {
|
|
missing.add(line.trim());
|
|
}
|
|
}
|
|
if (!missing.isEmpty()) {
|
|
Map<String, Object> data = new LinkedHashMap<>();
|
|
data.put("binary", binary.toString());
|
|
data.put("missing", missing);
|
|
return new Finding("linux.libs", Status.ERROR,
|
|
"Chromium shared libraries missing — browser will fail to start", data,
|
|
"apt-get install -y " + String.join(" ", REQUIRED_LINUX_LIBS.stream().map(l -> l + "-dev").toList())
|
|
+ " (or your distro's equivalent)");
|
|
}
|
|
return new Finding("linux.libs", Status.OK, "All required shared libraries resolved",
|
|
Map.of("binary", binary.toString()), null);
|
|
} catch (Exception e) {
|
|
return new Finding("linux.libs", Status.INFO,
|
|
"ldd probe failed: " + e.getMessage(), Map.of(), null);
|
|
}
|
|
}
|
|
|
|
// ==================== Helpers ====================
|
|
|
|
private static Path playwrightCacheDir() {
|
|
String override = System.getenv("PLAYWRIGHT_BROWSERS_PATH");
|
|
if (override != null && !override.isBlank() && !"0".equals(override)) {
|
|
return Path.of(override);
|
|
}
|
|
String home = System.getProperty("user.home");
|
|
if (IS_WINDOWS) {
|
|
String local = System.getenv("LOCALAPPDATA");
|
|
if (local != null && !local.isBlank()) {
|
|
return Path.of(local, "ms-playwright");
|
|
}
|
|
return Path.of(home, "AppData", "Local", "ms-playwright");
|
|
}
|
|
if (System.getProperty("os.name", "").toLowerCase(Locale.ROOT).contains("mac")) {
|
|
return Path.of(home, "Library", "Caches", "ms-playwright");
|
|
}
|
|
return Path.of(home, ".cache", "ms-playwright");
|
|
}
|
|
|
|
private static String stripTrailing(String url) {
|
|
String s = url.trim();
|
|
while (s.endsWith("/")) s = s.substring(0, s.length() - 1);
|
|
return s;
|
|
}
|
|
|
|
private static String port(String url) {
|
|
int colon = url.lastIndexOf(':');
|
|
if (colon < 0) return "?";
|
|
String tail = url.substring(colon + 1);
|
|
int slash = tail.indexOf('/');
|
|
return slash > 0 ? tail.substring(0, slash) : tail;
|
|
}
|
|
|
|
private static String installBrowserAdvice() {
|
|
if (IS_WINDOWS) {
|
|
return "Install Chrome (https://www.google.com/chrome/) or Edge, or set mateclaw.browser.chrome-path.";
|
|
}
|
|
if (IS_LINUX) {
|
|
return "Install Chrome: `wget -q -O - https://dl.google.com/linux/linux_signing_key.pub | apt-key add - && apt install google-chrome-stable` or `apt install chromium`.";
|
|
}
|
|
return "Install Chrome or Edge, or set mateclaw.browser.chrome-path to point at a browser binary.";
|
|
}
|
|
|
|
private static String deriveOverall(List<Finding> findings) {
|
|
boolean hasError = findings.stream().anyMatch(f -> f.status == Status.ERROR);
|
|
boolean hasWarn = findings.stream().anyMatch(f -> f.status == Status.WARN);
|
|
boolean canLaunch = findings.stream().anyMatch(
|
|
f -> f.status == Status.OK && (f.id.equals("system.browsers")
|
|
|| f.id.equals("config.cdp-url") || f.id.equals("config.chrome-path")
|
|
|| f.id.equals("playwright.cache")));
|
|
if (canLaunch && !hasError) return "healthy";
|
|
if (canLaunch) return "warning";
|
|
if (hasError || !canLaunch) return "error";
|
|
return hasWarn ? "warning" : "healthy";
|
|
}
|
|
|
|
private static List<String> deriveAdvice(List<Finding> findings) {
|
|
List<String> out = new ArrayList<>();
|
|
for (Finding f : findings) {
|
|
if (f.advice != null && (f.status == Status.ERROR || f.status == Status.WARN)) {
|
|
out.add("[" + f.id + "] " + f.advice);
|
|
}
|
|
}
|
|
if (out.isEmpty()) {
|
|
out.add("Browser stack looks healthy.");
|
|
}
|
|
return out;
|
|
}
|
|
|
|
// ==================== Records ====================
|
|
|
|
public enum Status { OK, WARN, ERROR, INFO }
|
|
|
|
public record Finding(String id, Status status, String message, Map<String, Object> data, String advice) {}
|
|
|
|
public record Report(String overall, List<Finding> findings, List<String> advice) {}
|
|
|
|
/** Summarise the report as a short string suitable for logs / tool responses. */
|
|
public static String summarise(Report r) {
|
|
StringBuilder sb = new StringBuilder();
|
|
sb.append("Browser diagnostics: ").append(r.overall).append('\n');
|
|
for (Finding f : r.findings) {
|
|
sb.append(" [").append(f.status).append("] ").append(f.id).append(" — ").append(f.message).append('\n');
|
|
}
|
|
if (!r.advice.isEmpty()) {
|
|
sb.append("Advice:\n");
|
|
for (String a : r.advice) sb.append(" - ").append(a).append('\n');
|
|
}
|
|
return sb.toString();
|
|
}
|
|
}
|