Class: Chocomint::Console::ConPTY
- Inherits:
-
Object
- Object
- Chocomint::Console::ConPTY
- Defined in:
- lib/chocomint/console/conpty.rb
Overview
Windows の擬似コンソール (ConPTY) を FFI で直接叩き、対話シェルを 本物の tty として起動するラッパー。xterm.js のバックエンドに使う。
重要な前提 (スパイク検証で判明): CreatePseudoConsole を呼ぶプロセスは 自身がコンソールにアタッチされている必要がある。コンソールを持たない プロセス (CREATE_NO_WINDOW で起動された等) から生成すると、初期 VT シーケンスは出るが子プロセスの出力が out パイプに流れてこない。 そのため本クラスを使うワーカーは必ずコンソール付きで起動すること (例: 非表示ウィンドウの新規コンソール)。詳細は console_worker を参照。
セキュリティ注意: 生の対話 tty は PathGuard / allowed_commands の監督外で 任意コマンドを実行できる。信頼できるローカル環境専用 (127.0.0.1 限定 + トークン認証) で使うこと。DESIGN §13 の shell 展開禁止原則の例外。
Defined Under Namespace
Modules: Native Classes: Error
Constant Summary collapse
- DEFAULT_SHELL =
出力を UTF-8 に固定する (-NoExit で初期コマンド実行後もセッションを継続)。 これをしないと OutputEncoding が環境依存になり、日本語が化ける (âââ 等)。
%(pwsh.exe -NoProfile -NoLogo -NoExit ) + %(-Command "[Console]::OutputEncoding=[Text.Encoding]::UTF8;) + %($OutputEncoding=[Text.Encoding]::UTF8;chcp 65001 > $null;Clear-Host")
- STILL_ACTIVE =
259
Instance Attribute Summary collapse
-
#cols ⇒ Object
readonly
Returns the value of attribute cols.
-
#rows ⇒ Object
readonly
Returns the value of attribute rows.
Class Method Summary collapse
-
.pack_size(cols, rows) ⇒ Object
COORD を CreatePseudoConsole/ResizePseudoConsole 用の DWORD にパックする。.
Instance Method Summary collapse
-
#alive? ⇒ Boolean
子プロセスが生きているか。.
-
#close ⇒ Object
擬似コンソールとプロセスを閉じ、ハンドルを解放する。冪等。.
-
#exit_code ⇒ Object
子の終了コード (まだ生きているなら nil)。.
-
#initialize(command: DEFAULT_SHELL, cols: 80, rows: 25, cwd: nil) ⇒ ConPTY
constructor
起動せずにインスタンスだけ作る。実際の起動は #open で行う。 command: 起動するシェル。cwd: 作業ディレクトリ (nil で継承)。.
-
#open ⇒ Object
擬似コンソールを作りシェルを起動する。二重 open は禁止。.
- #open? ⇒ Boolean
-
#read_available(max = 65_536) ⇒ Object
現在パイプに来ている出力をノンブロッキングで読む。無ければ "" を返す (バイナリエンコーディング; VT シーケンスを含む生ストリーム)。.
-
#resize(cols, rows) ⇒ Object
端末サイズを変更する (xterm.js のリサイズに追従)。.
-
#write(data) ⇒ Object
シェルへ書き込む (xterm.js のキー入力に相当)。バイト数を返す。.
Constructor Details
#initialize(command: DEFAULT_SHELL, cols: 80, rows: 25, cwd: nil) ⇒ ConPTY
起動せずにインスタンスだけ作る。実際の起動は #open で行う。 command: 起動するシェル。cwd: 作業ディレクトリ (nil で継承)。
108 109 110 111 112 113 114 |
# File 'lib/chocomint/console/conpty.rb', line 108 def initialize(command: DEFAULT_SHELL, cols: 80, rows: 25, cwd: nil) @command = command @cols = cols @rows = rows @cwd = cwd @open = false end |
Instance Attribute Details
#cols ⇒ Object (readonly)
Returns the value of attribute cols.
116 117 118 |
# File 'lib/chocomint/console/conpty.rb', line 116 def cols @cols end |
#rows ⇒ Object (readonly)
Returns the value of attribute rows.
116 117 118 |
# File 'lib/chocomint/console/conpty.rb', line 116 def rows @rows end |
Class Method Details
.pack_size(cols, rows) ⇒ Object
COORD を CreatePseudoConsole/ResizePseudoConsole 用の DWORD にパックする。
102 103 104 |
# File 'lib/chocomint/console/conpty.rb', line 102 def self.pack_size(cols, rows) (rows << 16) | cols end |
Instance Method Details
#alive? ⇒ Boolean
子プロセスが生きているか。
186 187 188 189 190 191 192 193 |
# File 'lib/chocomint/console/conpty.rb', line 186 def alive? return false unless @open code = FFI::MemoryPointer.new(:uint32) return false if Native.GetExitCodeProcess(@proc_handle, code).zero? code.read(:uint32) == STILL_ACTIVE end |
#close ⇒ Object
擬似コンソールとプロセスを閉じ、ハンドルを解放する。冪等。
207 208 209 210 211 212 213 214 215 216 217 218 |
# File 'lib/chocomint/console/conpty.rb', line 207 def close return unless @open Native.TerminateProcess(@proc_handle, 0) if alive? Native.ClosePseudoConsole(@hpcon) if @hpcon Native.CloseHandle(@in_write) if @in_write Native.CloseHandle(@out_read) if @out_read Native.CloseHandle(@proc_handle) if @proc_handle Native.DeleteProcThreadAttributeList(@attr_list) if @attr_list @open = false nil end |
#exit_code ⇒ Object
子の終了コード (まだ生きているなら nil)。
196 197 198 199 200 201 202 203 204 |
# File 'lib/chocomint/console/conpty.rb', line 196 def exit_code return nil unless @open code = FFI::MemoryPointer.new(:uint32) return nil if Native.GetExitCodeProcess(@proc_handle, code).zero? value = code.read(:uint32) value == STILL_ACTIVE ? nil : value end |
#open ⇒ Object
擬似コンソールを作りシェルを起動する。二重 open は禁止。
123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 |
# File 'lib/chocomint/console/conpty.rb', line 123 def open raise Error, "already open" if @open create_pipes create_pseudo_console build_attribute_list spawn_process # 親側で不要なハンドルを閉じる。out_write を閉じないと EOF が来ない。 Native.CloseHandle(@out_write) Native.CloseHandle(@in_read) @out_write = nil @in_read = nil @open = true self end |
#open? ⇒ Boolean
118 119 120 |
# File 'lib/chocomint/console/conpty.rb', line 118 def open? @open end |
#read_available(max = 65_536) ⇒ Object
現在パイプに来ている出力をノンブロッキングで読む。無ければ "" を返す (バイナリエンコーディング; VT シーケンスを含む生ストリーム)。
156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 |
# File 'lib/chocomint/console/conpty.rb', line 156 def read_available(max = 65_536) ensure_open avail = FFI::MemoryPointer.new(:uint32) peek_ok = Native.PeekNamedPipe(@out_read, nil, 0, nil, avail, nil) # PeekNamedPipe 失敗はパイプが壊れた (子終了) とみなす。 return nil if peek_ok.zero? n = avail.read(:uint32) return "".b if n.zero? want = [n, max].min buf = FFI::MemoryPointer.new(:uint8, want) read = FFI::MemoryPointer.new(:uint32) read_ok = Native.ReadFile(@out_read, buf, want, read, nil) return nil if read_ok.zero? buf.get_bytes(0, read.read(:uint32)) end |
#resize(cols, rows) ⇒ Object
端末サイズを変更する (xterm.js のリサイズに追従)。
176 177 178 179 180 181 182 183 |
# File 'lib/chocomint/console/conpty.rb', line 176 def resize(cols, rows) ensure_open hr = Native.ResizePseudoConsole(@hpcon, self.class.pack_size(cols, rows)) raise Error, "ResizePseudoConsole failed (hr=0x#{hr.to_s(16)})" unless hr.zero? @cols = cols @rows = rows end |
#write(data) ⇒ Object
シェルへ書き込む (xterm.js のキー入力に相当)。バイト数を返す。
140 141 142 143 144 145 146 147 148 149 150 151 152 |
# File 'lib/chocomint/console/conpty.rb', line 140 def write(data) ensure_open bytes = data.to_s.b return 0 if bytes.empty? buf = FFI::MemoryPointer.new(:uint8, bytes.bytesize) buf.put_bytes(0, bytes) written = FFI::MemoryPointer.new(:uint32) ok = Native.WriteFile(@in_write, buf, bytes.bytesize, written, nil) raise Error, "WriteFile failed (#{Native.GetLastError})" if ok.zero? written.read(:uint32) end |