Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,12 @@ test_*
!ghostscope-process/ebpf/build_sysmon_bpf.sh
# Keep installer script tracked
!scripts/install.sh
# Keep e2e wrapper script tracked
!scripts/e2e_runner/run_e2e_runner.sh
# Keep e2e runner service launcher tracked
!scripts/e2e_runner/start_e2e_runner_service.sh
# Keep skill installer tracked
!scripts/e2e_runner/install_codex_skill.sh
# Keep other C files in test fixtures but exclude random C files in root
/*.c
CLA*
Expand All @@ -61,3 +67,6 @@ gdb*
.gdb*
libtest*
nginx*

# Python cache
__pycache__/
18 changes: 18 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# GhostScope Agent Notes

## Skill Routing
- Prefer skill `ghostscope-e2e-runner` for all e2e execution requests.
- Install shared project skill with `./scripts/e2e_runner/install_codex_skill.sh` and restart Codex.

## Scope
- Keep CI workflows and developer-facing docs on normal project test commands.
- Treat `run_e2e_runner.sh` as an agent-oriented operational helper.

## Verification
- After code changes, always run formatting and lint checks before handoff.
- Use the same commands as CI in `.github/workflows/ci.yml` whenever possible.
- Local formatting: `cargo fmt --all` (single run is enough).
- CI uses `cargo fmt --all -- --check` for verification only.
- Minimum local checks (aligned with CI):
- `cargo clippy --all-targets --all-features -- -D warnings`
- If full-workspace `clippy` is too slow or blocked, run `clippy` for affected crates and clearly report scope.
23 changes: 23 additions & 0 deletions docs/development.md
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,29 @@ After rebuilding, a regular workspace build will pick up the new objects automat
sudo cargo test
```

### Agent E2E Runner (Codex)

This runner path is for running e2e from an AI agent environment, where the agent may not be able to execute `sudo cargo test` directly.

The service must be started by the developer manually with `sudo`:

```bash
cd /mnt/500g/code/ghostscope
sudo env HOST=127.0.0.1 PORT=8788 DEFAULT_SUDO=1 DEFAULT_REPO_DIR=/mnt/500g/code/ghostscope ./scripts/e2e_runner/start_e2e_runner_service.sh
```

Then run e2e through the agent wrapper:

```bash
./scripts/e2e_runner/run_e2e_runner.sh
```

Optional variables:

- `E2E_REPO_DIR=/path/to/repo`
- `E2E_TEST_CASE=<cargo_test_filter>`
- `E2E_SUDO=1|0` (default: `1`)

### Testing DWARF Parsing with dwarf-tool

GhostScope provides a standalone `dwarf-tool` for testing and debugging DWARF parsing:
Expand Down
26 changes: 26 additions & 0 deletions docs/zh/development.md
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,33 @@ docker build -t ghostscope-builder:ubuntu20.04 .
## 测试

### 集成测试和 UT

```bash
sudo cargo test
```

### Agent E2E Runner(Codex)

该流程用于在 AI agent 环境中执行 e2e,目的是规避 agent 无法直接执行 `sudo cargo test` 的限制。

`runner service` 需要开发者自行使用 `sudo` 启动:

```bash
cd /mnt/500g/code/ghostscope
sudo env HOST=127.0.0.1 PORT=8788 DEFAULT_SUDO=1 DEFAULT_REPO_DIR=/mnt/500g/code/ghostscope ./scripts/e2e_runner/start_e2e_runner_service.sh
```

启动后,通过 agent 包装脚本触发 e2e:

```bash
./scripts/e2e_runner/run_e2e_runner.sh
```

可选变量:

- `E2E_REPO_DIR=/path/to/repo`
- `E2E_TEST_CASE=<cargo_test_filter>`
- `E2E_SUDO=1|0`(默认:`1`)

### 使用 dwarf-tool 测试 DWARF 解析

Expand Down
42 changes: 40 additions & 2 deletions ghostscope-ui/src/components/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,11 @@ impl App {
let mut loading_ui_ticker = tokio::time::interval(tokio::time::Duration::from_secs(1));
loading_ui_ticker.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);

// Periodic housekeeping ticker for lightweight timeout/cleanup checks.
// Use an interval instead of recreating sleep futures in each select iteration.
let mut housekeeping_ticker = tokio::time::interval(tokio::time::Duration::from_millis(50));
housekeeping_ticker.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);

// Initial render
self.terminal.draw(|f| Self::draw_ui(f, &mut self.state))?;

Expand Down Expand Up @@ -175,8 +180,8 @@ impl App {
needs_render = true;
}

// Check for jk escape sequence timeout periodically
_ = tokio::time::sleep(std::time::Duration::from_millis(50)) => {
// Check for jk escape sequence timeout and periodic cleanup
_ = housekeeping_ticker.tick() => {
// Check jk timeout
if crate::components::command_panel::input_handler::InputHandler::check_jk_timeout(&mut self.state.command_panel) {
needs_render = true;
Expand Down Expand Up @@ -3258,6 +3263,17 @@ impl App {
};
let _ = self.handle_action(action);
}
RuntimeStatus::TraceBackpressure {
dropped_since_last,
dropped_total,
queue_capacity,
} => {
self.show_trace_backpressure_alert(
dropped_since_last,
dropped_total,
queue_capacity,
);
}
_ => {
// Handle other runtime status messages (delegate to command panel or other components)
// For now, pass them to command panel for display
Expand Down Expand Up @@ -3313,6 +3329,28 @@ impl App {
self.state.ebpf_panel.add_trace_event(trace_event);
}

fn show_trace_backpressure_alert(
&mut self,
dropped_since_last: u64,
dropped_total: u64,
queue_capacity: usize,
) {
let content = format!(
"⚠ Trace queue saturated: dropped {dropped_since_last} events in last 1s (total {dropped_total}, capacity {queue_capacity})"
);
let styled_lines =
crate::components::command_panel::ResponseFormatter::style_generic_message_lines(
&content,
);
crate::components::command_panel::ResponseFormatter::upsert_runtime_alert_with_style(
&mut self.state.command_panel,
content,
Some(styled_lines),
crate::action::ResponseType::Warning,
);
self.state.command_renderer.mark_pending_updates();
}

/// Format runtime status for display in command panel
fn format_runtime_status_for_display(
&mut self,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ impl OptimizedRenderer {
}
}
}
LineType::Response => {
LineType::Response | LineType::RuntimeAlert => {
// Check if we have pre-styled content
if let Some(ref styled_content) = static_line.styled_content {
let wrapped_lines = self.wrap_styled_line(styled_content, width as usize);
Expand Down
105 changes: 102 additions & 3 deletions ghostscope-ui/src/components/command_panel/response_formatter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,50 @@ impl ResponseFormatter {
Self::update_static_lines(state);
}

/// Upsert a runtime alert line that is independent from command history.
/// This is used for periodic/system warnings (e.g., backpressure) and must
/// remain visible even when no command has been entered yet.
pub fn upsert_runtime_alert_with_style(
state: &mut CommandPanelState,
content: String,
styled_lines: Option<Vec<Line<'static>>>,
response_type: ResponseType,
) {
state
.static_lines
.retain(|line| line.line_type != LineType::RuntimeAlert);

if let Some(styled) = styled_lines {
for styled_line in styled {
let plain: String = styled_line
.spans
.iter()
.map(|span| span.content.as_ref())
.collect();
state.static_lines.push(StaticTextLine {
content: plain,
line_type: LineType::RuntimeAlert,
history_index: None,
response_type: Some(response_type),
styled_content: Some(styled_line),
});
}
} else {
for line in Self::split_response_lines(&content) {
state.static_lines.push(StaticTextLine {
content: line,
line_type: LineType::RuntimeAlert,
history_index: None,
response_type: Some(response_type),
styled_content: None,
});
}
}

state.styled_buffer = None;
state.styled_at_history_index = None;
}

/// Helper method to create a simple single-line styled response
/// This reduces code duplication for common response patterns
pub fn add_simple_styled_response(
Expand Down Expand Up @@ -306,10 +350,10 @@ impl ResponseFormatter {

/// Update the static lines display from command history
pub fn update_static_lines(state: &mut CommandPanelState) {
// Keep welcome messages but remove command/response lines
// Keep welcome/runtime alert messages but remove command/response lines
state
.static_lines
.retain(|line| line.line_type == LineType::Welcome);
.retain(|line| matches!(line.line_type, LineType::Welcome | LineType::RuntimeAlert));
state.styled_buffer = None;
state.styled_at_history_index = None;

Expand Down Expand Up @@ -376,7 +420,7 @@ impl ResponseFormatter {
) -> Vec<Line<'static>> {
match line.line_type {
LineType::Command => Self::format_command_line(&line.content, width),
LineType::Response => Self::format_response_line(line, width),
LineType::Response | LineType::RuntimeAlert => Self::format_response_line(line, width),
LineType::Welcome => Self::format_response_line(line, width), // Format welcome messages like responses
LineType::CurrentInput => {
if is_current_input {
Expand Down Expand Up @@ -1345,4 +1389,59 @@ impl ResponseFormatter {
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn runtime_alert_visible_without_command_history() {
let mut state = CommandPanelState::new();
let content = "⚠ Trace queue saturated: dropped 10 events in last 1s".to_string();
let styled = ResponseFormatter::style_generic_message_lines(&content);

ResponseFormatter::upsert_runtime_alert_with_style(
&mut state,
content.clone(),
Some(styled),
ResponseType::Warning,
);

assert!(state.command_history.is_empty());
assert!(state.static_lines.iter().any(|line| {
line.line_type == LineType::RuntimeAlert
&& line.content.contains("Trace queue saturated")
}));
}

#[test]
fn runtime_alert_is_upserted_and_survives_history_refresh() {
let mut state = CommandPanelState::new();

ResponseFormatter::upsert_runtime_alert_with_style(
&mut state,
"⚠ old alert".to_string(),
None,
ResponseType::Warning,
);
ResponseFormatter::upsert_runtime_alert_with_style(
&mut state,
"⚠ new alert".to_string(),
None,
ResponseType::Warning,
);

// Add one command and refresh static lines to simulate normal command flow.
state.add_command_entry("info trace");
ResponseFormatter::update_static_lines(&mut state);

let alert_lines: Vec<_> = state
.static_lines
.iter()
.filter(|line| line.line_type == LineType::RuntimeAlert)
.collect();
assert_eq!(alert_lines.len(), 1);
assert!(alert_lines[0].content.contains("new alert"));
}
}

// Removed tests for dynamic help styling (now pre-styled upstream)
Loading