octo-agent 0.11.2
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.
- checksums.yaml +7 -0
- data/.clacky/skills/commit/SKILL.md +423 -0
- data/.clacky/skills/gem-release/SKILL.md +199 -0
- data/.clacky/skills/gem-release/scripts/release.sh +304 -0
- data/.clacky/skills/oss-upload/SKILL.md +47 -0
- data/.octorules +106 -0
- data/.rspec +3 -0
- data/.rubocop.yml +8 -0
- data/CHANGELOG.md +76 -0
- data/CODE_OF_CONDUCT.md +132 -0
- data/CONTRIBUTING.md +92 -0
- data/Dockerfile +28 -0
- data/LICENSE.txt +22 -0
- data/POSITIONING.md +46 -0
- data/README.md +134 -0
- data/README_CN.md +134 -0
- data/Rakefile +34 -0
- data/benchmark/fixtures/sample_project/Gemfile +3 -0
- data/benchmark/fixtures/sample_project/lib/api_handler.rb +32 -0
- data/benchmark/fixtures/sample_project/lib/order_calculator.rb +23 -0
- data/benchmark/fixtures/sample_project/lib/user_renderer.rb +20 -0
- data/benchmark/fixtures/sample_project/spec/order_calculator_spec.rb +20 -0
- data/benchmark/results/EVALUATION_REPORT.md +165 -0
- data/benchmark/results/baseline_20260511_174424.json +128 -0
- data/benchmark/results/report_20260511_175256.json +271 -0
- data/benchmark/results/report_20260511_175444.json +271 -0
- data/benchmark/results/treatment_20260511_175103.json +130 -0
- data/benchmark/runner.rb +441 -0
- data/bin/octo +7 -0
- data/docs/agent-first-ui-design.md +77 -0
- data/docs/billing-system.md +318 -0
- data/docs/channel-architecture.md +235 -0
- data/docs/engineering-article.md +343 -0
- data/docs/session-skill-invocation.md +69 -0
- data/docs/time_machine_design.md +247 -0
- data/docs/ui2-architecture.md +124 -0
- data/homebrew/README.md +96 -0
- data/homebrew/openocto.rb +24 -0
- data/lib/octo/agent/hook_manager.rb +61 -0
- data/lib/octo/agent/llm_caller.rb +800 -0
- data/lib/octo/agent/memory_updater.rb +246 -0
- data/lib/octo/agent/message_compressor.rb +225 -0
- data/lib/octo/agent/message_compressor_helper.rb +869 -0
- data/lib/octo/agent/next_message_suggester.rb +215 -0
- data/lib/octo/agent/session_serializer.rb +685 -0
- data/lib/octo/agent/skill_auto_creator.rb +114 -0
- data/lib/octo/agent/skill_evolution.rb +61 -0
- data/lib/octo/agent/skill_manager.rb +466 -0
- data/lib/octo/agent/skill_reflector.rb +89 -0
- data/lib/octo/agent/system_prompt_builder.rb +101 -0
- data/lib/octo/agent/time_machine.rb +214 -0
- data/lib/octo/agent/tool_executor.rb +454 -0
- data/lib/octo/agent/tool_registry.rb +150 -0
- data/lib/octo/agent.rb +2180 -0
- data/lib/octo/agent_config.rb +989 -0
- data/lib/octo/agent_profile.rb +112 -0
- data/lib/octo/anthropic_stream_aggregator.rb +137 -0
- data/lib/octo/background_task_registry.rb +324 -0
- data/lib/octo/banner.rb +34 -0
- data/lib/octo/bedrock_stream_aggregator.rb +137 -0
- data/lib/octo/block_font.rb +331 -0
- data/lib/octo/cli.rb +968 -0
- data/lib/octo/client.rb +623 -0
- data/lib/octo/default_agents/SOUL.md +3 -0
- data/lib/octo/default_agents/USER.md +1 -0
- data/lib/octo/default_agents/base_prompt.md +66 -0
- data/lib/octo/default_agents/coding/profile.yml +2 -0
- data/lib/octo/default_agents/coding/system_prompt.md +67 -0
- data/lib/octo/default_agents/general/profile.yml +2 -0
- data/lib/octo/default_agents/general/system_prompt.md +16 -0
- data/lib/octo/default_parsers/doc_parser.rb +69 -0
- data/lib/octo/default_parsers/docx_parser.rb +188 -0
- data/lib/octo/default_parsers/pdf_parser.rb +120 -0
- data/lib/octo/default_parsers/pdf_parser_ocr.py +103 -0
- data/lib/octo/default_parsers/pdf_parser_plumber.py +62 -0
- data/lib/octo/default_parsers/pptx_parser.rb +140 -0
- data/lib/octo/default_parsers/xlsx_parser.rb +121 -0
- data/lib/octo/default_skills/browser-setup/SKILL.md +426 -0
- data/lib/octo/default_skills/channel-manager/SKILL.md +623 -0
- data/lib/octo/default_skills/channel-manager/dingtalk_setup.rb +191 -0
- data/lib/octo/default_skills/channel-manager/discord_setup.rb +199 -0
- data/lib/octo/default_skills/channel-manager/feishu_setup.rb +574 -0
- data/lib/octo/default_skills/channel-manager/import_lark_skills.rb +97 -0
- data/lib/octo/default_skills/channel-manager/install_feishu_skills.rb +105 -0
- data/lib/octo/default_skills/channel-manager/weixin_setup.rb +274 -0
- data/lib/octo/default_skills/code-explorer/SKILL.md +36 -0
- data/lib/octo/default_skills/cron-task-creator/SKILL.md +257 -0
- data/lib/octo/default_skills/cron-task-creator/evals/evals.json +38 -0
- data/lib/octo/default_skills/onboard/SKILL.md +578 -0
- data/lib/octo/default_skills/onboard/scripts/import_external_skills.rb +413 -0
- data/lib/octo/default_skills/onboard/scripts/install_builtin_skills.rb +97 -0
- data/lib/octo/default_skills/persist-memory/SKILL.md +59 -0
- data/lib/octo/default_skills/personal-website/SKILL.md +113 -0
- data/lib/octo/default_skills/personal-website/publish.rb +235 -0
- data/lib/octo/default_skills/product-help/SKILL.md +123 -0
- data/lib/octo/default_skills/product-help/docs/agent-config.md +74 -0
- data/lib/octo/default_skills/product-help/docs/best-practices.md +49 -0
- data/lib/octo/default_skills/product-help/docs/browser-tool.md +53 -0
- data/lib/octo/default_skills/product-help/docs/built-in-skills.md +43 -0
- data/lib/octo/default_skills/product-help/docs/cli-reference.md +82 -0
- data/lib/octo/default_skills/product-help/docs/create-your-first-skill.md +47 -0
- data/lib/octo/default_skills/product-help/docs/faq.md +98 -0
- data/lib/octo/default_skills/product-help/docs/how-to-use-a-skill.md +58 -0
- data/lib/octo/default_skills/product-help/docs/installation.md +59 -0
- data/lib/octo/default_skills/product-help/docs/memory-system.md +61 -0
- data/lib/octo/default_skills/product-help/docs/octorules.md +62 -0
- data/lib/octo/default_skills/product-help/docs/session-management.md +63 -0
- data/lib/octo/default_skills/product-help/docs/skill-basics.md +55 -0
- data/lib/octo/default_skills/product-help/docs/skill-frontmatter.md +61 -0
- data/lib/octo/default_skills/product-help/docs/web-server.md +49 -0
- data/lib/octo/default_skills/product-help/docs/what-is-octo.md +37 -0
- data/lib/octo/default_skills/product-help/docs/windows-installation.md +36 -0
- data/lib/octo/default_skills/product-help/docs/writing-tips.md +53 -0
- data/lib/octo/default_skills/recall-memory/SKILL.md +65 -0
- data/lib/octo/default_skills/skill-add/SKILL.md +59 -0
- data/lib/octo/default_skills/skill-add/scripts/install_from_zip.rb +295 -0
- data/lib/octo/default_skills/skill-creator/SKILL.md +602 -0
- data/lib/octo/default_skills/skill-creator/agents/analyzer.md +274 -0
- data/lib/octo/default_skills/skill-creator/agents/comparator.md +202 -0
- data/lib/octo/default_skills/skill-creator/agents/grader.md +223 -0
- data/lib/octo/default_skills/skill-creator/eval-viewer/generate_review.py +471 -0
- data/lib/octo/default_skills/skill-creator/eval-viewer/viewer.html +1325 -0
- data/lib/octo/default_skills/skill-creator/references/schemas.md +430 -0
- data/lib/octo/default_skills/skill-creator/scripts/__init__.py +0 -0
- data/lib/octo/default_skills/skill-creator/scripts/aggregate_benchmark.py +401 -0
- data/lib/octo/default_skills/skill-creator/scripts/generate_report.py +326 -0
- data/lib/octo/default_skills/skill-creator/scripts/improve_description.py +310 -0
- data/lib/octo/default_skills/skill-creator/scripts/quick_validate.py +103 -0
- data/lib/octo/default_skills/skill-creator/scripts/run_eval.py +317 -0
- data/lib/octo/default_skills/skill-creator/scripts/run_loop.py +331 -0
- data/lib/octo/default_skills/skill-creator/scripts/utils.py +47 -0
- data/lib/octo/default_skills/skill-creator/scripts/validate_skill_frontmatter.rb +143 -0
- data/lib/octo/idle_compression_timer.rb +115 -0
- data/lib/octo/json_ui_controller.rb +204 -0
- data/lib/octo/message_format/anthropic.rb +409 -0
- data/lib/octo/message_format/bedrock.rb +361 -0
- data/lib/octo/message_format/open_ai.rb +222 -0
- data/lib/octo/message_history.rb +373 -0
- data/lib/octo/openai_stream_aggregator.rb +130 -0
- data/lib/octo/plain_ui_controller.rb +166 -0
- data/lib/octo/providers.rb +534 -0
- data/lib/octo/server/browser_manager.rb +397 -0
- data/lib/octo/server/channel/adapters/base.rb +82 -0
- data/lib/octo/server/channel/adapters/dingtalk/adapter.rb +314 -0
- data/lib/octo/server/channel/adapters/dingtalk/api_client.rb +391 -0
- data/lib/octo/server/channel/adapters/dingtalk/stream_client.rb +203 -0
- data/lib/octo/server/channel/adapters/discord/adapter.rb +229 -0
- data/lib/octo/server/channel/adapters/discord/api_client.rb +107 -0
- data/lib/octo/server/channel/adapters/discord/gateway_client.rb +270 -0
- data/lib/octo/server/channel/adapters/feishu/adapter.rb +320 -0
- data/lib/octo/server/channel/adapters/feishu/bot.rb +478 -0
- data/lib/octo/server/channel/adapters/feishu/file_processor.rb +36 -0
- data/lib/octo/server/channel/adapters/feishu/message_parser.rb +129 -0
- data/lib/octo/server/channel/adapters/feishu/ws_client.rb +423 -0
- data/lib/octo/server/channel/adapters/telegram/adapter.rb +375 -0
- data/lib/octo/server/channel/adapters/telegram/api_client.rb +205 -0
- data/lib/octo/server/channel/adapters/wecom/adapter.rb +148 -0
- data/lib/octo/server/channel/adapters/wecom/media_downloader.rb +115 -0
- data/lib/octo/server/channel/adapters/wecom/ws_client.rb +395 -0
- data/lib/octo/server/channel/adapters/weixin/adapter.rb +692 -0
- data/lib/octo/server/channel/adapters/weixin/api_client.rb +402 -0
- data/lib/octo/server/channel/channel_config.rb +178 -0
- data/lib/octo/server/channel/channel_manager.rb +468 -0
- data/lib/octo/server/channel/channel_ui_controller.rb +224 -0
- data/lib/octo/server/channel.rb +33 -0
- data/lib/octo/server/discover.rb +77 -0
- data/lib/octo/server/epipe_safe_io.rb +105 -0
- data/lib/octo/server/http_server.rb +3554 -0
- data/lib/octo/server/scheduler.rb +317 -0
- data/lib/octo/server/server_master.rb +325 -0
- data/lib/octo/server/session_registry.rb +431 -0
- data/lib/octo/server/web_ui_controller.rb +487 -0
- data/lib/octo/session_manager.rb +385 -0
- data/lib/octo/skill.rb +466 -0
- data/lib/octo/skill_loader.rb +328 -0
- data/lib/octo/tools/base.rb +118 -0
- data/lib/octo/tools/browser.rb +625 -0
- data/lib/octo/tools/edit.rb +165 -0
- data/lib/octo/tools/file_reader.rb +549 -0
- data/lib/octo/tools/glob.rb +162 -0
- data/lib/octo/tools/grep.rb +356 -0
- data/lib/octo/tools/invoke_skill.rb +96 -0
- data/lib/octo/tools/list_tasks.rb +54 -0
- data/lib/octo/tools/redo_task.rb +41 -0
- data/lib/octo/tools/request_user_feedback.rb +84 -0
- data/lib/octo/tools/security.rb +333 -0
- data/lib/octo/tools/terminal/output_cleaner.rb +63 -0
- data/lib/octo/tools/terminal/persistent_session.rb +268 -0
- data/lib/octo/tools/terminal/safe_rm.sh +106 -0
- data/lib/octo/tools/terminal/session_manager.rb +213 -0
- data/lib/octo/tools/terminal.rb +1828 -0
- data/lib/octo/tools/todo_manager.rb +374 -0
- data/lib/octo/tools/trash_manager.rb +388 -0
- data/lib/octo/tools/undo_task.rb +35 -0
- data/lib/octo/tools/web_fetch.rb +242 -0
- data/lib/octo/tools/web_search.rb +260 -0
- data/lib/octo/tools/write.rb +77 -0
- data/lib/octo/ui2/block_font.rb +10 -0
- data/lib/octo/ui2/components/base_component.rb +163 -0
- data/lib/octo/ui2/components/command_suggestions.rb +290 -0
- data/lib/octo/ui2/components/common_component.rb +96 -0
- data/lib/octo/ui2/components/inline_input.rb +226 -0
- data/lib/octo/ui2/components/input_area.rb +1338 -0
- data/lib/octo/ui2/components/message_component.rb +99 -0
- data/lib/octo/ui2/components/modal_component.rb +419 -0
- data/lib/octo/ui2/components/todo_area.rb +149 -0
- data/lib/octo/ui2/components/tool_component.rb +107 -0
- data/lib/octo/ui2/components/welcome_banner.rb +139 -0
- data/lib/octo/ui2/layout_manager.rb +807 -0
- data/lib/octo/ui2/line_editor.rb +363 -0
- data/lib/octo/ui2/markdown_renderer.rb +100 -0
- data/lib/octo/ui2/output_buffer.rb +370 -0
- data/lib/octo/ui2/progress_handle.rb +362 -0
- data/lib/octo/ui2/progress_indicator.rb +55 -0
- data/lib/octo/ui2/screen_buffer.rb +273 -0
- data/lib/octo/ui2/terminal_detector.rb +119 -0
- data/lib/octo/ui2/theme_manager.rb +85 -0
- data/lib/octo/ui2/themes/base_theme.rb +105 -0
- data/lib/octo/ui2/themes/hacker_theme.rb +62 -0
- data/lib/octo/ui2/themes/minimal_theme.rb +56 -0
- data/lib/octo/ui2/thinking_verbs.rb +26 -0
- data/lib/octo/ui2/ui_controller.rb +1625 -0
- data/lib/octo/ui2/view_renderer.rb +177 -0
- data/lib/octo/ui2.rb +40 -0
- data/lib/octo/ui_interface.rb +154 -0
- data/lib/octo/utils/arguments_parser.rb +191 -0
- data/lib/octo/utils/browser_detector.rb +195 -0
- data/lib/octo/utils/encoding.rb +92 -0
- data/lib/octo/utils/environment_detector.rb +140 -0
- data/lib/octo/utils/file_ignore_helper.rb +170 -0
- data/lib/octo/utils/file_processor.rb +601 -0
- data/lib/octo/utils/gitignore_parser.rb +154 -0
- data/lib/octo/utils/limit_stack.rb +152 -0
- data/lib/octo/utils/logger.rb +124 -0
- data/lib/octo/utils/login_shell.rb +72 -0
- data/lib/octo/utils/model_pricing.rb +646 -0
- data/lib/octo/utils/parser_manager.rb +165 -0
- data/lib/octo/utils/path_helper.rb +15 -0
- data/lib/octo/utils/scripts_manager.rb +59 -0
- data/lib/octo/utils/string_matcher.rb +158 -0
- data/lib/octo/utils/trash_directory.rb +112 -0
- data/lib/octo/utils/workspace_rules.rb +46 -0
- data/lib/octo/version.rb +5 -0
- data/lib/octo/web/app.css +7141 -0
- data/lib/octo/web/app.js +543 -0
- data/lib/octo/web/apple-touch-icon.png +0 -0
- data/lib/octo/web/auth.js +150 -0
- data/lib/octo/web/channels.js +276 -0
- data/lib/octo/web/datepicker.js +205 -0
- data/lib/octo/web/favicon.png +0 -0
- data/lib/octo/web/i18n.js +1073 -0
- data/lib/octo/web/icon-512.png +0 -0
- data/lib/octo/web/icon-dark.svg +25 -0
- data/lib/octo/web/icon.svg +29 -0
- data/lib/octo/web/index.html +871 -0
- data/lib/octo/web/marked.min.js +69 -0
- data/lib/octo/web/onboard.js +491 -0
- data/lib/octo/web/profile.js +442 -0
- data/lib/octo/web/sessions.js +4421 -0
- data/lib/octo/web/settings.js +913 -0
- data/lib/octo/web/sidebar.js +32 -0
- data/lib/octo/web/skills.js +885 -0
- data/lib/octo/web/tasks.js +297 -0
- data/lib/octo/web/theme.js +105 -0
- data/lib/octo/web/trash.js +343 -0
- data/lib/octo/web/vendor/hljs/highlight.min.js +1244 -0
- data/lib/octo/web/vendor/hljs/hljs-theme.css +95 -0
- data/lib/octo/web/vendor/katex/auto-render.min.js +1 -0
- data/lib/octo/web/vendor/katex/fonts/KaTeX_AMS-Regular.woff2 +0 -0
- data/lib/octo/web/vendor/katex/fonts/KaTeX_Caligraphic-Bold.woff2 +0 -0
- data/lib/octo/web/vendor/katex/fonts/KaTeX_Caligraphic-Regular.woff2 +0 -0
- data/lib/octo/web/vendor/katex/fonts/KaTeX_Fraktur-Bold.woff2 +0 -0
- data/lib/octo/web/vendor/katex/fonts/KaTeX_Fraktur-Regular.woff2 +0 -0
- data/lib/octo/web/vendor/katex/fonts/KaTeX_Main-Bold.woff2 +0 -0
- data/lib/octo/web/vendor/katex/fonts/KaTeX_Main-BoldItalic.woff2 +0 -0
- data/lib/octo/web/vendor/katex/fonts/KaTeX_Main-Italic.woff2 +0 -0
- data/lib/octo/web/vendor/katex/fonts/KaTeX_Main-Regular.woff2 +0 -0
- data/lib/octo/web/vendor/katex/fonts/KaTeX_Math-BoldItalic.woff2 +0 -0
- data/lib/octo/web/vendor/katex/fonts/KaTeX_Math-Italic.woff2 +0 -0
- data/lib/octo/web/vendor/katex/fonts/KaTeX_SansSerif-Bold.woff2 +0 -0
- data/lib/octo/web/vendor/katex/fonts/KaTeX_SansSerif-Italic.woff2 +0 -0
- data/lib/octo/web/vendor/katex/fonts/KaTeX_SansSerif-Regular.woff2 +0 -0
- data/lib/octo/web/vendor/katex/fonts/KaTeX_Script-Regular.woff2 +0 -0
- data/lib/octo/web/vendor/katex/fonts/KaTeX_Size1-Regular.woff2 +0 -0
- data/lib/octo/web/vendor/katex/fonts/KaTeX_Size2-Regular.woff2 +0 -0
- data/lib/octo/web/vendor/katex/fonts/KaTeX_Size3-Regular.woff2 +0 -0
- data/lib/octo/web/vendor/katex/fonts/KaTeX_Size4-Regular.woff2 +0 -0
- data/lib/octo/web/vendor/katex/fonts/KaTeX_Typewriter-Regular.woff2 +0 -0
- data/lib/octo/web/vendor/katex/katex.min.css +1 -0
- data/lib/octo/web/vendor/katex/katex.min.js +1 -0
- data/lib/octo/web/version.js +449 -0
- data/lib/octo/web/weixin-qr.html +209 -0
- data/lib/octo/web/ws-dispatcher.js +357 -0
- data/lib/octo/web/ws.js +128 -0
- data/lib/octo.rb +145 -0
- data/scripts/build/build.sh +329 -0
- data/scripts/build/lib/apt.sh +56 -0
- data/scripts/build/lib/brew.sh +89 -0
- data/scripts/build/lib/colors.sh +17 -0
- data/scripts/build/lib/gem.sh +95 -0
- data/scripts/build/lib/mise.sh +125 -0
- data/scripts/build/lib/network.sh +157 -0
- data/scripts/build/lib/os.sh +57 -0
- data/scripts/build/lib/shell.sh +37 -0
- data/scripts/build/src/install.sh.cc +174 -0
- data/scripts/build/src/install_browser.sh.cc +101 -0
- data/scripts/build/src/install_full.sh.cc +290 -0
- data/scripts/build/src/install_rails_deps.sh.cc +145 -0
- data/scripts/build/src/install_system_deps.sh.cc +123 -0
- data/scripts/build/src/uninstall.sh.cc +101 -0
- data/scripts/install.ps1 +532 -0
- data/scripts/install.sh +567 -0
- data/scripts/install_browser.sh +479 -0
- data/scripts/install_full.sh +838 -0
- data/scripts/install_rails_deps.sh +746 -0
- data/scripts/install_system_deps.sh +518 -0
- data/scripts/uninstall.sh +287 -0
- data/sig/octo.rbs +4 -0
- metadata +614 -0
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "digest"
|
|
4
|
+
|
|
5
|
+
module Octo
|
|
6
|
+
module Tools
|
|
7
|
+
class Terminal < Base
|
|
8
|
+
# Holds (at most) ONE long-lived PTY shell session that is reused
|
|
9
|
+
# across multiple terminal calls. Reusing the session eliminates the
|
|
10
|
+
# ~1s cold-start cost of `zsh -l -i` / `bash -l -i` on every command.
|
|
11
|
+
#
|
|
12
|
+
# Reuse rules:
|
|
13
|
+
# - Only non-background, non-dedicated calls take from the persistent
|
|
14
|
+
# slot. background / env-overridden calls spawn a fresh session.
|
|
15
|
+
# - Before each call we diff rc-file mtime(s); if changed, we
|
|
16
|
+
# `source` them once inside the live shell so the user sees freshly
|
|
17
|
+
# installed PATH / functions / aliases on the very next command.
|
|
18
|
+
# - If a command leaves the session in a non-clean state (marker not
|
|
19
|
+
# hit — i.e. the program is still running and interactive), the
|
|
20
|
+
# session is "donated" to the caller as a dedicated session_id and
|
|
21
|
+
# the persistent slot is cleared (next call rebuilds a fresh one).
|
|
22
|
+
# - If cleanup fails or a spawn fails, we transparently fall back to
|
|
23
|
+
# the old one-shot `bash --noprofile --norc -i` spawn.
|
|
24
|
+
#
|
|
25
|
+
# Thread safety:
|
|
26
|
+
# - Each persistent session has its own mutex (Session#mutex) that
|
|
27
|
+
# serialises PTY writes (unchanged).
|
|
28
|
+
# - The PersistentSessionPool itself is guarded by a class-level
|
|
29
|
+
# mutex so concurrent terminal calls don't race on acquire/release.
|
|
30
|
+
class PersistentSessionPool
|
|
31
|
+
class << self
|
|
32
|
+
def instance
|
|
33
|
+
@instance ||= new
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def reset!
|
|
37
|
+
if @instance
|
|
38
|
+
begin
|
|
39
|
+
@instance.shutdown!
|
|
40
|
+
rescue StandardError
|
|
41
|
+
# swallow — best-effort during tests / shutdown
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
@instance = nil
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def initialize
|
|
49
|
+
@mutex = Mutex.new
|
|
50
|
+
@session = nil # currently-idle persistent session, or nil
|
|
51
|
+
@rc_fingerprint = nil # mtime snapshot used to detect rc changes
|
|
52
|
+
@last_env_keys = [] # keys we exported last time; unset them on env change
|
|
53
|
+
@disabled = false # set to true after a spawn failure to stop retrying
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Acquire a persistent session for a new command.
|
|
57
|
+
#
|
|
58
|
+
# Returns [session, reused:] where `session` is a running PTY
|
|
59
|
+
# session ready to accept a command (no concurrent command in
|
|
60
|
+
# flight). Raises SpawnFailed if we can't build one.
|
|
61
|
+
#
|
|
62
|
+
# `reused:` is true when an existing session was handed out; false
|
|
63
|
+
# when we had to spawn a fresh one.
|
|
64
|
+
#
|
|
65
|
+
# Side effects when reusing:
|
|
66
|
+
# - Sources rc files if their mtimes changed.
|
|
67
|
+
# - `cd`s to `cwd` if given.
|
|
68
|
+
# - Resets env vars that were exported last time and exports the
|
|
69
|
+
# new ones (only when `env` is non-nil).
|
|
70
|
+
def acquire(runner:, cwd: nil, env: nil)
|
|
71
|
+
@mutex.synchronize do
|
|
72
|
+
return [nil, false] if @disabled
|
|
73
|
+
|
|
74
|
+
# 1) Make sure the stored session is still healthy.
|
|
75
|
+
if @session
|
|
76
|
+
unless session_healthy?(@session)
|
|
77
|
+
drop_locked
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# 2) Spawn a fresh one if we don't have anything warm.
|
|
82
|
+
unless @session
|
|
83
|
+
begin
|
|
84
|
+
@session = runner.spawn_persistent_session
|
|
85
|
+
rescue StandardError => e
|
|
86
|
+
@disabled = true
|
|
87
|
+
raise SpawnFailed, e.message
|
|
88
|
+
end
|
|
89
|
+
@rc_fingerprint = current_rc_fingerprint
|
|
90
|
+
@last_env_keys = []
|
|
91
|
+
reused = false
|
|
92
|
+
else
|
|
93
|
+
reused = true
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# 3) If rc files changed since last use, re-source them once.
|
|
97
|
+
if reused && rc_changed?
|
|
98
|
+
runner.source_rc_in_session(@session, rc_files_for_shell(@session.shell_name))
|
|
99
|
+
@rc_fingerprint = current_rc_fingerprint
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# 4) Reset env — unset old, export new.
|
|
103
|
+
if env && !env.empty?
|
|
104
|
+
new_keys = env.keys.map(&:to_s)
|
|
105
|
+
to_unset = @last_env_keys - new_keys
|
|
106
|
+
runner.reset_env_in_session(@session, unset_keys: to_unset, set_env: env)
|
|
107
|
+
@last_env_keys = new_keys
|
|
108
|
+
elsif !@last_env_keys.empty?
|
|
109
|
+
runner.reset_env_in_session(@session, unset_keys: @last_env_keys, set_env: {})
|
|
110
|
+
@last_env_keys = []
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# 5) cd to the requested directory.
|
|
114
|
+
if cwd && Dir.exist?(cwd.to_s)
|
|
115
|
+
runner.cd_in_session(@session, cwd.to_s)
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
session = @session
|
|
119
|
+
# Remove it from the slot for the duration of the command so
|
|
120
|
+
# a concurrent caller can't grab the same shell mid-run.
|
|
121
|
+
@session = nil
|
|
122
|
+
|
|
123
|
+
[session, reused]
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# Put a session back into the persistent slot after a successful
|
|
128
|
+
# command. Returns true if stored (caller keeps the session),
|
|
129
|
+
# false if the slot was already filled or the session is unhealthy
|
|
130
|
+
# (caller MUST clean up the session — fds and process — itself).
|
|
131
|
+
def release(session)
|
|
132
|
+
@mutex.synchronize do
|
|
133
|
+
if @session.nil? && session_healthy?(session)
|
|
134
|
+
@session = session
|
|
135
|
+
true
|
|
136
|
+
else
|
|
137
|
+
false
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# The caller has decided the session is unusable (e.g. command left
|
|
143
|
+
# an interactive program running). Forget it without killing — the
|
|
144
|
+
# caller is keeping the PTY alive for their own use.
|
|
145
|
+
def discard
|
|
146
|
+
@mutex.synchronize { @session = nil }
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# Shut the persistent session down (typically at_exit).
|
|
150
|
+
def shutdown!
|
|
151
|
+
@mutex.synchronize do
|
|
152
|
+
sess = @session
|
|
153
|
+
@session = nil
|
|
154
|
+
next unless sess
|
|
155
|
+
begin
|
|
156
|
+
Process.kill("TERM", sess.pid)
|
|
157
|
+
rescue StandardError
|
|
158
|
+
# ignore
|
|
159
|
+
end
|
|
160
|
+
close_fds(sess)
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def drop_locked
|
|
165
|
+
sess = @session
|
|
166
|
+
@session = nil
|
|
167
|
+
return unless sess
|
|
168
|
+
begin
|
|
169
|
+
Process.kill("TERM", sess.pid)
|
|
170
|
+
rescue StandardError
|
|
171
|
+
# ignore
|
|
172
|
+
end
|
|
173
|
+
close_fds(sess)
|
|
174
|
+
SessionManager.forget(sess.id)
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
private :drop_locked
|
|
178
|
+
|
|
179
|
+
# Close all open file descriptors on a session struct. Safe to call
|
|
180
|
+
# multiple times (all closes are rescue-wrapped).
|
|
181
|
+
private def close_fds(session)
|
|
182
|
+
session.log_io&.close rescue nil
|
|
183
|
+
session.writer&.close rescue nil
|
|
184
|
+
session.reader&.close rescue nil
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
def session_healthy?(session)
|
|
188
|
+
return false unless session
|
|
189
|
+
return false if %w[exited killed].include?(session.status.to_s)
|
|
190
|
+
# Probe the child process to make sure it's still alive.
|
|
191
|
+
begin
|
|
192
|
+
Process.kill(0, session.pid)
|
|
193
|
+
true
|
|
194
|
+
rescue Errno::ESRCH
|
|
195
|
+
false
|
|
196
|
+
rescue StandardError
|
|
197
|
+
# EPERM etc. — assume alive
|
|
198
|
+
true
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
private :session_healthy?
|
|
203
|
+
|
|
204
|
+
# --- rc mtime tracking ---------------------------------------------------
|
|
205
|
+
|
|
206
|
+
def current_rc_fingerprint
|
|
207
|
+
files = rc_files_for_shell(nil) # superset of all known rc files
|
|
208
|
+
files.each_with_object({}) do |path, h|
|
|
209
|
+
h[path] = File.mtime(path).to_f if File.exist?(path)
|
|
210
|
+
end
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
private :current_rc_fingerprint
|
|
214
|
+
|
|
215
|
+
def rc_changed?
|
|
216
|
+
new_fp = current_rc_fingerprint
|
|
217
|
+
changed = (new_fp != @rc_fingerprint)
|
|
218
|
+
changed
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
private :rc_changed?
|
|
222
|
+
|
|
223
|
+
# Return the rc files relevant to the given shell, in the *startup*
|
|
224
|
+
# order the shell itself would read them. This order matters when
|
|
225
|
+
# we re-source after a user edit: later files may depend on vars /
|
|
226
|
+
# PATH prefixes set by earlier ones (e.g. `.zshrc` invoking
|
|
227
|
+
# `mise activate zsh` which expects `~/.local/bin` already on PATH
|
|
228
|
+
# from `.zshenv` / `.zprofile`).
|
|
229
|
+
#
|
|
230
|
+
# zsh order: .zshenv -> .zprofile (login) -> .zshrc (interactive)
|
|
231
|
+
# bash order: .profile / .bash_profile (login) -> .bashrc
|
|
232
|
+
#
|
|
233
|
+
# If shell_name is nil (used by current_rc_fingerprint when we have
|
|
234
|
+
# no session), we return a superset so we always catch changes
|
|
235
|
+
# regardless of shell.
|
|
236
|
+
def rc_files_for_shell(shell_name)
|
|
237
|
+
home = ENV["HOME"].to_s
|
|
238
|
+
case shell_name
|
|
239
|
+
when "zsh"
|
|
240
|
+
%w[.zshenv .zprofile .zshrc]
|
|
241
|
+
when "bash"
|
|
242
|
+
%w[.profile .bash_profile .bashrc]
|
|
243
|
+
else
|
|
244
|
+
%w[.zshenv .zprofile .zshrc .profile .bash_profile .bashrc]
|
|
245
|
+
end.map { |f| File.join(home, f) }.select { |f| File.exist?(f) }
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
private :rc_files_for_shell
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
# Raised by the pool when a persistent spawn can't be created; callers
|
|
252
|
+
# should fall back to a one-shot session.
|
|
253
|
+
class SpawnFailed < StandardError; end
|
|
254
|
+
end
|
|
255
|
+
end
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
# Ensure the persistent shell is cleaned up on interpreter exit. Session-
|
|
259
|
+
# level kill_all! in SessionManager handles anything that's still registered,
|
|
260
|
+
# but we also explicitly SIGTERM the pool's current slot so the child shell
|
|
261
|
+
# doesn't linger.
|
|
262
|
+
at_exit do
|
|
263
|
+
begin
|
|
264
|
+
Octo::Tools::Terminal::PersistentSessionPool.instance.shutdown!
|
|
265
|
+
rescue StandardError
|
|
266
|
+
# never raise from at_exit
|
|
267
|
+
end
|
|
268
|
+
end
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# Safe rm shell function — sourced by Octo::Tools::Terminal at the top
|
|
2
|
+
# of every interactive PTY session. See terminal.rb (SAFE_RM_PATH /
|
|
3
|
+
# install_marker) for rationale.
|
|
4
|
+
#
|
|
5
|
+
# Defines a `rm` function that moves files to $OCTO_TRASH_DIR instead
|
|
6
|
+
# of deleting them, so deletions can be recovered via `trash_manager`.
|
|
7
|
+
# The metadata sidecar schema matches
|
|
8
|
+
# Octo::Tools::Security::Replacer#create_delete_metadata so
|
|
9
|
+
# `trash_manager list/restore` keeps working unchanged.
|
|
10
|
+
#
|
|
11
|
+
# Covers: direct `rm ...` calls in the interactive shell, including
|
|
12
|
+
# multi-line commands, heredocs (heredoc bodies no longer trigger
|
|
13
|
+
# the rewriter), and shell glob expansion.
|
|
14
|
+
# Does NOT cover: `command rm`, `/bin/rm` (absolute path), `xargs rm`,
|
|
15
|
+
# `find -exec rm`, and child scripts — these bypass shell functions
|
|
16
|
+
# by design. This is the same coverage the old static Ruby rewriter
|
|
17
|
+
# had; it could not see inside those either.
|
|
18
|
+
|
|
19
|
+
rm() {
|
|
20
|
+
# Parse args: respect `--`, collect flag-like and path-like args.
|
|
21
|
+
local __dd=0
|
|
22
|
+
local -a __paths=() __flags=()
|
|
23
|
+
local __a
|
|
24
|
+
for __a in "$@"; do
|
|
25
|
+
if [ "$__dd" = "1" ]; then
|
|
26
|
+
__paths+=("$__a")
|
|
27
|
+
elif [ "$__a" = "--" ]; then
|
|
28
|
+
__dd=1
|
|
29
|
+
elif [ "${__a:0:1}" = "-" ] && [ -n "${__a:1}" ]; then
|
|
30
|
+
__flags+=("$__a")
|
|
31
|
+
else
|
|
32
|
+
__paths+=("$__a")
|
|
33
|
+
fi
|
|
34
|
+
done
|
|
35
|
+
|
|
36
|
+
# Trash dir is provisioned by the Ruby side via env.
|
|
37
|
+
local __trash="${OCTO_TRASH_DIR:-}"
|
|
38
|
+
if [ -z "$__trash" ]; then
|
|
39
|
+
echo "[octo-rm] OCTO_TRASH_DIR not set; refusing to rm" >&2
|
|
40
|
+
return 1
|
|
41
|
+
fi
|
|
42
|
+
mkdir -p "$__trash" 2>/dev/null || true
|
|
43
|
+
|
|
44
|
+
# Safety: refuse catastrophic targets (pre-expansion by the shell).
|
|
45
|
+
local __p __norm
|
|
46
|
+
for __p in ${__paths[@]+"${__paths[@]}"}; do
|
|
47
|
+
__norm="${__p%/}"
|
|
48
|
+
[ -z "$__norm" ] && __norm="/"
|
|
49
|
+
case "$__norm" in
|
|
50
|
+
/|/root|/etc|/usr|/bin|/sbin|/var)
|
|
51
|
+
echo "[octo-rm] refused dangerous target: $__p" >&2
|
|
52
|
+
return 1
|
|
53
|
+
;;
|
|
54
|
+
esac
|
|
55
|
+
if [ "$__norm" = "$HOME" ] || [ "$__p" = "~" ]; then
|
|
56
|
+
echo "[octo-rm] refused dangerous target: $__p" >&2
|
|
57
|
+
return 1
|
|
58
|
+
fi
|
|
59
|
+
done
|
|
60
|
+
|
|
61
|
+
# `-f` semantics: suppress "no such file" errors.
|
|
62
|
+
local __has_f=0 __f
|
|
63
|
+
for __f in ${__flags[@]+"${__flags[@]}"}; do
|
|
64
|
+
case "$__f" in *f*) __has_f=1 ;; esac
|
|
65
|
+
done
|
|
66
|
+
|
|
67
|
+
local __rc=0 __base __ts __dest __abs __size __mode __ext __now
|
|
68
|
+
for __p in ${__paths[@]+"${__paths[@]}"}; do
|
|
69
|
+
if [ ! -e "$__p" ] && [ ! -L "$__p" ]; then
|
|
70
|
+
if [ "$__has_f" = "0" ]; then
|
|
71
|
+
echo "rm: $__p: No such file or directory" >&2
|
|
72
|
+
__rc=1
|
|
73
|
+
fi
|
|
74
|
+
continue
|
|
75
|
+
fi
|
|
76
|
+
__base="$(basename -- "$__p")"
|
|
77
|
+
__ts="$(date +%Y%m%d_%H%M%S_%N 2>/dev/null || date +%Y%m%d_%H%M%S)"
|
|
78
|
+
__dest="$__trash/${__base}_deleted_${__ts}"
|
|
79
|
+
# Resolve absolute path for metadata BEFORE mv (path won't exist after).
|
|
80
|
+
if [ -d "$__p" ]; then
|
|
81
|
+
__abs="$(cd "$__p" 2>/dev/null && pwd)" || __abs="$__p"
|
|
82
|
+
else
|
|
83
|
+
__abs="$(cd "$(dirname -- "$__p")" 2>/dev/null && pwd)/$(basename -- "$__p")" || __abs="$__p"
|
|
84
|
+
fi
|
|
85
|
+
# Size / mode best-effort; ignore for dirs or on failure.
|
|
86
|
+
__size="$(stat -f%z "$__p" 2>/dev/null || stat -c%s "$__p" 2>/dev/null || echo 0)"
|
|
87
|
+
__mode="$(stat -f%Lp "$__p" 2>/dev/null || stat -c%a "$__p" 2>/dev/null || echo 644)"
|
|
88
|
+
case "$__base" in
|
|
89
|
+
*.*) __ext=".${__base##*.}" ;;
|
|
90
|
+
*) __ext="" ;;
|
|
91
|
+
esac
|
|
92
|
+
__now="$(date -u +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || date +%s)"
|
|
93
|
+
if command mv -- "$__p" "$__dest" 2>/dev/null; then
|
|
94
|
+
# Metadata sidecar — schema matches
|
|
95
|
+
# Octo::Tools::Security::Replacer#create_delete_metadata so
|
|
96
|
+
# `trash_manager list/restore` continue to work.
|
|
97
|
+
printf '{"original_path":"%s","trash_directory":"%s","deleted_at":"%s","deleted_by":"octo_rm_shell","file_size":%s,"file_type":"%s","file_mode":"%s"}\n' \
|
|
98
|
+
"$__abs" "$__trash" "$__now" "${__size:-0}" "$__ext" "${__mode:-644}" \
|
|
99
|
+
> "$__dest.metadata.json" 2>/dev/null || true
|
|
100
|
+
else
|
|
101
|
+
echo "rm: failed to move $__p to trash" >&2
|
|
102
|
+
__rc=1
|
|
103
|
+
fi
|
|
104
|
+
done
|
|
105
|
+
return $__rc
|
|
106
|
+
}
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
require "tmpdir"
|
|
5
|
+
require "securerandom"
|
|
6
|
+
|
|
7
|
+
module Octo
|
|
8
|
+
module Tools
|
|
9
|
+
class Terminal < Base
|
|
10
|
+
# In-process registry of interactive PTY sessions.
|
|
11
|
+
#
|
|
12
|
+
# Lifecycle: sessions die with the octo process because the child
|
|
13
|
+
# bash is a grandchild of octo (PTY.spawn forks then execs), and
|
|
14
|
+
# we also SIGKILL them on interpreter exit via an at_exit hook.
|
|
15
|
+
#
|
|
16
|
+
# Thread-safety: all mutations go through a class-level Mutex. The
|
|
17
|
+
# reader thread writes to Session#log_io concurrently with the main
|
|
18
|
+
# thread reading log_file, but File IO is append-safe on POSIX so we
|
|
19
|
+
# don't need to lock reads — we just pin them by byte offset.
|
|
20
|
+
#
|
|
21
|
+
# Status values:
|
|
22
|
+
# "starting" - PTY spawned, setup in progress
|
|
23
|
+
# "running" - ready to receive commands
|
|
24
|
+
# "exited" - child process ended
|
|
25
|
+
# "killed" - we signalled it
|
|
26
|
+
class SessionManager
|
|
27
|
+
Session = Struct.new(
|
|
28
|
+
:id, # Integer, 1-based unique id within this octo process
|
|
29
|
+
:pid, # Integer, PID of the PTY child
|
|
30
|
+
:command, # String, original command launched
|
|
31
|
+
:cwd, # String, working directory at launch
|
|
32
|
+
:started_at, # Time
|
|
33
|
+
:log_file, # String path, raw PTY output append-only
|
|
34
|
+
:log_io, # File, write handle owned by reader thread
|
|
35
|
+
:reader, # IO, PTY read end
|
|
36
|
+
:writer, # IO, PTY write end
|
|
37
|
+
:reader_thread, # Thread, reads PTY → log file
|
|
38
|
+
:status, # "starting" | "running" | "exited" | "killed"
|
|
39
|
+
:exit_code, # Integer or nil
|
|
40
|
+
:mode, # "shell" (marker-based) | "raw" (idle-based)
|
|
41
|
+
:marker_token, # String, unique per-session token for PROMPT_COMMAND
|
|
42
|
+
:marker_regex, # Regexp, compiled match for marker
|
|
43
|
+
:read_offset, # Integer, bytes already returned by previous read calls
|
|
44
|
+
:mutex, # per-session mutex for PTY writes
|
|
45
|
+
:shell_name, # "zsh" | "bash" | "sh" — informs marker syntax & rc reload
|
|
46
|
+
keyword_init: true
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
@sessions = {}
|
|
50
|
+
@next_id = 0
|
|
51
|
+
@mutex = Mutex.new
|
|
52
|
+
|
|
53
|
+
class << self
|
|
54
|
+
# Register a new session. Caller has already spawned the PTY and
|
|
55
|
+
# started the reader thread; we just record the metadata.
|
|
56
|
+
def register(pid:, command:, cwd:, log_file:, log_io:, reader:, writer:,
|
|
57
|
+
reader_thread:, mode:, marker_token: nil, shell_name: nil)
|
|
58
|
+
@mutex.synchronize do
|
|
59
|
+
@next_id += 1
|
|
60
|
+
session = Session.new(
|
|
61
|
+
id: @next_id,
|
|
62
|
+
pid: pid,
|
|
63
|
+
command: command,
|
|
64
|
+
cwd: cwd,
|
|
65
|
+
started_at: Time.now,
|
|
66
|
+
log_file: log_file,
|
|
67
|
+
log_io: log_io,
|
|
68
|
+
reader: reader,
|
|
69
|
+
writer: writer,
|
|
70
|
+
reader_thread: reader_thread,
|
|
71
|
+
status: "starting",
|
|
72
|
+
exit_code: nil,
|
|
73
|
+
mode: mode,
|
|
74
|
+
marker_token: marker_token,
|
|
75
|
+
marker_regex: marker_token ? /__OCTO_DONE_#{marker_token}_(\d+)__/ : nil,
|
|
76
|
+
read_offset: 0,
|
|
77
|
+
mutex: Mutex.new,
|
|
78
|
+
shell_name: shell_name
|
|
79
|
+
)
|
|
80
|
+
@sessions[session.id] = session
|
|
81
|
+
session
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def get(id)
|
|
86
|
+
@mutex.synchronize { @sessions[id] }
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def list
|
|
90
|
+
refresh_all
|
|
91
|
+
@mutex.synchronize { @sessions.values.sort_by(&:id) }
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# Send signal to child, mark as killed. Returns the Session, or nil
|
|
95
|
+
# if unknown.
|
|
96
|
+
def kill(id, signal: "TERM")
|
|
97
|
+
session = @mutex.synchronize { @sessions[id] }
|
|
98
|
+
return nil unless session
|
|
99
|
+
|
|
100
|
+
begin
|
|
101
|
+
Process.kill(signal, session.pid)
|
|
102
|
+
rescue Errno::ESRCH, Errno::EPERM
|
|
103
|
+
# Already dead — fall through and mark killed.
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
@mutex.synchronize do
|
|
107
|
+
if session.status == "starting" || session.status == "running"
|
|
108
|
+
session.status = "killed"
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
session
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# Forget a session (after it has been killed/exited). Does NOT kill
|
|
115
|
+
# the process — callers should kill first.
|
|
116
|
+
def forget(id)
|
|
117
|
+
@mutex.synchronize { @sessions.delete(id) }
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# Refresh status of one session in-place (mutex-held).
|
|
121
|
+
private def refresh_locked(session)
|
|
122
|
+
return unless session.status == "starting" || session.status == "running"
|
|
123
|
+
|
|
124
|
+
# Probe the child with kill(0).
|
|
125
|
+
begin
|
|
126
|
+
Process.kill(0, session.pid)
|
|
127
|
+
rescue Errno::ESRCH
|
|
128
|
+
session.status = "exited"
|
|
129
|
+
rescue Errno::EPERM
|
|
130
|
+
# Process exists but owned by someone else; keep as-is.
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def refresh_all
|
|
135
|
+
@mutex.synchronize do
|
|
136
|
+
@sessions.each_value { |s| refresh_locked(s) }
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def refresh(id)
|
|
141
|
+
@mutex.synchronize do
|
|
142
|
+
session = @sessions[id]
|
|
143
|
+
refresh_locked(session) if session
|
|
144
|
+
session
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# Mark running (called by the Terminal action after setup completes).
|
|
149
|
+
def mark_running(id)
|
|
150
|
+
@mutex.synchronize do
|
|
151
|
+
session = @sessions[id]
|
|
152
|
+
session.status = "running" if session && session.status == "starting"
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def advance_offset(id, new_offset)
|
|
157
|
+
@mutex.synchronize do
|
|
158
|
+
s = @sessions[id]
|
|
159
|
+
s.read_offset = new_offset if s
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def log_dir
|
|
164
|
+
@log_dir ||= begin
|
|
165
|
+
dir = File.join(Dir.tmpdir, "octo-terminals-#{Process.pid}")
|
|
166
|
+
FileUtils.mkdir_p(dir)
|
|
167
|
+
dir
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def allocate_log_file
|
|
172
|
+
@mutex.synchronize do
|
|
173
|
+
next_id = @next_id + 1
|
|
174
|
+
File.join(log_dir, "#{next_id}.log")
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
# Kill every live session and close any open fds. Called from at_exit.
|
|
179
|
+
def kill_all!
|
|
180
|
+
(@sessions.values rescue []).each do |s|
|
|
181
|
+
begin
|
|
182
|
+
Process.kill("KILL", s.pid) unless %w[exited killed].include?(s.status.to_s)
|
|
183
|
+
rescue StandardError
|
|
184
|
+
# ignore
|
|
185
|
+
end
|
|
186
|
+
s.log_io&.close rescue nil
|
|
187
|
+
s.writer&.close rescue nil
|
|
188
|
+
s.reader&.close rescue nil
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
# Test-only: clear state without killing processes.
|
|
193
|
+
def reset!
|
|
194
|
+
@mutex.synchronize do
|
|
195
|
+
@sessions.clear
|
|
196
|
+
@next_id = 0
|
|
197
|
+
@log_dir = nil
|
|
198
|
+
end
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
end
|
|
203
|
+
end
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
# Ensure orphaned PTY children are reaped even on unclean exit.
|
|
207
|
+
at_exit do
|
|
208
|
+
begin
|
|
209
|
+
Octo::Tools::Terminal::SessionManager.kill_all!
|
|
210
|
+
rescue StandardError
|
|
211
|
+
# never raise out of at_exit
|
|
212
|
+
end
|
|
213
|
+
end
|