mirror of
https://github.com/par274/sharpemu.git
synced 2026-07-22 10:56:20 +08:00
627 lines
24 KiB
JavaScript
627 lines
24 KiB
JavaScript
/*
|
|
Copyright (C) 2026 SharpEmu Emulator Project
|
|
SPDX-License-Identifier: GPL-2.0-or-later
|
|
*/
|
|
|
|
"use strict";
|
|
|
|
const REGISTER_ORDER = [
|
|
"rax", "rcx", "rdx", "rbx", "rsp", "rbp", "rsi", "rdi",
|
|
"r8", "r9", "r10", "r11", "r12", "r13", "r14", "r15",
|
|
"rip", "rflags", "fs_base", "gs_base",
|
|
];
|
|
|
|
const ui = {
|
|
cursor: 0,
|
|
snapshot: null,
|
|
activity: [],
|
|
polling: false,
|
|
refreshing: false,
|
|
};
|
|
|
|
const byId = (id) => document.getElementById(id);
|
|
|
|
async function api(path, options = {}) {
|
|
const response = await fetch(path, {
|
|
headers: { "Content-Type": "application/json" },
|
|
cache: "no-store",
|
|
...options,
|
|
});
|
|
let payload;
|
|
try {
|
|
payload = await response.json();
|
|
} catch {
|
|
throw new Error(`Frontend returned HTTP ${response.status}.`);
|
|
}
|
|
if (!response.ok) {
|
|
throw new Error(payload.error || `Request failed with HTTP ${response.status}.`);
|
|
}
|
|
return payload;
|
|
}
|
|
|
|
async function pollSnapshot() {
|
|
if (ui.polling) return;
|
|
ui.polling = true;
|
|
try {
|
|
const snapshot = await api(`/api/snapshot?since=${ui.cursor}`);
|
|
applySnapshot(snapshot);
|
|
} catch (error) {
|
|
showToast("Frontend unavailable", error.message, "error");
|
|
} finally {
|
|
ui.polling = false;
|
|
}
|
|
}
|
|
|
|
function applySnapshot(snapshot) {
|
|
const previous = ui.snapshot;
|
|
const previousCursor = ui.cursor;
|
|
const newMessages = (snapshot.messages || []).filter((message) => message.id > previousCursor);
|
|
ui.snapshot = snapshot;
|
|
ui.cursor = snapshot.cursor;
|
|
if (newMessages.length) {
|
|
ui.activity.push(...newMessages);
|
|
ui.activity = ui.activity.slice(-350);
|
|
renderActivity();
|
|
for (const message of newMessages) {
|
|
handleActivityNotification(message);
|
|
}
|
|
}
|
|
|
|
if (!previous && snapshot.defaultEndpoint) {
|
|
byId("debug-host").value = snapshot.defaultEndpoint.host;
|
|
byId("debug-port").value = snapshot.defaultEndpoint.port;
|
|
}
|
|
|
|
renderConnection(snapshot);
|
|
renderEmulator(snapshot.emulator || {});
|
|
renderTarget(snapshot);
|
|
renderRegisters(snapshot.registers || {});
|
|
renderBreakpoints(snapshot.breakpoints || []);
|
|
updateControls(snapshot);
|
|
}
|
|
|
|
function renderConnection(snapshot) {
|
|
const status = byId("connection-status");
|
|
status.classList.toggle("connected", snapshot.connected);
|
|
status.classList.toggle("disconnected", !snapshot.connected);
|
|
byId("connection-state").textContent = snapshot.connected ? "Connected" : "Offline";
|
|
byId("connection-endpoint").textContent = snapshot.endpoint || "Not connected";
|
|
byId("connection-button-label").textContent = snapshot.connected ? "Disconnect" : "Connect";
|
|
byId("connection-button").classList.toggle("button-danger", snapshot.connected);
|
|
byId("connection-button").classList.toggle("button-primary", !snapshot.connected);
|
|
byId("protocol-version").textContent = snapshot.protocol || "—";
|
|
}
|
|
|
|
function renderEmulator(emulator) {
|
|
const statusWrap = byId("emulator-status").closest(".launch-status");
|
|
statusWrap.classList.toggle("running", Boolean(emulator.running));
|
|
statusWrap.classList.toggle("exited", !emulator.running && emulator.exitCode !== null && emulator.exitCode !== undefined);
|
|
if (emulator.running) {
|
|
byId("emulator-status").textContent = `SharpEmu running · PID ${emulator.pid}`;
|
|
byId("emulator-detail").textContent = emulator.eboot || "Launching game executable";
|
|
} else if (emulator.exitCode !== null && emulator.exitCode !== undefined) {
|
|
byId("emulator-status").textContent = `SharpEmu exited · code ${emulator.exitCode}`;
|
|
byId("emulator-detail").textContent = emulator.eboot || "The launched process has ended.";
|
|
} else {
|
|
byId("emulator-status").textContent = "No frontend-launched session";
|
|
byId("emulator-detail").textContent = "You can still attach to an emulator that is already running.";
|
|
}
|
|
if (emulator.eboot && document.activeElement !== byId("eboot-path")) {
|
|
byId("eboot-path").value = emulator.eboot;
|
|
}
|
|
byId("eboot-path").disabled = Boolean(emulator.running);
|
|
byId("browse-eboot-button").disabled = Boolean(emulator.running);
|
|
byId("launch-button").disabled = Boolean(emulator.running);
|
|
byId("stop-emulator-button").disabled = !emulator.running;
|
|
}
|
|
|
|
function renderTarget(snapshot) {
|
|
const state = snapshot.state || "Disconnected";
|
|
const stop = snapshot.lastStop || {};
|
|
const registers = snapshot.registers || {};
|
|
byId("toolbar-target-state").textContent = state;
|
|
const badge = byId("target-state-badge");
|
|
badge.textContent = state;
|
|
badge.className = `state-badge ${state.toLowerCase()}`;
|
|
byId("target-address").textContent = stop.address || registers.rip || "—";
|
|
byId("stop-reason").textContent = stop.reason || "—";
|
|
byId("frame-kind").textContent = stop.frameKind || "—";
|
|
byId("frame-label").textContent = stop.frameLabel || "—";
|
|
byId("stop-result").textContent = stop.result || "—";
|
|
byId("opcode-bytes").textContent = stop.opcodeBytes || "—";
|
|
const detailWrap = byId("stop-detail-wrap");
|
|
detailWrap.classList.toggle("hidden", !stop.detail);
|
|
byId("stop-detail").textContent = stop.detail || "";
|
|
renderStallAnalysis(stop.analysis);
|
|
}
|
|
|
|
function renderStallAnalysis(analysis) {
|
|
const panel = byId("stall-analysis");
|
|
if (!analysis) {
|
|
panel.classList.add("hidden");
|
|
return;
|
|
}
|
|
panel.classList.remove("hidden");
|
|
byId("stall-analysis-title").textContent = analysis.title || "Execution stall detected";
|
|
byId("stall-confidence").textContent = `${analysis.confidence || "Medium"} confidence`;
|
|
byId("stall-summary").textContent = analysis.summary || "The guest is not making forward progress.";
|
|
byId("stall-cause").textContent = analysis.cause || "The repeated path is not changing the state checked by the guest.";
|
|
byId("stall-fix").textContent = analysis.fix || "Trace the repeated import and implement its missing state transition.";
|
|
|
|
const actions = (analysis.actions || []).map((text) => createTextElement("li", text));
|
|
byId("stall-actions").replaceChildren(...actions);
|
|
const evidence = (analysis.evidence || []).map((text) => createTextElement("li", text));
|
|
byId("stall-evidence").replaceChildren(...evidence);
|
|
}
|
|
|
|
function renderRegisters(registers) {
|
|
const grid = byId("register-grid");
|
|
if (!Object.keys(registers).length) {
|
|
grid.className = "register-grid empty-state";
|
|
grid.replaceChildren(createTextElement("p", "Pause the target to inspect registers."));
|
|
return;
|
|
}
|
|
|
|
grid.className = "register-grid";
|
|
const fragment = document.createDocumentFragment();
|
|
for (const name of REGISTER_ORDER) {
|
|
const value = registers[name] ?? "—";
|
|
const item = document.createElement("div");
|
|
item.className = `register-item ${["rip", "rflags", "fs_base", "gs_base"].includes(name) ? "special" : ""}`;
|
|
const label = createTextElement("span", name);
|
|
label.className = "register-name";
|
|
const button = createTextElement("button", value);
|
|
button.type = "button";
|
|
button.className = "register-value";
|
|
button.dataset.register = name;
|
|
button.title = `Edit ${name}`;
|
|
button.addEventListener("click", () => openRegisterDialog(name, value));
|
|
item.append(label, button);
|
|
fragment.append(item);
|
|
}
|
|
grid.replaceChildren(fragment);
|
|
}
|
|
|
|
function renderBreakpoints(breakpoints) {
|
|
byId("breakpoint-count").textContent = breakpoints.length;
|
|
const body = byId("breakpoint-table");
|
|
if (!breakpoints.length) {
|
|
const row = document.createElement("tr");
|
|
row.className = "empty-row";
|
|
const cell = createTextElement("td", "No breakpoints configured.");
|
|
cell.colSpan = 6;
|
|
row.append(cell);
|
|
body.replaceChildren(row);
|
|
return;
|
|
}
|
|
|
|
const fragment = document.createDocumentFragment();
|
|
for (const breakpoint of breakpoints) {
|
|
const row = document.createElement("tr");
|
|
const enabledCell = document.createElement("td");
|
|
const toggle = document.createElement("input");
|
|
toggle.type = "checkbox";
|
|
toggle.className = "switch";
|
|
toggle.checked = Boolean(breakpoint.enabled);
|
|
toggle.title = toggle.checked ? "Disable breakpoint" : "Enable breakpoint";
|
|
toggle.addEventListener("change", async () => {
|
|
toggle.disabled = true;
|
|
try {
|
|
await sendCommand({ command: "enable-breakpoint", id: breakpoint.id, enabled: toggle.checked });
|
|
await refreshBreakpoints();
|
|
} catch (error) {
|
|
toggle.checked = !toggle.checked;
|
|
showToast("Breakpoint update failed", error.message, "error");
|
|
} finally {
|
|
toggle.disabled = false;
|
|
}
|
|
});
|
|
enabledCell.append(toggle);
|
|
|
|
const idCell = createTextElement("td", String(breakpoint.id));
|
|
const kindCell = document.createElement("td");
|
|
const kind = createTextElement("span", breakpoint.kind);
|
|
kind.className = "kind-pill";
|
|
kindCell.append(kind);
|
|
const addressCell = createTextElement("td", breakpoint.address);
|
|
const lengthCell = createTextElement("td", String(breakpoint.length));
|
|
const actionCell = document.createElement("td");
|
|
const remove = createTextElement("button", "Remove");
|
|
remove.type = "button";
|
|
remove.className = "row-action";
|
|
remove.addEventListener("click", async () => {
|
|
remove.disabled = true;
|
|
try {
|
|
await sendCommand({ command: "remove-breakpoint", id: breakpoint.id });
|
|
await refreshBreakpoints();
|
|
showToast("Breakpoint removed", `Breakpoint ${breakpoint.id} was removed.`, "success");
|
|
} catch (error) {
|
|
showToast("Remove failed", error.message, "error");
|
|
} finally {
|
|
remove.disabled = false;
|
|
}
|
|
});
|
|
actionCell.append(remove);
|
|
row.append(enabledCell, idCell, kindCell, addressCell, lengthCell, actionCell);
|
|
fragment.append(row);
|
|
}
|
|
body.replaceChildren(fragment);
|
|
}
|
|
|
|
function updateControls(snapshot) {
|
|
const connected = Boolean(snapshot.connected);
|
|
const state = String(snapshot.state || "").toLowerCase();
|
|
const paused = connected && state === "paused";
|
|
const running = connected && state === "running";
|
|
byId("continue-button").disabled = !paused;
|
|
byId("step-button").disabled = !paused;
|
|
byId("pause-button").disabled = !running;
|
|
byId("refresh-button").disabled = !connected;
|
|
for (const id of [
|
|
"breakpoint-address", "breakpoint-kind", "breakpoint-length",
|
|
"memory-address", "memory-length", "memory-write-address", "memory-write-bytes",
|
|
"raw-command-input",
|
|
]) {
|
|
byId(id).disabled = !connected || (["memory-address", "memory-length", "memory-write-address", "memory-write-bytes"].includes(id) && !paused);
|
|
}
|
|
byId("breakpoint-form").querySelector("button").disabled = !connected;
|
|
byId("memory-read-form").querySelector("button").disabled = !paused;
|
|
byId("memory-write-form").querySelector("button").disabled = !paused;
|
|
byId("raw-command-form").querySelector("button").disabled = !connected;
|
|
}
|
|
|
|
async function sendCommand(request) {
|
|
const payload = await api("/api/command", {
|
|
method: "POST",
|
|
body: JSON.stringify({ request }),
|
|
});
|
|
const response = payload.response;
|
|
if (!response?.ok) {
|
|
throw new Error(response?.error || `${request.command} failed.`);
|
|
}
|
|
await pollSnapshot();
|
|
return response;
|
|
}
|
|
|
|
async function refreshTarget() {
|
|
if (!ui.snapshot?.connected || ui.refreshing) return;
|
|
ui.refreshing = true;
|
|
byId("refresh-button").disabled = true;
|
|
try {
|
|
const status = await sendCommand({ command: "status" });
|
|
if (String(status.data?.state).toLowerCase() === "paused") {
|
|
await sendCommand({ command: "registers" });
|
|
}
|
|
await sendCommand({ command: "list-breakpoints" });
|
|
} catch (error) {
|
|
showToast("Refresh failed", error.message, "error");
|
|
} finally {
|
|
ui.refreshing = false;
|
|
await pollSnapshot();
|
|
}
|
|
}
|
|
|
|
async function refreshBreakpoints() {
|
|
await sendCommand({ command: "list-breakpoints" });
|
|
}
|
|
|
|
async function connectOrDisconnect(event) {
|
|
event.preventDefault();
|
|
const button = byId("connection-button");
|
|
button.disabled = true;
|
|
try {
|
|
if (ui.snapshot?.connected) {
|
|
const snapshot = await api("/api/disconnect", { method: "POST", body: "{}" });
|
|
applySnapshot(snapshot);
|
|
showToast("Disconnected", "The debugger connection was closed.");
|
|
return;
|
|
}
|
|
const host = byId("debug-host").value.trim();
|
|
const port = Number.parseInt(byId("debug-port").value, 10);
|
|
const snapshot = await api("/api/connect", {
|
|
method: "POST",
|
|
body: JSON.stringify({ host, port }),
|
|
});
|
|
applySnapshot(snapshot);
|
|
showToast("Debugger connected", `Attached to ${host}:${port}.`, "success");
|
|
await refreshTarget();
|
|
} catch (error) {
|
|
showToast("Connection failed", error.message, "error");
|
|
await pollSnapshot();
|
|
} finally {
|
|
button.disabled = false;
|
|
}
|
|
}
|
|
|
|
async function browseForEboot() {
|
|
const button = byId("browse-eboot-button");
|
|
button.disabled = true;
|
|
const previousLabel = button.textContent;
|
|
button.textContent = "Choosing…";
|
|
try {
|
|
const result = await api("/api/select-eboot", {
|
|
method: "POST",
|
|
body: JSON.stringify({ initialPath: byId("eboot-path").value.trim() || null }),
|
|
});
|
|
if (result.path) {
|
|
byId("eboot-path").value = result.path;
|
|
byId("eboot-path").focus();
|
|
byId("eboot-path").setSelectionRange(result.path.length, result.path.length);
|
|
}
|
|
} catch (error) {
|
|
showToast("File picker unavailable", error.message, "error");
|
|
} finally {
|
|
button.textContent = previousLabel;
|
|
button.disabled = Boolean(ui.snapshot?.emulator?.running);
|
|
}
|
|
}
|
|
|
|
async function launchEboot(event) {
|
|
event.preventDefault();
|
|
const ebootPath = byId("eboot-path").value.trim();
|
|
const debugPort = Number.parseInt(byId("debug-port").value, 10);
|
|
const button = byId("launch-button");
|
|
const previousHtml = button.innerHTML;
|
|
button.disabled = true;
|
|
button.textContent = "Starting SharpEmu…";
|
|
showToast("Launching SharpEmu", "Waiting for the local debug server to become ready.");
|
|
try {
|
|
const snapshot = await api("/api/launch", {
|
|
method: "POST",
|
|
body: JSON.stringify({ ebootPath, debugPort }),
|
|
});
|
|
byId("debug-host").value = "127.0.0.1";
|
|
applySnapshot(snapshot);
|
|
showToast("Launch complete", "SharpEmu is running and the debugger attached automatically.", "success");
|
|
await refreshTarget();
|
|
} catch (error) {
|
|
showToast("Launch failed", error.message, "error");
|
|
await pollSnapshot();
|
|
} finally {
|
|
button.innerHTML = previousHtml;
|
|
button.disabled = Boolean(ui.snapshot?.emulator?.running);
|
|
}
|
|
}
|
|
|
|
async function stopEmulator() {
|
|
if (!window.confirm("Stop the SharpEmu process launched by this frontend?")) return;
|
|
const button = byId("stop-emulator-button");
|
|
button.disabled = true;
|
|
button.textContent = "Stopping…";
|
|
try {
|
|
const snapshot = await api("/api/stop-emulator", { method: "POST", body: "{}" });
|
|
applySnapshot(snapshot);
|
|
showToast("SharpEmu stopped", "The frontend-launched emulator process was stopped.");
|
|
} catch (error) {
|
|
showToast("Stop failed", error.message, "error");
|
|
} finally {
|
|
button.textContent = "Stop";
|
|
button.disabled = !ui.snapshot?.emulator?.running;
|
|
}
|
|
}
|
|
|
|
async function executionCommand(command, label) {
|
|
try {
|
|
await sendCommand({ command });
|
|
showToast(label, command === "pause" ? "Pause requested at the next frame boundary." : `${label} command accepted.`, "success");
|
|
} catch (error) {
|
|
showToast(`${label} failed`, error.message, "error");
|
|
}
|
|
}
|
|
|
|
function openRegisterDialog(name, value) {
|
|
if (String(ui.snapshot?.state).toLowerCase() !== "paused") return;
|
|
byId("register-name").value = name;
|
|
byId("register-value").value = value;
|
|
byId("register-dialog").showModal();
|
|
byId("register-value").focus();
|
|
byId("register-value").select();
|
|
}
|
|
|
|
async function applyRegister(event) {
|
|
event.preventDefault();
|
|
const name = byId("register-name").value;
|
|
const value = byId("register-value").value.trim();
|
|
try {
|
|
await sendCommand({ command: "set-register", register: name, value });
|
|
await sendCommand({ command: "registers" });
|
|
byId("register-dialog").close();
|
|
showToast("Register updated", `${name.toUpperCase()} is now ${value}.`, "success");
|
|
} catch (error) {
|
|
showToast("Register update failed", error.message, "error");
|
|
}
|
|
}
|
|
|
|
async function addBreakpoint(event) {
|
|
event.preventDefault();
|
|
const address = byId("breakpoint-address").value.trim();
|
|
const kind = byId("breakpoint-kind").value;
|
|
const length = Number.parseInt(byId("breakpoint-length").value, 10);
|
|
try {
|
|
await sendCommand({ command: "add-breakpoint", address, kind, length });
|
|
await refreshBreakpoints();
|
|
byId("breakpoint-address").value = "";
|
|
showToast("Breakpoint added", `${kind} breakpoint created at ${address}.`, "success");
|
|
} catch (error) {
|
|
showToast("Breakpoint failed", error.message, "error");
|
|
}
|
|
}
|
|
|
|
async function readMemory(event) {
|
|
event.preventDefault();
|
|
const address = byId("memory-address").value.trim();
|
|
const length = Number.parseInt(byId("memory-length").value, 10);
|
|
try {
|
|
const response = await sendCommand({ command: "read-memory", address, length });
|
|
const data = response.data || {};
|
|
byId("memory-view").textContent = formatHexDump(data.address || address, data.bytes || "");
|
|
byId("memory-meta").textContent = `${data.length || 0} bytes from ${data.address || address}`;
|
|
if (!byId("memory-write-address").value) byId("memory-write-address").value = data.address || address;
|
|
} catch (error) {
|
|
showToast("Memory read failed", error.message, "error");
|
|
}
|
|
}
|
|
|
|
async function writeMemory(event) {
|
|
event.preventDefault();
|
|
const address = byId("memory-write-address").value.trim();
|
|
const bytes = byId("memory-write-bytes").value.replace(/\s+/g, "");
|
|
if (!bytes || bytes.length % 2 || !/^[0-9a-f]+$/i.test(bytes)) {
|
|
showToast("Invalid bytes", "Enter an even number of hexadecimal digits.", "error");
|
|
return;
|
|
}
|
|
try {
|
|
const response = await sendCommand({ command: "write-memory", address, bytes });
|
|
showToast("Memory written", `${response.data?.written || bytes.length / 2} bytes written at ${address}.`, "success");
|
|
if (byId("memory-address").value.trim() === address) {
|
|
byId("memory-read-form").requestSubmit();
|
|
}
|
|
} catch (error) {
|
|
showToast("Memory write failed", error.message, "error");
|
|
}
|
|
}
|
|
|
|
function formatHexDump(startAddress, hex) {
|
|
const clean = String(hex).replace(/\s+/g, "");
|
|
if (!clean) return "No bytes returned.";
|
|
const bytes = clean.match(/.{1,2}/g) || [];
|
|
let base = 0n;
|
|
try { base = BigInt(startAddress); } catch { /* display from zero */ }
|
|
const lines = [];
|
|
for (let offset = 0; offset < bytes.length; offset += 16) {
|
|
const chunk = bytes.slice(offset, offset + 16);
|
|
const address = `0x${(base + BigInt(offset)).toString(16).toUpperCase().padStart(16, "0")}`;
|
|
const left = chunk.slice(0, 8).join(" ").padEnd(23, " ");
|
|
const right = chunk.slice(8).join(" ").padEnd(23, " ");
|
|
const ascii = chunk.map((value) => {
|
|
const code = Number.parseInt(value, 16);
|
|
return code >= 32 && code <= 126 ? String.fromCharCode(code) : ".";
|
|
}).join("");
|
|
lines.push(`${address} ${left} ${right} |${ascii.padEnd(16, " ")}|`);
|
|
}
|
|
return lines.join("\n");
|
|
}
|
|
|
|
async function sendRawCommand(event) {
|
|
event.preventDefault();
|
|
try {
|
|
const request = JSON.parse(byId("raw-command-input").value);
|
|
if (!request || Array.isArray(request) || typeof request !== "object") throw new Error("Request must be a JSON object.");
|
|
await sendCommand(request);
|
|
showToast("Raw request sent", request.command || "Request completed.", "success");
|
|
} catch (error) {
|
|
showToast("Raw request failed", error.message, "error");
|
|
}
|
|
}
|
|
|
|
function renderActivity() {
|
|
const stream = byId("activity-stream");
|
|
const query = byId("activity-search").value.trim().toLowerCase();
|
|
const messages = query
|
|
? ui.activity.filter((item) => `${item.kind} ${item.summary} ${JSON.stringify(item.payload || "")}`.toLowerCase().includes(query))
|
|
: ui.activity;
|
|
if (!messages.length) {
|
|
const empty = createTextElement("div", query ? "No activity matches this filter." : "Protocol events and commands will appear here.");
|
|
empty.className = "activity-empty";
|
|
stream.replaceChildren(empty);
|
|
return;
|
|
}
|
|
|
|
const nearBottom = stream.scrollHeight - stream.scrollTop - stream.clientHeight < 70;
|
|
const fragment = document.createDocumentFragment();
|
|
for (const item of messages.slice(-250)) {
|
|
const row = document.createElement("div");
|
|
row.className = `activity-row ${item.kind}`;
|
|
const timestamp = createTextElement("span", item.time);
|
|
timestamp.className = "activity-time";
|
|
const kind = createTextElement("span", item.kind);
|
|
kind.className = "activity-kind";
|
|
const summary = document.createElement("div");
|
|
summary.className = "activity-summary";
|
|
summary.append(createTextElement("span", item.summary));
|
|
if (item.payload !== undefined) {
|
|
const details = document.createElement("details");
|
|
details.append(createTextElement("summary", "View payload"));
|
|
const pre = createTextElement("pre", JSON.stringify(item.payload, null, 2));
|
|
details.append(pre);
|
|
summary.append(details);
|
|
}
|
|
row.append(timestamp, kind, summary);
|
|
fragment.append(row);
|
|
}
|
|
stream.replaceChildren(fragment);
|
|
if (nearBottom || !query) stream.scrollTop = stream.scrollHeight;
|
|
}
|
|
|
|
function handleActivityNotification(message) {
|
|
if (message.kind === "event" && message.summary === "stopped") {
|
|
const reason = message.payload?.reason || "Target stopped";
|
|
showToast("Target paused", `${reason} at ${message.payload?.address || "unknown address"}.`);
|
|
} else if (message.kind === "event" && message.summary === "terminated") {
|
|
showToast("Target terminated", "The emulation run has completed.");
|
|
} else if (message.kind === "emulator" && message.summary.startsWith("Emulator exited")) {
|
|
showToast("Emulator exited", message.summary);
|
|
}
|
|
}
|
|
|
|
function showToast(title, message, tone = "") {
|
|
const toast = document.createElement("div");
|
|
toast.className = `toast ${tone}`;
|
|
toast.append(createTextElement("strong", title), createTextElement("span", message));
|
|
byId("toast-stack").append(toast);
|
|
window.setTimeout(() => toast.remove(), 4300);
|
|
}
|
|
|
|
function createTextElement(tag, text) {
|
|
const element = document.createElement(tag);
|
|
element.textContent = text == null ? "" : String(text);
|
|
return element;
|
|
}
|
|
|
|
function bindEvents() {
|
|
byId("connection-form").addEventListener("submit", connectOrDisconnect);
|
|
byId("launch-form").addEventListener("submit", launchEboot);
|
|
byId("browse-eboot-button").addEventListener("click", browseForEboot);
|
|
byId("stop-emulator-button").addEventListener("click", stopEmulator);
|
|
byId("continue-button").addEventListener("click", () => executionCommand("continue", "Continue"));
|
|
byId("pause-button").addEventListener("click", () => executionCommand("pause", "Pause"));
|
|
byId("step-button").addEventListener("click", () => executionCommand("step", "Step"));
|
|
byId("refresh-button").addEventListener("click", refreshTarget);
|
|
byId("breakpoint-form").addEventListener("submit", addBreakpoint);
|
|
byId("memory-read-form").addEventListener("submit", readMemory);
|
|
byId("memory-write-form").addEventListener("submit", writeMemory);
|
|
byId("raw-command-form").addEventListener("submit", sendRawCommand);
|
|
byId("register-form").addEventListener("submit", applyRegister);
|
|
byId("register-dialog-close").addEventListener("click", () => byId("register-dialog").close());
|
|
byId("register-cancel").addEventListener("click", () => byId("register-dialog").close());
|
|
byId("activity-search").addEventListener("input", renderActivity);
|
|
byId("clear-activity").addEventListener("click", () => {
|
|
ui.activity = [];
|
|
renderActivity();
|
|
});
|
|
document.addEventListener("keydown", (event) => {
|
|
if (["INPUT", "TEXTAREA", "SELECT"].includes(document.activeElement?.tagName)) return;
|
|
if (event.key === "F5") {
|
|
event.preventDefault();
|
|
if (!byId("continue-button").disabled) executionCommand("continue", "Continue");
|
|
} else if (event.key === "F6") {
|
|
event.preventDefault();
|
|
if (!byId("pause-button").disabled) executionCommand("pause", "Pause");
|
|
} else if (event.key === "F10") {
|
|
event.preventDefault();
|
|
if (!byId("step-button").disabled) executionCommand("step", "Step");
|
|
}
|
|
});
|
|
}
|
|
|
|
async function initialize() {
|
|
bindEvents();
|
|
await pollSnapshot();
|
|
window.setInterval(pollSnapshot, 700);
|
|
window.setInterval(() => {
|
|
if (ui.snapshot?.connected) refreshTarget();
|
|
}, 7000);
|
|
}
|
|
|
|
initialize();
|