chocomint 1.0.0
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/LICENSE +21 -0
- data/README.md +224 -0
- data/THIRD_PARTY_LICENSES.md +45 -0
- data/bin/chocomint +37 -0
- data/bin/chocomint-console-worker +35 -0
- data/bin/chocomint-server +43 -0
- data/config/config.yml +37 -0
- data/lib/chocomint/config.rb +74 -0
- data/lib/chocomint/console/conpty.rb +296 -0
- data/lib/chocomint/console/server.rb +164 -0
- data/lib/chocomint/console/worker.rb +88 -0
- data/lib/chocomint/console/worker_process.rb +254 -0
- data/lib/chocomint/edit_git_handlers.rb +397 -0
- data/lib/chocomint/edit_handlers.rb +696 -0
- data/lib/chocomint/edit_html.rb +9 -0
- data/lib/chocomint/errors.rb +18 -0
- data/lib/chocomint/factory.rb +145 -0
- data/lib/chocomint/llm/base_client.rb +113 -0
- data/lib/chocomint/llm/chat_client.rb +206 -0
- data/lib/chocomint/llm/primary_client.rb +320 -0
- data/lib/chocomint/llm/verifier_client.rb +127 -0
- data/lib/chocomint/logger/sqlite_logger.rb +245 -0
- data/lib/chocomint/orchestrator.rb +175 -0
- data/lib/chocomint/planner.rb +241 -0
- data/lib/chocomint/security/path_guard.rb +84 -0
- data/lib/chocomint/server.rb +863 -0
- data/lib/chocomint/tools/append_file.rb +50 -0
- data/lib/chocomint/tools/base.rb +88 -0
- data/lib/chocomint/tools/delete_file.rb +42 -0
- data/lib/chocomint/tools/edit.rb +65 -0
- data/lib/chocomint/tools/fetch_url.rb +156 -0
- data/lib/chocomint/tools/file_info.rb +54 -0
- data/lib/chocomint/tools/glob.rb +58 -0
- data/lib/chocomint/tools/grep.rb +78 -0
- data/lib/chocomint/tools/list_dir.rb +41 -0
- data/lib/chocomint/tools/ls.rb +51 -0
- data/lib/chocomint/tools/make_dir.rb +44 -0
- data/lib/chocomint/tools/read_file.rb +45 -0
- data/lib/chocomint/tools/registry.rb +29 -0
- data/lib/chocomint/tools/run_command.rb +96 -0
- data/lib/chocomint/tools/shell.rb +169 -0
- data/lib/chocomint/tools/write_file.rb +54 -0
- data/lib/chocomint/validator/machine_validator.rb +65 -0
- data/lib/chocomint/validator/semantic_validator.rb +52 -0
- data/lib/chocomint.rb +51 -0
- data/public/edit/edit.css +414 -0
- data/public/edit/edit.js +2137 -0
- data/public/edit/index.html +251 -0
- data/public/vendor/monaco/LICENSE.txt +23 -0
- data/public/vendor/monaco/vs/base/browser/ui/codicons/codicon/codicon.ttf +0 -0
- data/public/vendor/monaco/vs/base/worker/workerMain.js +31 -0
- data/public/vendor/monaco/vs/basic-languages/abap/abap.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/apex/apex.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/azcli/azcli.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/bat/bat.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/bicep/bicep.js +11 -0
- data/public/vendor/monaco/vs/basic-languages/cameligo/cameligo.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/clojure/clojure.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/coffee/coffee.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/cpp/cpp.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/csharp/csharp.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/csp/csp.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/css/css.js +12 -0
- data/public/vendor/monaco/vs/basic-languages/cypher/cypher.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/dart/dart.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/dockerfile/dockerfile.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/ecl/ecl.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/elixir/elixir.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/flow9/flow9.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/freemarker2/freemarker2.js +12 -0
- data/public/vendor/monaco/vs/basic-languages/fsharp/fsharp.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/go/go.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/graphql/graphql.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/handlebars/handlebars.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/hcl/hcl.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/html/html.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/ini/ini.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/java/java.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/javascript/javascript.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/julia/julia.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/kotlin/kotlin.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/less/less.js +11 -0
- data/public/vendor/monaco/vs/basic-languages/lexon/lexon.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/liquid/liquid.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/lua/lua.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/m3/m3.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/markdown/markdown.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/mdx/mdx.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/mips/mips.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/msdax/msdax.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/mysql/mysql.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/objective-c/objective-c.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/pascal/pascal.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/pascaligo/pascaligo.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/perl/perl.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/pgsql/pgsql.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/php/php.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/pla/pla.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/postiats/postiats.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/powerquery/powerquery.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/powershell/powershell.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/protobuf/protobuf.js +11 -0
- data/public/vendor/monaco/vs/basic-languages/pug/pug.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/python/python.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/qsharp/qsharp.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/r/r.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/razor/razor.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/redis/redis.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/redshift/redshift.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/restructuredtext/restructuredtext.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/ruby/ruby.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/rust/rust.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/sb/sb.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/scala/scala.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/scheme/scheme.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/scss/scss.js +12 -0
- data/public/vendor/monaco/vs/basic-languages/shell/shell.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/solidity/solidity.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/sophia/sophia.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/sparql/sparql.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/sql/sql.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/st/st.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/swift/swift.js +13 -0
- data/public/vendor/monaco/vs/basic-languages/systemverilog/systemverilog.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/tcl/tcl.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/twig/twig.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/typescript/typescript.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/typespec/typespec.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/vb/vb.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/wgsl/wgsl.js +307 -0
- data/public/vendor/monaco/vs/basic-languages/xml/xml.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/yaml/yaml.js +10 -0
- data/public/vendor/monaco/vs/editor/editor.main.css +8 -0
- data/public/vendor/monaco/vs/editor/editor.main.js +798 -0
- data/public/vendor/monaco/vs/language/css/cssMode.js +13 -0
- data/public/vendor/monaco/vs/language/css/cssWorker.js +77 -0
- data/public/vendor/monaco/vs/language/html/htmlMode.js +13 -0
- data/public/vendor/monaco/vs/language/html/htmlWorker.js +454 -0
- data/public/vendor/monaco/vs/language/json/jsonMode.js +19 -0
- data/public/vendor/monaco/vs/language/json/jsonWorker.js +42 -0
- data/public/vendor/monaco/vs/language/typescript/tsMode.js +20 -0
- data/public/vendor/monaco/vs/language/typescript/tsWorker.js +51328 -0
- data/public/vendor/monaco/vs/loader.js +11 -0
- data/public/vendor/monaco/vs/nls.messages.de.js +21 -0
- data/public/vendor/monaco/vs/nls.messages.es.js +21 -0
- data/public/vendor/monaco/vs/nls.messages.fr.js +19 -0
- data/public/vendor/monaco/vs/nls.messages.it.js +19 -0
- data/public/vendor/monaco/vs/nls.messages.ja.js +21 -0
- data/public/vendor/monaco/vs/nls.messages.ko.js +19 -0
- data/public/vendor/monaco/vs/nls.messages.ru.js +21 -0
- data/public/vendor/monaco/vs/nls.messages.zh-cn.js +21 -0
- data/public/vendor/monaco/vs/nls.messages.zh-tw.js +19 -0
- data/public/vendor/xterm/LICENSE +25 -0
- data/public/vendor/xterm/addon-fit.js +2 -0
- data/public/vendor/xterm/xterm.css +218 -0
- data/public/vendor/xterm/xterm.js +2 -0
- metadata +318 -0
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "ffi"
|
|
4
|
+
require_relative "../errors"
|
|
5
|
+
|
|
6
|
+
module Chocomint
|
|
7
|
+
module Console
|
|
8
|
+
# Windows の擬似コンソール (ConPTY) を FFI で直接叩き、対話シェルを
|
|
9
|
+
# 本物の tty として起動するラッパー。xterm.js のバックエンドに使う。
|
|
10
|
+
#
|
|
11
|
+
# 重要な前提 (スパイク検証で判明): CreatePseudoConsole を呼ぶプロセスは
|
|
12
|
+
# 自身がコンソールにアタッチされている必要がある。コンソールを持たない
|
|
13
|
+
# プロセス (CREATE_NO_WINDOW で起動された等) から生成すると、初期 VT
|
|
14
|
+
# シーケンスは出るが子プロセスの出力が out パイプに流れてこない。
|
|
15
|
+
# そのため本クラスを使うワーカーは必ずコンソール付きで起動すること
|
|
16
|
+
# (例: 非表示ウィンドウの新規コンソール)。詳細は console_worker を参照。
|
|
17
|
+
#
|
|
18
|
+
# セキュリティ注意: 生の対話 tty は PathGuard / allowed_commands の監督外で
|
|
19
|
+
# 任意コマンドを実行できる。信頼できるローカル環境専用 (127.0.0.1 限定 +
|
|
20
|
+
# トークン認証) で使うこと。DESIGN §13 の shell 展開禁止原則の例外。
|
|
21
|
+
class ConPTY
|
|
22
|
+
# Win32 API 束縛。FFI に size/alignment を委ね、手動オフセットを排除する。
|
|
23
|
+
module Native
|
|
24
|
+
extend FFI::Library
|
|
25
|
+
ffi_lib "kernel32"
|
|
26
|
+
ffi_convention :stdcall
|
|
27
|
+
|
|
28
|
+
# STARTUPINFOW (x64)。フィールド順は Win32 定義に厳密に一致させる。
|
|
29
|
+
class StartupInfoW < FFI::Struct
|
|
30
|
+
layout(
|
|
31
|
+
:cb, :uint32,
|
|
32
|
+
:lpReserved, :pointer,
|
|
33
|
+
:lpDesktop, :pointer,
|
|
34
|
+
:lpTitle, :pointer,
|
|
35
|
+
:dwX, :uint32,
|
|
36
|
+
:dwY, :uint32,
|
|
37
|
+
:dwXSize, :uint32,
|
|
38
|
+
:dwYSize, :uint32,
|
|
39
|
+
:dwXCountChars, :uint32,
|
|
40
|
+
:dwYCountChars, :uint32,
|
|
41
|
+
:dwFillAttribute, :uint32,
|
|
42
|
+
:dwFlags, :uint32,
|
|
43
|
+
:wShowWindow, :uint16,
|
|
44
|
+
:cbReserved2, :uint16,
|
|
45
|
+
:lpReserved2, :pointer,
|
|
46
|
+
:hStdInput, :pointer,
|
|
47
|
+
:hStdOutput, :pointer,
|
|
48
|
+
:hStdError, :pointer
|
|
49
|
+
)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# STARTUPINFOEXW = STARTUPINFOW + lpAttributeList。ネストで正しいオフセットに。
|
|
53
|
+
class StartupInfoExW < FFI::Struct
|
|
54
|
+
layout :StartupInfo, StartupInfoW, :lpAttributeList, :pointer
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
class ProcessInformation < FFI::Struct
|
|
58
|
+
layout :hProcess, :pointer, :hThread, :pointer,
|
|
59
|
+
:dwProcessId, :uint32, :dwThreadId, :uint32
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
EXTENDED_STARTUPINFO_PRESENT = 0x00080000
|
|
63
|
+
PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE = 0x00020016
|
|
64
|
+
LMEM_ZEROINIT = 0x0040
|
|
65
|
+
WAIT_TIMEOUT = 0x102
|
|
66
|
+
|
|
67
|
+
attach_function :CreatePipe, %i[pointer pointer pointer uint32], :int32
|
|
68
|
+
# COORD(80x25 等) は 4 バイトなので uint32 に X:lo16 / Y:hi16 でパックして値渡し。
|
|
69
|
+
attach_function :CreatePseudoConsole, %i[uint32 pointer pointer uint32 pointer], :long
|
|
70
|
+
attach_function :ResizePseudoConsole, %i[pointer uint32], :long
|
|
71
|
+
attach_function :ClosePseudoConsole, [:pointer], :void
|
|
72
|
+
attach_function :InitializeProcThreadAttributeList,
|
|
73
|
+
%i[pointer uint32 uint32 pointer], :int32
|
|
74
|
+
attach_function :UpdateProcThreadAttribute,
|
|
75
|
+
%i[pointer uint32 uintptr_t pointer size_t pointer pointer], :int32
|
|
76
|
+
attach_function :DeleteProcThreadAttributeList, [:pointer], :void
|
|
77
|
+
attach_function :CreateProcessW,
|
|
78
|
+
%i[pointer pointer pointer pointer int32 uint32
|
|
79
|
+
pointer pointer pointer pointer], :int32
|
|
80
|
+
attach_function :ReadFile, %i[pointer pointer uint32 pointer pointer], :int32
|
|
81
|
+
attach_function :WriteFile, %i[pointer pointer uint32 pointer pointer], :int32
|
|
82
|
+
attach_function :PeekNamedPipe,
|
|
83
|
+
%i[pointer pointer uint32 pointer pointer pointer], :int32
|
|
84
|
+
attach_function :WaitForSingleObject, %i[pointer uint32], :uint32
|
|
85
|
+
attach_function :GetExitCodeProcess, %i[pointer pointer], :int32
|
|
86
|
+
attach_function :TerminateProcess, %i[pointer uint32], :int32
|
|
87
|
+
attach_function :CloseHandle, [:pointer], :int32
|
|
88
|
+
attach_function :GetLastError, [], :uint32
|
|
89
|
+
attach_function :LocalAlloc, %i[uint32 size_t], :pointer
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
class Error < Chocomint::Error; end
|
|
93
|
+
|
|
94
|
+
# 出力を UTF-8 に固定する (-NoExit で初期コマンド実行後もセッションを継続)。
|
|
95
|
+
# これをしないと OutputEncoding が環境依存になり、日本語が化ける (âââ 等)。
|
|
96
|
+
DEFAULT_SHELL = %(pwsh.exe -NoProfile -NoLogo -NoExit ) +
|
|
97
|
+
%(-Command "[Console]::OutputEncoding=[Text.Encoding]::UTF8;) +
|
|
98
|
+
%($OutputEncoding=[Text.Encoding]::UTF8;chcp 65001 > $null;Clear-Host")
|
|
99
|
+
STILL_ACTIVE = 259
|
|
100
|
+
|
|
101
|
+
# COORD を CreatePseudoConsole/ResizePseudoConsole 用の DWORD にパックする。
|
|
102
|
+
def self.pack_size(cols, rows)
|
|
103
|
+
(rows << 16) | cols
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# 起動せずにインスタンスだけ作る。実際の起動は #open で行う。
|
|
107
|
+
# command: 起動するシェル。cwd: 作業ディレクトリ (nil で継承)。
|
|
108
|
+
def initialize(command: DEFAULT_SHELL, cols: 80, rows: 25, cwd: nil)
|
|
109
|
+
@command = command
|
|
110
|
+
@cols = cols
|
|
111
|
+
@rows = rows
|
|
112
|
+
@cwd = cwd
|
|
113
|
+
@open = false
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
attr_reader :cols, :rows
|
|
117
|
+
|
|
118
|
+
def open?
|
|
119
|
+
@open
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# 擬似コンソールを作りシェルを起動する。二重 open は禁止。
|
|
123
|
+
def open
|
|
124
|
+
raise Error, "already open" if @open
|
|
125
|
+
|
|
126
|
+
create_pipes
|
|
127
|
+
create_pseudo_console
|
|
128
|
+
build_attribute_list
|
|
129
|
+
spawn_process
|
|
130
|
+
# 親側で不要なハンドルを閉じる。out_write を閉じないと EOF が来ない。
|
|
131
|
+
Native.CloseHandle(@out_write)
|
|
132
|
+
Native.CloseHandle(@in_read)
|
|
133
|
+
@out_write = nil
|
|
134
|
+
@in_read = nil
|
|
135
|
+
@open = true
|
|
136
|
+
self
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# シェルへ書き込む (xterm.js のキー入力に相当)。バイト数を返す。
|
|
140
|
+
def write(data)
|
|
141
|
+
ensure_open
|
|
142
|
+
bytes = data.to_s.b
|
|
143
|
+
return 0 if bytes.empty?
|
|
144
|
+
|
|
145
|
+
buf = FFI::MemoryPointer.new(:uint8, bytes.bytesize)
|
|
146
|
+
buf.put_bytes(0, bytes)
|
|
147
|
+
written = FFI::MemoryPointer.new(:uint32)
|
|
148
|
+
ok = Native.WriteFile(@in_write, buf, bytes.bytesize, written, nil)
|
|
149
|
+
raise Error, "WriteFile failed (#{Native.GetLastError})" if ok.zero?
|
|
150
|
+
|
|
151
|
+
written.read(:uint32)
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
# 現在パイプに来ている出力をノンブロッキングで読む。無ければ "" を返す
|
|
155
|
+
# (バイナリエンコーディング; VT シーケンスを含む生ストリーム)。
|
|
156
|
+
def read_available(max = 65_536)
|
|
157
|
+
ensure_open
|
|
158
|
+
avail = FFI::MemoryPointer.new(:uint32)
|
|
159
|
+
peek_ok = Native.PeekNamedPipe(@out_read, nil, 0, nil, avail, nil)
|
|
160
|
+
# PeekNamedPipe 失敗はパイプが壊れた (子終了) とみなす。
|
|
161
|
+
return nil if peek_ok.zero?
|
|
162
|
+
|
|
163
|
+
n = avail.read(:uint32)
|
|
164
|
+
return "".b if n.zero?
|
|
165
|
+
|
|
166
|
+
want = [n, max].min
|
|
167
|
+
buf = FFI::MemoryPointer.new(:uint8, want)
|
|
168
|
+
read = FFI::MemoryPointer.new(:uint32)
|
|
169
|
+
read_ok = Native.ReadFile(@out_read, buf, want, read, nil)
|
|
170
|
+
return nil if read_ok.zero?
|
|
171
|
+
|
|
172
|
+
buf.get_bytes(0, read.read(:uint32))
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
# 端末サイズを変更する (xterm.js のリサイズに追従)。
|
|
176
|
+
def resize(cols, rows)
|
|
177
|
+
ensure_open
|
|
178
|
+
hr = Native.ResizePseudoConsole(@hpcon, self.class.pack_size(cols, rows))
|
|
179
|
+
raise Error, "ResizePseudoConsole failed (hr=0x#{hr.to_s(16)})" unless hr.zero?
|
|
180
|
+
|
|
181
|
+
@cols = cols
|
|
182
|
+
@rows = rows
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
# 子プロセスが生きているか。
|
|
186
|
+
def alive?
|
|
187
|
+
return false unless @open
|
|
188
|
+
|
|
189
|
+
code = FFI::MemoryPointer.new(:uint32)
|
|
190
|
+
return false if Native.GetExitCodeProcess(@proc_handle, code).zero?
|
|
191
|
+
|
|
192
|
+
code.read(:uint32) == STILL_ACTIVE
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
# 子の終了コード (まだ生きているなら nil)。
|
|
196
|
+
def exit_code
|
|
197
|
+
return nil unless @open
|
|
198
|
+
|
|
199
|
+
code = FFI::MemoryPointer.new(:uint32)
|
|
200
|
+
return nil if Native.GetExitCodeProcess(@proc_handle, code).zero?
|
|
201
|
+
|
|
202
|
+
value = code.read(:uint32)
|
|
203
|
+
value == STILL_ACTIVE ? nil : value
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
# 擬似コンソールとプロセスを閉じ、ハンドルを解放する。冪等。
|
|
207
|
+
def close
|
|
208
|
+
return unless @open
|
|
209
|
+
|
|
210
|
+
Native.TerminateProcess(@proc_handle, 0) if alive?
|
|
211
|
+
Native.ClosePseudoConsole(@hpcon) if @hpcon
|
|
212
|
+
Native.CloseHandle(@in_write) if @in_write
|
|
213
|
+
Native.CloseHandle(@out_read) if @out_read
|
|
214
|
+
Native.CloseHandle(@proc_handle) if @proc_handle
|
|
215
|
+
Native.DeleteProcThreadAttributeList(@attr_list) if @attr_list
|
|
216
|
+
@open = false
|
|
217
|
+
nil
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
private
|
|
221
|
+
|
|
222
|
+
def ensure_open
|
|
223
|
+
raise Error, "not open" unless @open
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
def create_pipes
|
|
227
|
+
# 公式 ConPTY サンプル準拠: パイプは継承フラグなし。ハンドルは ConPTY 属性
|
|
228
|
+
# 経由でのみ子へ渡す (CreateProcess の bInheritHandles も FALSE)。
|
|
229
|
+
in_read = FFI::MemoryPointer.new(:pointer)
|
|
230
|
+
in_write = FFI::MemoryPointer.new(:pointer)
|
|
231
|
+
out_read = FFI::MemoryPointer.new(:pointer)
|
|
232
|
+
out_write = FFI::MemoryPointer.new(:pointer)
|
|
233
|
+
raise Error, "CreatePipe(in) failed" if Native.CreatePipe(in_read, in_write, nil, 0).zero?
|
|
234
|
+
raise Error, "CreatePipe(out) failed" if Native.CreatePipe(out_read, out_write, nil, 0).zero?
|
|
235
|
+
|
|
236
|
+
@in_read = in_read.read_pointer
|
|
237
|
+
@in_write = in_write.read_pointer
|
|
238
|
+
@out_read = out_read.read_pointer
|
|
239
|
+
@out_write = out_write.read_pointer
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
def create_pseudo_console
|
|
243
|
+
hpc = FFI::MemoryPointer.new(:pointer)
|
|
244
|
+
hr = Native.CreatePseudoConsole(
|
|
245
|
+
self.class.pack_size(@cols, @rows), @in_read, @out_write, 0, hpc
|
|
246
|
+
)
|
|
247
|
+
raise Error, "CreatePseudoConsole failed (hr=0x#{hr.to_s(16)})" unless hr.zero?
|
|
248
|
+
|
|
249
|
+
@hpcon = hpc.read_pointer
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
def build_attribute_list
|
|
253
|
+
lp = FFI::MemoryPointer.new(:size_t)
|
|
254
|
+
Native.InitializeProcThreadAttributeList(nil, 1, 0, lp) # サイズ取得(1回目は失敗が正常)
|
|
255
|
+
@attr_list = Native.LocalAlloc(Native::LMEM_ZEROINIT, lp.read(:size_t))
|
|
256
|
+
raise Error, "LocalAlloc failed" if @attr_list.null?
|
|
257
|
+
if Native.InitializeProcThreadAttributeList(@attr_list, 1, 0, lp).zero?
|
|
258
|
+
raise Error, "InitializeProcThreadAttributeList failed"
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
# lpValue には HPCON 値そのもの(=ハンドル)を渡す。cbSize はポインタ幅。
|
|
262
|
+
ok = Native.UpdateProcThreadAttribute(
|
|
263
|
+
@attr_list, 0, Native::PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE,
|
|
264
|
+
@hpcon, FFI::Pointer.size, nil, nil
|
|
265
|
+
)
|
|
266
|
+
raise Error, "UpdateProcThreadAttribute failed (#{Native.GetLastError})" if ok.zero?
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
def spawn_process
|
|
270
|
+
si = Native::StartupInfoExW.new
|
|
271
|
+
si[:StartupInfo][:cb] = Native::StartupInfoExW.size
|
|
272
|
+
si[:lpAttributeList] = @attr_list
|
|
273
|
+
pi = Native::ProcessInformation.new
|
|
274
|
+
|
|
275
|
+
cmd_w = @command.encode("UTF-16LE").b + "\x00\x00".b
|
|
276
|
+
cmd_ptr = FFI::MemoryPointer.new(:uint8, cmd_w.bytesize)
|
|
277
|
+
cmd_ptr.put_bytes(0, cmd_w)
|
|
278
|
+
cwd_ptr = nil
|
|
279
|
+
if @cwd
|
|
280
|
+
cwd_w = @cwd.encode("UTF-16LE").b + "\x00\x00".b
|
|
281
|
+
cwd_ptr = FFI::MemoryPointer.new(:uint8, cwd_w.bytesize)
|
|
282
|
+
cwd_ptr.put_bytes(0, cwd_w)
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
ok = Native.CreateProcessW(
|
|
286
|
+
nil, cmd_ptr, nil, nil, 0, Native::EXTENDED_STARTUPINFO_PRESENT,
|
|
287
|
+
nil, cwd_ptr, si.pointer, pi.pointer
|
|
288
|
+
)
|
|
289
|
+
raise Error, "CreateProcessW failed (#{Native.GetLastError})" if ok.zero?
|
|
290
|
+
|
|
291
|
+
@proc_handle = pi[:hProcess]
|
|
292
|
+
Native.CloseHandle(pi[:hThread])
|
|
293
|
+
end
|
|
294
|
+
end
|
|
295
|
+
end
|
|
296
|
+
end
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "socket"
|
|
4
|
+
require "json"
|
|
5
|
+
require "base64"
|
|
6
|
+
require "uri"
|
|
7
|
+
require "websocket/driver"
|
|
8
|
+
require_relative "worker_process"
|
|
9
|
+
|
|
10
|
+
module Chocomint
|
|
11
|
+
module Console
|
|
12
|
+
# xterm.js のバックエンドとなる WebSocket サーバー。
|
|
13
|
+
#
|
|
14
|
+
# WEBrick は WebSocket 非対応なので、コンソール用途だけ独立した TCP サーバーを
|
|
15
|
+
# 別スレッド・別ポートで動かす (既存の /edit・/logs HTTP は WEBrick のまま)。
|
|
16
|
+
# 接続ごとに ConPTY ワーカー (新規コンソール付き) を spawn し、ブラウザの
|
|
17
|
+
# キー入力を tty へ、tty 出力をブラウザへ双方向に橋渡しする。
|
|
18
|
+
#
|
|
19
|
+
# セキュリティ: 生の対話 tty は監督外で任意コマンド実行が可能。bind は
|
|
20
|
+
# 127.0.0.1 固定。token を要求し (URL クエリ ?token=)、一致しなければ拒否する。
|
|
21
|
+
class Server
|
|
22
|
+
# command: 起動するシェル。cwd: 作業ディレクトリ。token: 接続に要求する共有秘密。
|
|
23
|
+
def initialize(host: "127.0.0.1", port: 8081, command: ConPTY::DEFAULT_SHELL,
|
|
24
|
+
cwd: nil, token: nil, logger: $stderr)
|
|
25
|
+
@host = host
|
|
26
|
+
@port = port
|
|
27
|
+
@command = command
|
|
28
|
+
@cwd = cwd
|
|
29
|
+
@token = token
|
|
30
|
+
@logger = logger
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# ブロッキングで待ち受ける。別スレッドで呼ぶ想定 (#start_in_thread)。
|
|
34
|
+
def start
|
|
35
|
+
server = TCPServer.new(@host, @port)
|
|
36
|
+
@logger.puts("chocomint console (ws) listening on ws://#{@host}:#{@port}/")
|
|
37
|
+
loop do
|
|
38
|
+
socket = server.accept
|
|
39
|
+
Thread.new(socket) { |s| handle_connection(s) }
|
|
40
|
+
end
|
|
41
|
+
rescue IOError, Errno::EBADF
|
|
42
|
+
# サーバーソケットが閉じられた (shutdown)。
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def start_in_thread
|
|
46
|
+
Thread.new { start }
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
private
|
|
50
|
+
|
|
51
|
+
def handle_connection(socket)
|
|
52
|
+
conn = Connection.new(socket, command: @command, cwd: @cwd, token: @token,
|
|
53
|
+
logger: @logger)
|
|
54
|
+
conn.run
|
|
55
|
+
rescue StandardError => e
|
|
56
|
+
@logger.puts("console connection error: #{e.class}: #{e.message}")
|
|
57
|
+
ensure
|
|
58
|
+
socket.close rescue nil
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# 1 つの WebSocket 接続を ConPTY ワーカーへ橋渡しするアダプタ。
|
|
62
|
+
class Connection
|
|
63
|
+
def initialize(socket, command:, cwd:, token:, logger:)
|
|
64
|
+
@socket = socket
|
|
65
|
+
@command = command
|
|
66
|
+
@cwd = cwd
|
|
67
|
+
@token = token
|
|
68
|
+
@logger = logger
|
|
69
|
+
@open = false
|
|
70
|
+
@worker = nil
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# websocket-driver は write(String) を持つオブジェクトを要求する。
|
|
74
|
+
def write(data)
|
|
75
|
+
@socket.write(data)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def run
|
|
79
|
+
@driver = WebSocket::Driver.server(self)
|
|
80
|
+
setup_driver(@driver)
|
|
81
|
+
|
|
82
|
+
loop do
|
|
83
|
+
data = @socket.readpartial(65_536)
|
|
84
|
+
@driver.parse(data)
|
|
85
|
+
end
|
|
86
|
+
rescue EOFError, IOError, Errno::ECONNRESET
|
|
87
|
+
# クライアント切断。
|
|
88
|
+
ensure
|
|
89
|
+
@pump&.kill
|
|
90
|
+
@worker&.close
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
private
|
|
94
|
+
|
|
95
|
+
def setup_driver(driver)
|
|
96
|
+
# ハンドシェイクの QUERY_STRING の ?token= で認証する。失敗なら接続を閉じる。
|
|
97
|
+
driver.on(:connect) do
|
|
98
|
+
if authorized?(driver)
|
|
99
|
+
driver.start
|
|
100
|
+
else
|
|
101
|
+
@socket.close rescue nil
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
driver.on(:open) do
|
|
106
|
+
@open = true
|
|
107
|
+
start_worker(driver)
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
driver.on(:message) { |event| handle_client_message(event.data) }
|
|
111
|
+
driver.on(:close) { @worker&.close }
|
|
112
|
+
driver.on(:error) { |e| @logger.puts("console ws error: #{e.message}") }
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# ハンドシェイク要求の QUERY_STRING の token が設定値と一致するか。
|
|
116
|
+
# token 未設定なら常に許可 (ローカル限定運用向け)。
|
|
117
|
+
def authorized?(driver)
|
|
118
|
+
return true if @token.nil? || @token.empty?
|
|
119
|
+
|
|
120
|
+
query = driver.env["QUERY_STRING"].to_s
|
|
121
|
+
params = (URI.decode_www_form(query).to_h rescue {})
|
|
122
|
+
params["token"] == @token
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def start_worker(driver)
|
|
126
|
+
@worker = WorkerProcess.new(command: @command, cols: 80, rows: 25, cwd: @cwd).start
|
|
127
|
+
@pump = start_worker_pump(driver)
|
|
128
|
+
rescue StandardError => e
|
|
129
|
+
driver.text(JSON.generate("type" => "error", "message" => e.message))
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# worker → ブラウザ: tty 出力を WebSocket テキストフレームで送る。
|
|
133
|
+
def start_worker_pump(driver)
|
|
134
|
+
Thread.new do
|
|
135
|
+
loop do
|
|
136
|
+
msg = @worker.read_message
|
|
137
|
+
if msg
|
|
138
|
+
driver.text(JSON.generate(msg))
|
|
139
|
+
break if msg["type"] == "exit"
|
|
140
|
+
elsif !@worker.alive?
|
|
141
|
+
driver.text(JSON.generate("type" => "exit", "code" => 0))
|
|
142
|
+
break
|
|
143
|
+
end
|
|
144
|
+
sleep 0.01
|
|
145
|
+
end
|
|
146
|
+
rescue StandardError
|
|
147
|
+
nil
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
# ブラウザ → worker: 入力・リサイズを転送する。
|
|
152
|
+
def handle_client_message(data)
|
|
153
|
+
msg = JSON.parse(data)
|
|
154
|
+
case msg["type"]
|
|
155
|
+
when "input" then @worker&.send_input(msg["data"].to_s)
|
|
156
|
+
when "resize" then @worker&.send_resize(msg["cols"].to_i, msg["rows"].to_i)
|
|
157
|
+
end
|
|
158
|
+
rescue JSON::ParserError
|
|
159
|
+
nil
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
end
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "base64"
|
|
5
|
+
require_relative "conpty"
|
|
6
|
+
|
|
7
|
+
module Chocomint
|
|
8
|
+
module Console
|
|
9
|
+
# ConPTY を保持し、名前付きパイプを介して親 (WorkerProcess) と対話するワーカー。
|
|
10
|
+
#
|
|
11
|
+
# ConPTY は「コンソール付きプロセス」からしか正しく動かない
|
|
12
|
+
# (chocomint-conpty メモの制約) ため、このワーカーは新規コンソール付きで
|
|
13
|
+
# spawn される。通信に stdio を使うと新規コンソールとの関連付けが壊れて
|
|
14
|
+
# ConPTY 出力が止まるため、制御通信は独立した名前付きパイプで行う。
|
|
15
|
+
#
|
|
16
|
+
# プロトコル (親 ⇄ worker、いずれも 1 行 = 1 JSON):
|
|
17
|
+
# 親 → worker: {"type":"input","data":<base64>} / {"type":"resize","cols":N,"rows":M}
|
|
18
|
+
# worker → 親: {"type":"output","data":<base64>} / {"type":"exit","code":N}
|
|
19
|
+
class Worker
|
|
20
|
+
# c2w_name: 親→worker パイプ, w2c_name: worker→親 パイプ (親が作成済み)。
|
|
21
|
+
def initialize(command:, cols:, rows:, cwd:, c2w_name:, w2c_name:)
|
|
22
|
+
@pty = ConPTY.new(command: command, cols: cols, rows: rows, cwd: cwd)
|
|
23
|
+
@c2w_name = c2w_name
|
|
24
|
+
@w2c_name = w2c_name
|
|
25
|
+
@mutex = Mutex.new
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def run
|
|
29
|
+
# 親の ConnectNamedPipe の順序 (c2w → w2c) に合わせて接続する。
|
|
30
|
+
@c2w = File.open(@c2w_name, "rb")
|
|
31
|
+
@w2c = File.open(@w2c_name, "wb")
|
|
32
|
+
@w2c.sync = true
|
|
33
|
+
@pty.open
|
|
34
|
+
reader = start_output_pump
|
|
35
|
+
while (line = @c2w.gets)
|
|
36
|
+
handle(line)
|
|
37
|
+
end
|
|
38
|
+
rescue StandardError
|
|
39
|
+
# 接続断・tty 破損等。ensure でクリーンアップする。
|
|
40
|
+
ensure
|
|
41
|
+
@pty.close if @pty.open?
|
|
42
|
+
reader&.join(1)
|
|
43
|
+
@c2w&.close
|
|
44
|
+
@w2c&.close
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
private
|
|
48
|
+
|
|
49
|
+
def start_output_pump
|
|
50
|
+
Thread.new do
|
|
51
|
+
loop do
|
|
52
|
+
chunk = @pty.read_available
|
|
53
|
+
if chunk.nil? # パイプ破損 = 子終了
|
|
54
|
+
emit("exit", "code" => (@pty.exit_code || 0))
|
|
55
|
+
break
|
|
56
|
+
end
|
|
57
|
+
emit("output", "data" => Base64.strict_encode64(chunk)) unless chunk.empty?
|
|
58
|
+
sleep 0.01
|
|
59
|
+
end
|
|
60
|
+
rescue StandardError => e
|
|
61
|
+
emit("error", "message" => e.message)
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def handle(line)
|
|
66
|
+
msg = JSON.parse(line)
|
|
67
|
+
case msg["type"]
|
|
68
|
+
when "input" then @pty.write(Base64.strict_decode64(msg["data"].to_s))
|
|
69
|
+
when "resize" then @pty.resize(msg["cols"].to_i, msg["rows"].to_i)
|
|
70
|
+
end
|
|
71
|
+
rescue JSON::ParserError
|
|
72
|
+
nil
|
|
73
|
+
rescue ConPTY::Error
|
|
74
|
+
nil
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def emit(type, extra = {})
|
|
78
|
+
@mutex.synchronize do
|
|
79
|
+
@w2c.write(JSON.generate({ "type" => type }.merge(extra)))
|
|
80
|
+
@w2c.write("\n")
|
|
81
|
+
@w2c.flush
|
|
82
|
+
end
|
|
83
|
+
rescue StandardError
|
|
84
|
+
nil
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|