Change output handling, fix step debugging issues

This commit is contained in:
Badvision
2025-03-17 17:39:03 -05:00
parent e6db52f896
commit 53057f46e4
2 changed files with 382 additions and 194 deletions
+104 -22
View File
@@ -648,41 +648,123 @@ public class MonitorMode implements TerminalMode {
isStepping.set(true);
try {
// Make sure the emulator is paused while we step
Emulator.withComputer(computer -> {
// Ensure motherboard is suspended while we perform manual stepping
computer.getMotherboard().suspend();
MOS65C02 cpu = getCpu();
if (cpu == null) {
output.println("Error: Could not access CPU");
return;
}
// Keep track of the instructions stepped for output
for (int i = 0; i < count; i++) {
// Execute a single instruction
debugger.step = true;
// Get the current state before stepping
int currentPC = cpu.getProgramCounter();
String currentDisasm = cpu.disassemble(currentPC);
try {
// Resume the motherboard to execute the step
computer.getMotherboard().resume();
// Give the CPU a chance to execute the step
Thread.sleep(10);
// Then pause again
computer.getMotherboard().suspend();
// Show current state with step count info
displayCurrentInstruction(i + 1, count);
output.flush(); // Ensure output is displayed immediately
// If this is not the last step, add a small delay for readability
if (i < count - 1) {
// Calculate padding (min 2 spaces, but with less total width)
int padding = Math.max(2, 20 - currentDisasm.length());
StringBuilder paddingStr = new StringBuilder();
for (int j = 0; j < padding; j++) {
paddingStr.append(" ");
}
// Display current instruction
output.printf("%04X: %s", currentPC, currentDisasm);
// Use a special method to force a single CPU instruction
executeOneSingleInstruction(computer, cpu);
// Now display the CPU state after execution
output.printf("%sA:%02X X:%02X Y:%02X S:%02X [%s] (%d/%d)%n",
paddingStr,
cpu.A & 0xFF, cpu.X & 0xFF, cpu.Y & 0xFF, cpu.STACK & 0xFF,
cpu.getFlags(), i + 1, count);
output.flush(); // Ensure output is displayed immediately
// Add a small delay between steps for readability
if (i < count - 1) {
try {
Thread.sleep(5);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
break;
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
break;
}
}
// Ensure motherboard remains suspended after stepping
computer.getMotherboard().suspend();
});
} finally {
// Always make sure we reset the stepping flag
isStepping.set(false);
}
}
/**
* Executes exactly one CPU instruction using CPU's debugging mechanisms
*/
private void executeOneSingleInstruction(jace.apple2e.Apple2e computer, MOS65C02 cpu) {
// Since we can't access executeOpcode() directly, we need a more controlled approach
// Store the current program counter before stepping
final int originalPC = cpu.getProgramCounter();
// We'll detect completion by watching for the PC to change
final AtomicBoolean instructionComplete = new AtomicBoolean(false);
// Create a special one-time execution listener
RAMListener execListener = computer.getMemory().observe(
"SingleStepExec",
RAMEvent.TYPE.EXECUTE,
originalPC,
false, // main memory
event -> {
// After the instruction executes, prevent any further execution
if (!instructionComplete.get()) {
instructionComplete.set(true);
computer.getMotherboard().suspend();
}
}
);
try {
// Set the debugger to step mode
debugger.step = true;
// Resume the motherboard to start execution
computer.getMotherboard().resume();
// Wait for the instruction to complete (with timeout)
final long startTime = System.currentTimeMillis();
final long timeout = 50; // ms
while (!instructionComplete.get() &&
(System.currentTimeMillis() - startTime < timeout)) {
// Use onSpinWait instead of sleep for more efficient spinning
Thread.onSpinWait();
}
// Force suspension regardless of completion state
computer.getMotherboard().suspend();
// If we timed out, log it
if (!instructionComplete.get()) {
System.out.println("Warning: CPU single-step timed out");
}
} finally {
// Clean up our execution listener
computer.getMemory().removeListener(execListener);
// Reset the debugger step flag
debugger.step = false;
}
}
private void handleWatch(String[] args) {
if (args.length == 0) {
output.println("Usage: watch <address> [name] or watch -<address|name> or watch clear");
@@ -20,11 +20,16 @@ import java.io.BufferedReader;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.PipedInputStream;
import java.io.PipedOutputStream;
import java.io.PrintStream;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Consumer;
import java.util.function.Function;
import jace.Emulator;
import jace.apple2e.Apple2e;
import javafx.application.Platform;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
@@ -46,105 +51,283 @@ import javafx.stage.StageStyle;
*/
public class TerminalUIController {
// Track current Terminal mode for UI display - initialize with placeholder
private static TerminalMode currentMode;
// Initialize the mode using a static block to avoid issues with null terminals
static {
try {
// Create a minimal terminal implementation just for static initialization
JaceTerminal dummyTerminal = new JaceTerminal(
new BufferedReader(new InputStreamReader(System.in)),
new PrintStream(new ByteArrayOutputStream())
) {
@Override
public boolean setMode(String mode) {
// Dummy implementation - no-op
return true; // Always succeed
}
@Override
public EmulatorInterface getEmulator() {
// Return a simple mock implementation
return new EmulatorInterface() {
@Override
public void withComputer(java.util.function.Consumer<jace.apple2e.Apple2e> action) {
// No-op implementation
}
@Override
public <T> T withComputer(java.util.function.Function<jace.apple2e.Apple2e, T> function, T defaultValue) {
return defaultValue;
}
@Override
public void whileSuspended(java.util.function.Consumer<jace.apple2e.Apple2e> action) {
// No-op implementation
}
};
}
@Override
public void stop() {
// Dummy implementation - no-op
}
};
/**
* Custom PrintStream that directly updates the UI TextArea
* This eliminates the need for pipes which can be closed unexpectedly during debugging
*/
private static class TextAreaPrintStream extends PrintStream {
private final TextArea textArea;
private final StringBuilder lineBuffer = new StringBuilder();
private final AtomicBoolean updateScheduled = new AtomicBoolean(false);
private final ConcurrentLinkedQueue<String> outputQueue = new ConcurrentLinkedQueue<>();
public TextAreaPrintStream(TextArea textArea) {
super(new ByteArrayOutputStream(), true); // Autoflush
this.textArea = textArea;
}
@Override
public void write(int b) {
// Convert to char
char c = (char) b;
// Create MainMode with our dummy terminal
currentMode = new MainMode(dummyTerminal);
} catch (Exception e) {
// Fallback to safer initialization if needed
System.err.println("Warning: Failed to initialize default terminal mode: " + e.getMessage());
currentMode = new TerminalMode() {
@Override
public String getName() { return "Main"; }
// If newline, process the buffered line
if (c == '\n') {
commitLine();
} else {
// Add to buffer
lineBuffer.append(c);
}
}
@Override
public void write(byte[] buf, int off, int len) {
// For performance, handle byte arrays directly
String str = new String(buf, off, len);
// Check for newlines
int lastNewline = str.lastIndexOf('\n');
if (lastNewline >= 0) {
// Split by last newline
String beforeNewline = str.substring(0, lastNewline);
String afterNewline = str.substring(lastNewline + 1);
@Override
public String getPrompt() { return "JACE> "; }
// Process everything before the last newline
lineBuffer.append(beforeNewline);
commitLine();
@Override
public boolean processCommand(String command) { return false; }
// Start a new buffer with anything after the last newline
lineBuffer.append(afterNewline);
} else {
// No newlines, just append to buffer
lineBuffer.append(str);
}
}
private void commitLine() {
if (lineBuffer.length() > 0) {
// Get the processed line
String processedLine = processLine(lineBuffer.toString());
@Override
public void printHelp() {}
// Add to queue if not empty
if (!processedLine.isEmpty()) {
outputQueue.add(processedLine);
scheduleUpdate();
}
@Override
public boolean printCommandHelp(String command) { return false; }
};
// Clear the buffer
lineBuffer.setLength(0);
}
}
private String processLine(String line) {
// Skip processing if the line is empty
if (line.trim().isEmpty()) {
return "";
}
// Get the current mode
TerminalMode mode = getCurrentMode();
// Check for pure prompt lines (only the prompt with optional whitespace)
if (mode != null && line.trim().equals(mode.getPrompt())) {
// Don't display pure prompt lines (they're shown in the input area)
return "";
}
// Check if line starts with the current prompt
if (mode != null && line.startsWith(mode.getPrompt())) {
// Extract everything after the prompt
return line.substring(mode.getPrompt().length()).trim();
}
// Otherwise, return the line as-is
return line;
}
private void scheduleUpdate() {
// Only schedule if not already scheduled
if (updateScheduled.compareAndSet(false, true)) {
Platform.runLater(this::updateTextArea);
}
}
private void updateTextArea() {
try {
// Process all queued output
String line;
while ((line = outputQueue.poll()) != null) {
textArea.appendText(line + "\n");
}
// Force scroll to bottom
textArea.setScrollTop(Double.MAX_VALUE);
textArea.positionCaret(textArea.getText().length());
} catch (Exception e) {
System.err.println("Error updating console: " + e);
} finally {
// Reset the scheduled flag
updateScheduled.set(false);
// If more items were added during processing, schedule another update
if (!outputQueue.isEmpty()) {
scheduleUpdate();
}
}
}
@Override
public void flush() {
// Flush any buffered content
commitLine();
}
@Override
public void close() {
// Nothing special needed for close
super.close();
}
}
private static javafx.scene.control.Label modeLabel;
/**
* Set the current Terminal mode
* Called by UITerminal when the mode changes
*
* @param mode New Terminal mode
* Custom EmulatorAdapter implementation that works with our UI Terminal
*/
public static void setCurrentMode(TerminalMode mode) {
currentMode = mode;
private static class UIEmulatorAdapter implements EmulatorInterface {
@Override
public void withComputer(Consumer<Apple2e> action) {
Emulator.withComputer(action);
}
// Update the mode label if available
if (modeLabel != null && mode != null) {
Platform.runLater(() -> {
modeLabel.setText(mode.getPrompt());
@Override
public <T> T withComputer(Function<Apple2e, T> function, T defaultValue) {
return Emulator.withComputer(function, defaultValue);
}
@Override
public void whileSuspended(Consumer<Apple2e> action) {
Emulator.withComputer(c -> {
c.getMotherboard().whileSuspended(() -> action.accept(c));
});
}
}
/**
* Get the current Terminal mode
*
* @return Current Terminal mode or null if not set
* Current mode for UI tracking
*/
private static TerminalMode currentMode;
/**
* Terminal UI-specific implementation that maintains connection with UI
*/
private static class UITerminal extends jace.terminal.UITerminal {
public UITerminal(BufferedReader reader, PrintStream output) {
super(reader, output);
}
// Override setMode to update UI with mode changes
@Override
public boolean setMode(String mode) {
boolean result = super.setMode(mode);
if (result) {
// Use static method to update UI
TerminalUIController.setCurrentMode(getCurrentMode());
}
return result;
}
@Override
public EmulatorInterface getEmulator() {
// Force connection to the existing emulator
if (super.getEmulator() == null) {
setEmulator(new UIEmulatorAdapter());
}
return super.getEmulator();
}
@Override
public void stop() {
super.stop();
// Additional UI-specific cleanup could go here
}
}
/**
* An abstract class implementing TerminalMode to simplify custom mode creation
*/
private abstract static class AbstractTerminalMode implements TerminalMode {
private final JaceTerminal terminal;
public AbstractTerminalMode(JaceTerminal terminal) {
this.terminal = terminal;
}
protected JaceTerminal getTerminal() {
return terminal;
}
}
/**
* UI Terminal Mode for linking with the UI
*/
private static class UITerminalMode extends AbstractTerminalMode {
public UITerminalMode() {
super(null);
}
@Override
public String getName() { return "Main"; }
@Override
public String getPrompt() { return "JACE> "; }
@Override
public boolean processCommand(String command) { return false; }
@Override
public void printHelp() {}
@Override
public boolean printCommandHelp(String command) { return false; }
}
/**
* Label for displaying current mode
*/
private static javafx.scene.control.Label modeLabel;
/**
* Sets the current mode and updates the UI
* @param mode The new terminal mode
*/
public static void setCurrentMode(TerminalMode mode) {
currentMode = mode;
// Update UI on JavaFX thread
if (modeLabel != null) {
Platform.runLater(() -> {
try {
if (mode != null) {
modeLabel.setText(mode.getPrompt());
} else {
modeLabel.setText("MAIN>");
}
} catch (Exception e) {
System.err.println("Error updating mode label: " + e);
}
});
}
}
/**
* Gets the current terminal mode
* @return The current terminal mode
*/
public static TerminalMode getCurrentMode() {
return currentMode;
}
/**
* Open a new Terminal window
* This handles all UI setup and connects to a Terminal instance
* Opens a new Terminal window
*/
public static void openTerminalWindow() {
// Make sure we run UI creation on the JavaFX thread
@@ -184,46 +367,16 @@ public class TerminalUIController {
Scene scene = new Scene(layout, 650, 400);
terminalStage.setScene(scene);
// Set up piped I/O - this allows communication between the UI and Terminal
try {
// Create the pipes for input/output
// Create a pipe for input only - this is much simpler than before
final PipedOutputStream uiToTerminal = new PipedOutputStream();
final PipedInputStream terminalInput = new PipedInputStream(uiToTerminal);
final PipedInputStream terminalInput = new PipedInputStream(uiToTerminal, 8192);
final PipedOutputStream terminalOutput = new PipedOutputStream();
final PipedInputStream terminalToUi = new PipedInputStream(terminalOutput);
// Create readers/writers for the Terminal
// Create readers for the Terminal
BufferedReader reader = new BufferedReader(new InputStreamReader(terminalInput));
// Create a PrintStream with a custom OutputStream that formats output
PrintStream printStream = new PrintStream(new OutputStream() {
private final ByteArrayOutputStream buffer = new ByteArrayOutputStream();
@Override
public void write(int b) throws IOException {
// Pass through to the terminal output without any special handling
terminalOutput.write(b);
// If newline, flush our buffer
if (b == '\n') {
buffer.reset();
} else {
// Add to our buffer
buffer.write(b);
}
}
@Override
public void flush() throws IOException {
terminalOutput.flush();
}
@Override
public void close() throws IOException {
terminalOutput.close();
}
}, true);
// Create a custom PrintStream that updates the UI directly
PrintStream printStream = new TextAreaPrintStream(consoleOutput);
// Initialize the Terminal in a background thread - not on the JavaFX thread!
Thread terminalThread = new Thread(() -> {
@@ -234,15 +387,19 @@ public class TerminalUIController {
// Make sure it knows the emulator is already running
terminal.setEmulatorAlreadyRunning();
// Notify the user - safely update text on UI thread
// Clear the console and show initializing message
Platform.runLater(() -> {
try {
consoleOutput.setText("Initializing Terminal...\n");
} catch (Exception e) {
System.err.println("Error updating console: " + e);
}
consoleOutput.clear();
consoleOutput.appendText("Initializing Terminal...\n");
});
// Force a small delay to ensure UI is updated before terminal starts
try {
Thread.sleep(100);
} catch (InterruptedException e) {
// Ignore
}
// Run the Terminal - this will block until exit
terminal.run();
@@ -265,52 +422,6 @@ public class TerminalUIController {
terminalThread.setDaemon(true);
terminalThread.start();
// Create a final reference for tracking commands
final StringBuilder commandPrefix = new StringBuilder();
// Set up a reader thread to get output from the Terminal to the UI
Thread readerThread = new Thread(() -> {
try (BufferedReader uiReader = new BufferedReader(new InputStreamReader(terminalToUi))) {
String line;
while ((line = uiReader.readLine()) != null) {
final String finalLine = line;
// Update UI - must be on JavaFX thread
Platform.runLater(() -> {
try {
// Check for pure prompt lines first (only the prompt with optional whitespace)
if (currentMode != null && finalLine.trim().equals(currentMode.getPrompt())) {
// Don't display pure prompt lines (they're shown in the input area)
return;
}
// Process the line for display
String processedLine = finalLine;
// Check if line starts with the current prompt
if (currentMode != null && processedLine.startsWith(currentMode.getPrompt())) {
// Extract everything after the prompt
processedLine = processedLine.substring(currentMode.getPrompt().length()).trim();
}
// Display the processed line if not empty
if (!processedLine.isEmpty()) {
consoleOutput.appendText(processedLine + "\n");
// Force scroll to bottom with both methods to ensure it works
consoleOutput.setScrollTop(Double.MAX_VALUE);
consoleOutput.positionCaret(consoleOutput.getText().length());
}
} catch (Exception e) {
System.err.println("Error updating console: " + e);
}
});
}
} catch (IOException e) {
// This is expected when terminal closes
System.out.println("Terminal output pipe closed");
}
});
// Set up send button and enter key actions
EventHandler<ActionEvent> sendAction = event -> {
String command = inputField.getText().trim();
@@ -345,17 +456,12 @@ public class TerminalUIController {
sendButton.setOnAction(sendAction);
inputField.setOnAction(sendAction);
// Set this daemon as well
readerThread.setDaemon(true);
readerThread.start();
// Set up window close handling - close the pipes to end threads
// Set up window close handling - close input pipe to end thread
terminalStage.setOnCloseRequest(event -> {
try {
uiToTerminal.close();
terminalOutput.close();
} catch (IOException e) {
System.err.println("Error closing terminal pipes: " + e);
System.err.println("Error closing terminal pipe: " + e);
}
});