wcgw 2.8.10__py3-none-any.whl → 3.0.1__py3-none-any.whl

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.

Potentially problematic release.


This version of wcgw might be problematic. Click here for more details.

@@ -0,0 +1,788 @@
1
+ import datetime
2
+ import os
3
+ import platform
4
+ import subprocess
5
+ import threading
6
+ import time
7
+ import traceback
8
+ from dataclasses import dataclass
9
+ from typing import (
10
+ Any,
11
+ Literal,
12
+ Optional,
13
+ ParamSpec,
14
+ TypeVar,
15
+ )
16
+
17
+ import pexpect
18
+ import pyte
19
+
20
+ from ...types_ import (
21
+ BashCommand,
22
+ Command,
23
+ Console,
24
+ Modes,
25
+ SendAscii,
26
+ SendSpecials,
27
+ SendText,
28
+ StatusCheck,
29
+ )
30
+ from ..encoder import EncoderDecoder
31
+ from ..modes import BashCommandMode, FileEditMode, WriteIfEmptyMode
32
+
33
+ PROMPT_CONST = "wcgw→" + " "
34
+ PROMPT_STATEMENT = "export GIT_PAGER=cat PAGER=cat PROMPT_COMMAND= PS1='wcgw→'' '"
35
+ BASH_CLF_OUTPUT = Literal["repl", "pending"]
36
+ os.environ["TOKENIZERS_PARALLELISM"] = "false"
37
+
38
+
39
+ @dataclass
40
+ class Config:
41
+ timeout: float = 5
42
+ timeout_while_output: float = 20
43
+ output_wait_patience: float = 3
44
+
45
+ def update(
46
+ self, timeout: float, timeout_while_output: float, output_wait_patience: float
47
+ ) -> None:
48
+ self.timeout = timeout
49
+ self.timeout_while_output = timeout_while_output
50
+ self.output_wait_patience = output_wait_patience
51
+
52
+
53
+ CONFIG = Config()
54
+
55
+
56
+ def is_mac() -> bool:
57
+ return platform.system() == "Darwin"
58
+
59
+
60
+ def get_tmpdir() -> str:
61
+ current_tmpdir = os.environ.get("TMPDIR", "")
62
+ if current_tmpdir or not is_mac():
63
+ return current_tmpdir
64
+ try:
65
+ # Fix issue while running ocrmypdf -> tesseract -> leptonica, set TMPDIR
66
+ # https://github.com/tesseract-ocr/tesseract/issues/4333
67
+ result = subprocess.check_output(
68
+ ["getconf", "DARWIN_USER_TEMP_DIR"],
69
+ text=True,
70
+ timeout=CONFIG.timeout,
71
+ ).strip()
72
+ return result
73
+ except (subprocess.CalledProcessError, FileNotFoundError):
74
+ return "//tmp"
75
+ except Exception:
76
+ return ""
77
+
78
+
79
+ def check_if_screen_command_available() -> bool:
80
+ try:
81
+ subprocess.run(["screen", "-v"], capture_output=True, check=True, timeout=0.2)
82
+ return True
83
+ except (subprocess.CalledProcessError, FileNotFoundError):
84
+ return False
85
+
86
+
87
+ def cleanup_all_screens_with_name(name: str, console: Console) -> None:
88
+ """
89
+ There could be in worst case multiple screens with same name, clear them if any.
90
+ Clearing just using "screen -X -S {name} quit" doesn't work because screen complains
91
+ that there are several suitable screens.
92
+ """
93
+ try:
94
+ # Try to get the list of screens.
95
+ result = subprocess.run(
96
+ ["screen", "-ls"],
97
+ capture_output=True,
98
+ text=True,
99
+ check=True,
100
+ timeout=0.2,
101
+ )
102
+ output = result.stdout
103
+ except subprocess.CalledProcessError as e:
104
+ # When no screens exist, screen may return a non-zero exit code.
105
+ output = (e.stdout or "") + (e.stderr or "")
106
+ except FileNotFoundError:
107
+ return
108
+ except Exception as e:
109
+ console.log(f"{e}: exception while clearing running screens.")
110
+ return
111
+
112
+ sessions_to_kill = []
113
+
114
+ # Parse each line of the output. The lines containing sessions typically start with a digit.
115
+ for line in output.splitlines():
116
+ line = line.strip()
117
+ if not line or not line[0].isdigit():
118
+ continue
119
+
120
+ # Each session is usually shown as "1234.my_screen (Detached)".
121
+ # We extract the first part, then split on the period to get the session name.
122
+ session_info = line.split()[0].strip() # e.g., "1234.my_screen"
123
+ if session_info.endswith(f".{name}"):
124
+ sessions_to_kill.append(session_info)
125
+
126
+ # Now, for every session we found, tell screen to quit it.
127
+ for session in sessions_to_kill:
128
+ try:
129
+ subprocess.run(
130
+ ["screen", "-S", session, "-X", "quit"],
131
+ check=True,
132
+ timeout=CONFIG.timeout,
133
+ )
134
+ except Exception as e:
135
+ console.log(f"Failed to kill screen session: {session}\n{e}")
136
+
137
+
138
+ def start_shell(
139
+ is_restricted_mode: bool, initial_dir: str, console: Console, over_screen: bool
140
+ ) -> tuple[pexpect.spawn, str]: # type: ignore[type-arg]
141
+ cmd = "/bin/bash"
142
+ if is_restricted_mode:
143
+ cmd += " -r"
144
+
145
+ overrideenv = {
146
+ **os.environ,
147
+ "PS1": PROMPT_CONST,
148
+ "TMPDIR": get_tmpdir(),
149
+ "TERM": "xterm-256color",
150
+ }
151
+ try:
152
+ shell = pexpect.spawn(
153
+ cmd,
154
+ env=overrideenv, # type: ignore[arg-type]
155
+ echo=True,
156
+ encoding="utf-8",
157
+ timeout=CONFIG.timeout,
158
+ cwd=initial_dir,
159
+ codec_errors="backslashreplace",
160
+ dimensions=(500, 160),
161
+ )
162
+ shell.sendline(PROMPT_STATEMENT) # Unset prompt command to avoid interfering
163
+ shell.expect(PROMPT_CONST, timeout=0.2)
164
+ except Exception as e:
165
+ console.print(traceback.format_exc())
166
+ console.log(f"Error starting shell: {e}. Retrying without rc ...")
167
+
168
+ shell = pexpect.spawn(
169
+ "/bin/bash --noprofile --norc",
170
+ env=overrideenv, # type: ignore[arg-type]
171
+ echo=True,
172
+ encoding="utf-8",
173
+ timeout=CONFIG.timeout,
174
+ codec_errors="backslashreplace",
175
+ )
176
+ shell.sendline(PROMPT_STATEMENT)
177
+ shell.expect(PROMPT_CONST, timeout=0.2)
178
+
179
+ shellid = "wcgw." + time.strftime("%H%M%S")
180
+ if over_screen:
181
+ if not check_if_screen_command_available():
182
+ raise ValueError("Screen command not available")
183
+ # shellid is just hour, minute, second number
184
+ shell.sendline(f"trap 'screen -X -S {shellid} quit' EXIT")
185
+ shell.expect(PROMPT_CONST, timeout=0.2)
186
+
187
+ shell.sendline(f"screen -q -S {shellid} /bin/bash --noprofile --norc")
188
+ shell.expect(PROMPT_CONST, timeout=CONFIG.timeout)
189
+
190
+ return shell, shellid
191
+
192
+
193
+ def _is_int(mystr: str) -> bool:
194
+ try:
195
+ int(mystr)
196
+ return True
197
+ except ValueError:
198
+ return False
199
+
200
+
201
+ def render_terminal_output(text: str) -> list[str]:
202
+ screen = pyte.Screen(160, 500)
203
+ screen.set_mode(pyte.modes.LNM)
204
+ stream = pyte.Stream(screen)
205
+ stream.feed(text)
206
+ # Filter out empty lines
207
+ dsp = screen.display[::-1]
208
+ for i, line in enumerate(dsp):
209
+ if line.strip():
210
+ break
211
+ else:
212
+ i = len(dsp)
213
+ lines = screen.display[: len(dsp) - i]
214
+ return lines
215
+
216
+
217
+ P = ParamSpec("P")
218
+ R = TypeVar("R")
219
+
220
+
221
+ class BashState:
222
+ _use_screen: bool
223
+
224
+ def __init__(
225
+ self,
226
+ console: Console,
227
+ working_dir: str,
228
+ bash_command_mode: Optional[BashCommandMode],
229
+ file_edit_mode: Optional[FileEditMode],
230
+ write_if_empty_mode: Optional[WriteIfEmptyMode],
231
+ mode: Optional[Modes],
232
+ use_screen: bool,
233
+ whitelist_for_overwrite: Optional[set[str]] = None,
234
+ ) -> None:
235
+ self.console = console
236
+ self._cwd = working_dir or os.getcwd()
237
+ self._bash_command_mode: BashCommandMode = bash_command_mode or BashCommandMode(
238
+ "normal_mode", "all"
239
+ )
240
+ self._file_edit_mode: FileEditMode = file_edit_mode or FileEditMode("all")
241
+ self._write_if_empty_mode: WriteIfEmptyMode = (
242
+ write_if_empty_mode or WriteIfEmptyMode("all")
243
+ )
244
+ self._mode = mode or "wcgw"
245
+ self._whitelist_for_overwrite: set[str] = whitelist_for_overwrite or set()
246
+ self._bg_expect_thread: Optional[threading.Thread] = None
247
+ self._bg_expect_thread_stop_event = threading.Event()
248
+ self._use_screen = use_screen
249
+ self._init_shell()
250
+
251
+ def expect(self, pattern: Any, timeout: Optional[float] = -1) -> int:
252
+ self.close_bg_expect_thread()
253
+ output = self._shell.expect(pattern, timeout)
254
+ return output
255
+
256
+ def send(self, s: str | bytes) -> int:
257
+ self.close_bg_expect_thread()
258
+ output = self._shell.send(s)
259
+ return output
260
+
261
+ def sendline(self, s: str | bytes) -> int:
262
+ self.close_bg_expect_thread()
263
+ output = self._shell.sendline(s)
264
+ return output
265
+
266
+ @property
267
+ def linesep(self) -> Any:
268
+ return self._shell.linesep
269
+
270
+ def sendintr(self) -> None:
271
+ self.close_bg_expect_thread()
272
+ self._shell.sendintr()
273
+
274
+ @property
275
+ def before(self) -> Optional[str]:
276
+ return self._shell.before
277
+
278
+ def run_bg_expect_thread(self) -> None:
279
+ """
280
+ Run background expect thread for handling shell interactions.
281
+ """
282
+
283
+ def _bg_expect_thread_handler() -> None:
284
+ while True:
285
+ if self._bg_expect_thread_stop_event.is_set():
286
+ break
287
+ output = self._shell.expect([pexpect.EOF, pexpect.TIMEOUT], timeout=0.1)
288
+ if output == 0:
289
+ break
290
+
291
+ if self._bg_expect_thread:
292
+ self.close_bg_expect_thread()
293
+
294
+ self._bg_expect_thread = threading.Thread(
295
+ target=_bg_expect_thread_handler,
296
+ )
297
+ self._bg_expect_thread.start()
298
+
299
+ def close_bg_expect_thread(self) -> None:
300
+ if self._bg_expect_thread:
301
+ self._bg_expect_thread_stop_event.set()
302
+ self._bg_expect_thread.join()
303
+ self._bg_expect_thread = None
304
+ self._bg_expect_thread_stop_event = threading.Event()
305
+
306
+ def cleanup(self) -> None:
307
+ self.close_bg_expect_thread()
308
+ self._shell.close(True)
309
+ cleanup_all_screens_with_name(self._shell_id, self.console)
310
+
311
+ def __enter__(self) -> "BashState":
312
+ return self
313
+
314
+ def __exit__(self, exc_type: Any, exc_value: Any, traceback: Any) -> None:
315
+ self.cleanup()
316
+
317
+ @property
318
+ def mode(self) -> Modes:
319
+ return self._mode
320
+
321
+ @property
322
+ def bash_command_mode(self) -> BashCommandMode:
323
+ return self._bash_command_mode
324
+
325
+ @property
326
+ def file_edit_mode(self) -> FileEditMode:
327
+ return self._file_edit_mode
328
+
329
+ @property
330
+ def write_if_empty_mode(self) -> WriteIfEmptyMode:
331
+ return self._write_if_empty_mode
332
+
333
+ def ensure_env_and_bg_jobs(self) -> Optional[int]:
334
+ quick_timeout = 0.2 if not self.over_screen else 1
335
+ # First reset the prompt in case venv was sourced or other reasons.
336
+ self.sendline(PROMPT_STATEMENT)
337
+ self.expect(PROMPT_CONST, timeout=quick_timeout)
338
+ # Reset echo also if it was enabled
339
+ command = "jobs | wc -l"
340
+ self.sendline(command)
341
+ before = ""
342
+ counts = 0
343
+ while not _is_int(before): # Consume all previous output
344
+ try:
345
+ self.expect(PROMPT_CONST, timeout=quick_timeout)
346
+ except pexpect.TIMEOUT:
347
+ self.console.print(f"Couldn't get exit code, before: {before}")
348
+ raise
349
+
350
+ before_val = self.before
351
+ if not isinstance(before_val, str):
352
+ before_val = str(before_val)
353
+ assert isinstance(before_val, str)
354
+ before_lines = render_terminal_output(before_val)
355
+ before = "\n".join(before_lines).replace(command, "").strip()
356
+ counts += 1
357
+ if counts > 100:
358
+ raise ValueError(
359
+ "Error in understanding shell output. This shouldn't happen, likely shell is in a bad state, please reset it"
360
+ )
361
+ try:
362
+ return int(before)
363
+ except ValueError:
364
+ raise ValueError(f"Malformed output: {before}")
365
+
366
+ def _init_shell(self) -> None:
367
+ self._state: Literal["repl"] | datetime.datetime = "repl"
368
+ # Ensure self._cwd exists
369
+ os.makedirs(self._cwd, exist_ok=True)
370
+ try:
371
+ self._shell, self._shell_id = start_shell(
372
+ self._bash_command_mode.bash_mode == "restricted_mode",
373
+ self._cwd,
374
+ self.console,
375
+ over_screen=self._use_screen,
376
+ )
377
+ self.over_screen = self._use_screen
378
+ except Exception as e:
379
+ if not isinstance(e, ValueError):
380
+ self.console.log(traceback.format_exc())
381
+ self.console.log("Retrying without using screen")
382
+ # Try without over_screen
383
+ self._shell, self._shell_id = start_shell(
384
+ self._bash_command_mode.bash_mode == "restricted_mode",
385
+ self._cwd,
386
+ self.console,
387
+ over_screen=False,
388
+ )
389
+ self.over_screen = False
390
+
391
+ self._pending_output = ""
392
+ try:
393
+ self.ensure_env_and_bg_jobs()
394
+ except ValueError as e:
395
+ self.console.log("Error while running _ensure_env_and_bg_jobs" + str(e))
396
+
397
+ self.run_bg_expect_thread()
398
+
399
+ def set_pending(self, last_pending_output: str) -> None:
400
+ if not isinstance(self._state, datetime.datetime):
401
+ self._state = datetime.datetime.now()
402
+ self._pending_output = last_pending_output
403
+
404
+ def set_repl(self) -> None:
405
+ self._state = "repl"
406
+ self._pending_output = ""
407
+
408
+ @property
409
+ def state(self) -> BASH_CLF_OUTPUT:
410
+ if self._state == "repl":
411
+ return "repl"
412
+ return "pending"
413
+
414
+ @property
415
+ def cwd(self) -> str:
416
+ return self._cwd
417
+
418
+ @property
419
+ def prompt(self) -> str:
420
+ return PROMPT_CONST
421
+
422
+ def update_cwd(self) -> str:
423
+ self.sendline("pwd")
424
+ self.expect(PROMPT_CONST, timeout=0.2)
425
+ before_val = self.before
426
+ if not isinstance(before_val, str):
427
+ before_val = str(before_val)
428
+ before_lines = render_terminal_output(before_val)
429
+ current_dir = "\n".join(before_lines).strip()
430
+ self._cwd = current_dir
431
+ return current_dir
432
+
433
+ def reset_shell(self) -> None:
434
+ self.cleanup()
435
+ self._init_shell()
436
+
437
+ def serialize(self) -> dict[str, Any]:
438
+ """Serialize BashState to a dictionary for saving"""
439
+ return {
440
+ "bash_command_mode": self._bash_command_mode.serialize(),
441
+ "file_edit_mode": self._file_edit_mode.serialize(),
442
+ "write_if_empty_mode": self._write_if_empty_mode.serialize(),
443
+ "whitelist_for_overwrite": list(self._whitelist_for_overwrite),
444
+ "mode": self._mode,
445
+ }
446
+
447
+ @staticmethod
448
+ def parse_state(
449
+ state: dict[str, Any],
450
+ ) -> tuple[BashCommandMode, FileEditMode, WriteIfEmptyMode, Modes, list[str]]:
451
+ return (
452
+ BashCommandMode.deserialize(state["bash_command_mode"]),
453
+ FileEditMode.deserialize(state["file_edit_mode"]),
454
+ WriteIfEmptyMode.deserialize(state["write_if_empty_mode"]),
455
+ state["mode"],
456
+ state["whitelist_for_overwrite"],
457
+ )
458
+
459
+ def load_state(
460
+ self,
461
+ bash_command_mode: BashCommandMode,
462
+ file_edit_mode: FileEditMode,
463
+ write_if_empty_mode: WriteIfEmptyMode,
464
+ mode: Modes,
465
+ whitelist_for_overwrite: list[str],
466
+ cwd: str,
467
+ ) -> None:
468
+ """Create a new BashState instance from a serialized state dictionary"""
469
+ if (
470
+ self._bash_command_mode == bash_command_mode
471
+ and ((self._cwd == cwd) or not cwd)
472
+ and (self._file_edit_mode == file_edit_mode)
473
+ and (self._write_if_empty_mode == write_if_empty_mode)
474
+ and (self._mode == mode)
475
+ and (self._whitelist_for_overwrite == set(whitelist_for_overwrite))
476
+ ):
477
+ # No need to reset shell if the state is the same
478
+ return
479
+ self._bash_command_mode = bash_command_mode
480
+ self._cwd = cwd or self._cwd
481
+ self._file_edit_mode = file_edit_mode
482
+ self._write_if_empty_mode = write_if_empty_mode
483
+ self._whitelist_for_overwrite = set(whitelist_for_overwrite)
484
+ self._mode = mode
485
+ self.reset_shell()
486
+
487
+ def get_pending_for(self) -> str:
488
+ if isinstance(self._state, datetime.datetime):
489
+ timedelta = datetime.datetime.now() - self._state
490
+ return (
491
+ str(
492
+ int(
493
+ (
494
+ timedelta + datetime.timedelta(seconds=CONFIG.timeout)
495
+ ).total_seconds()
496
+ )
497
+ )
498
+ + " seconds"
499
+ )
500
+
501
+ return "Not pending"
502
+
503
+ @property
504
+ def whitelist_for_overwrite(self) -> set[str]:
505
+ return self._whitelist_for_overwrite
506
+
507
+ def add_to_whitelist_for_overwrite(self, file_path: str) -> None:
508
+ self._whitelist_for_overwrite.add(file_path)
509
+
510
+ @property
511
+ def pending_output(self) -> str:
512
+ return self._pending_output
513
+
514
+
515
+ WAITING_INPUT_MESSAGE = """A command is already running. NOTE: You can't run multiple shell sessions, likely a previous program hasn't exited.
516
+ 1. Get its output using `send_ascii: [10] or send_specials: ["Enter"]`
517
+ 2. Use `send_ascii` or `send_specials` to give inputs to the running program, don't use `BashCommand` OR
518
+ 3. kill the previous program by sending ctrl+c first using `send_ascii` or `send_specials`
519
+ 4. Interrupt and run the process in background by re-running it using screen
520
+ """
521
+
522
+
523
+ def get_incremental_output(old_output: list[str], new_output: list[str]) -> list[str]:
524
+ nold = len(old_output)
525
+ nnew = len(new_output)
526
+ if not old_output:
527
+ return new_output
528
+ for i in range(nnew - 1, -1, -1):
529
+ if new_output[i] != old_output[-1]:
530
+ continue
531
+ for j in range(i - 1, -1, -1):
532
+ if (nold - 1 + j - i) < 0:
533
+ break
534
+ if new_output[j] != old_output[-1 + j - i]:
535
+ break
536
+ else:
537
+ return new_output[i + 1 :]
538
+ return new_output
539
+
540
+
541
+ def rstrip(lines: list[str]) -> str:
542
+ return "\n".join([line.rstrip() for line in lines])
543
+
544
+
545
+ def _incremental_text(text: str, last_pending_output: str) -> str:
546
+ # text = render_terminal_output(text[-100_000:])
547
+ text = text[-100_000:]
548
+
549
+ last_rendered_lines = render_terminal_output(last_pending_output)
550
+ last_pending_output_rendered = "\n".join(last_rendered_lines)
551
+ if not last_rendered_lines:
552
+ return rstrip(render_terminal_output(text))
553
+
554
+ text = text[len(last_pending_output) :]
555
+ old_rendered_applied = render_terminal_output(last_pending_output_rendered + text)
556
+ # True incremental is then
557
+ rendered = get_incremental_output(last_rendered_lines[:-1], old_rendered_applied)
558
+
559
+ if not rendered:
560
+ return ""
561
+
562
+ if rendered[0] == last_rendered_lines[-1]:
563
+ rendered = rendered[1:]
564
+ return rstrip(rendered)
565
+
566
+
567
+ def get_status(bash_state: BashState) -> str:
568
+ status = "\n\n---\n\n"
569
+ if bash_state.state == "pending":
570
+ status += "status = still running\n"
571
+ status += "running for = " + bash_state.get_pending_for() + "\n"
572
+ status += "cwd = " + bash_state.cwd + "\n"
573
+ else:
574
+ bg_jobs = bash_state.ensure_env_and_bg_jobs()
575
+ bg_desc = ""
576
+ if bg_jobs and bg_jobs > 0:
577
+ bg_desc = f"; {bg_jobs} background jobs running"
578
+ status += "status = process exited" + bg_desc + "\n"
579
+ status += "cwd = " + bash_state.update_cwd() + "\n"
580
+
581
+ return status.rstrip()
582
+
583
+
584
+ def is_status_check(arg: BashCommand) -> bool:
585
+ return (
586
+ isinstance(arg.action, StatusCheck)
587
+ or (
588
+ isinstance(arg.action, SendSpecials)
589
+ and arg.action.send_specials == ["Enter"]
590
+ )
591
+ or (isinstance(arg.action, SendAscii) and arg.action.send_ascii == [10])
592
+ )
593
+
594
+
595
+ def execute_bash(
596
+ bash_state: BashState,
597
+ enc: EncoderDecoder[int],
598
+ bash_arg: BashCommand,
599
+ max_tokens: Optional[int],
600
+ timeout_s: Optional[float],
601
+ ) -> tuple[str, float]:
602
+ try:
603
+ output, cost = _execute_bash(bash_state, enc, bash_arg, max_tokens, timeout_s)
604
+
605
+ # Remove echo if it's a command
606
+ if isinstance(bash_arg.action, Command):
607
+ command = bash_arg.action.command.strip()
608
+ if output.startswith(command):
609
+ output = output[len(command) :]
610
+
611
+ finally:
612
+ bash_state.run_bg_expect_thread()
613
+ return output, cost
614
+
615
+
616
+ def _execute_bash(
617
+ bash_state: BashState,
618
+ enc: EncoderDecoder[int],
619
+ bash_arg: BashCommand,
620
+ max_tokens: Optional[int],
621
+ timeout_s: Optional[float],
622
+ ) -> tuple[str, float]:
623
+ try:
624
+ is_interrupt = False
625
+ command_data = bash_arg.action
626
+
627
+ if isinstance(command_data, Command):
628
+ if bash_state.bash_command_mode.allowed_commands == "none":
629
+ return "Error: BashCommand not allowed in current mode", 0.0
630
+
631
+ bash_state.console.print(f"$ {command_data.command}")
632
+
633
+ if bash_state.state == "pending":
634
+ raise ValueError(WAITING_INPUT_MESSAGE)
635
+
636
+ command = command_data.command.strip()
637
+ if "\n" in command:
638
+ raise ValueError(
639
+ "Command should not contain newline character in middle. Run only one command at a time."
640
+ )
641
+
642
+ for i in range(0, len(command), 128):
643
+ bash_state.send(command[i : i + 128])
644
+ bash_state.send(bash_state.linesep)
645
+
646
+ elif isinstance(command_data, StatusCheck):
647
+ bash_state.console.print("Checking status")
648
+ if bash_state.state != "pending":
649
+ return "No running command to check status of", 0.0
650
+
651
+ elif isinstance(command_data, SendText):
652
+ if not command_data.send_text:
653
+ return "Failure: send_text cannot be empty", 0.0
654
+
655
+ bash_state.console.print(f"Interact text: {command_data.send_text}")
656
+ for i in range(0, len(command_data.send_text), 128):
657
+ bash_state.send(command_data.send_text[i : i + 128])
658
+ bash_state.send(bash_state.linesep)
659
+
660
+ elif isinstance(command_data, SendSpecials):
661
+ if not command_data.send_specials:
662
+ return "Failure: send_specials cannot be empty", 0.0
663
+
664
+ bash_state.console.print(
665
+ f"Sending special sequence: {command_data.send_specials}"
666
+ )
667
+ for char in command_data.send_specials:
668
+ if char == "Key-up":
669
+ bash_state.send("\033[A")
670
+ elif char == "Key-down":
671
+ bash_state.send("\033[B")
672
+ elif char == "Key-left":
673
+ bash_state.send("\033[D")
674
+ elif char == "Key-right":
675
+ bash_state.send("\033[C")
676
+ elif char == "Enter":
677
+ bash_state.send("\n")
678
+ elif char == "Ctrl-c":
679
+ bash_state.sendintr()
680
+ is_interrupt = True
681
+ elif char == "Ctrl-d":
682
+ bash_state.sendintr()
683
+ is_interrupt = True
684
+ elif char == "Ctrl-z":
685
+ bash_state.send("\x1a")
686
+ else:
687
+ raise Exception(f"Unknown special character: {char}")
688
+
689
+ elif isinstance(command_data, SendAscii):
690
+ if not command_data.send_ascii:
691
+ return "Failure: send_ascii cannot be empty", 0.0
692
+
693
+ bash_state.console.print(
694
+ f"Sending ASCII sequence: {command_data.send_ascii}"
695
+ )
696
+ for ascii_char in command_data.send_ascii:
697
+ bash_state.send(chr(ascii_char))
698
+ if ascii_char == 3:
699
+ is_interrupt = True
700
+ else:
701
+ raise ValueError(f"Unknown command type: {type(command_data)}")
702
+
703
+ except KeyboardInterrupt:
704
+ bash_state.sendintr()
705
+ bash_state.expect(bash_state.prompt)
706
+ return "---\n\nFailure: user interrupted the execution", 0.0
707
+
708
+ wait = min(timeout_s or CONFIG.timeout, CONFIG.timeout_while_output)
709
+ index = bash_state.expect([bash_state.prompt, pexpect.TIMEOUT], timeout=wait)
710
+ if index == 1:
711
+ text = bash_state.before or ""
712
+ incremental_text = _incremental_text(text, bash_state.pending_output)
713
+
714
+ second_wait_success = False
715
+ if is_status_check(bash_arg):
716
+ # There's some text in BashInteraction mode wait for TIMEOUT_WHILE_OUTPUT
717
+ remaining = CONFIG.timeout_while_output - wait
718
+ patience = CONFIG.output_wait_patience
719
+ if not incremental_text:
720
+ patience -= 1
721
+ itext = incremental_text
722
+ while remaining > 0 and patience > 0:
723
+ index = bash_state.expect(
724
+ [bash_state.prompt, pexpect.TIMEOUT], timeout=wait
725
+ )
726
+ if index == 0:
727
+ second_wait_success = True
728
+ break
729
+ else:
730
+ _itext = bash_state.before or ""
731
+ _itext = _incremental_text(_itext, bash_state.pending_output)
732
+ if _itext != itext:
733
+ patience = 3
734
+ else:
735
+ patience -= 1
736
+ itext = _itext
737
+
738
+ remaining = remaining - wait
739
+
740
+ if not second_wait_success:
741
+ text = bash_state.before or ""
742
+ incremental_text = _incremental_text(text, bash_state.pending_output)
743
+
744
+ if not second_wait_success:
745
+ bash_state.set_pending(text)
746
+
747
+ tokens = enc.encoder(incremental_text)
748
+
749
+ if max_tokens and len(tokens) >= max_tokens:
750
+ incremental_text = "(...truncated)\n" + enc.decoder(
751
+ tokens[-(max_tokens - 1) :]
752
+ )
753
+
754
+ if is_interrupt:
755
+ incremental_text = (
756
+ incremental_text
757
+ + """---
758
+ ----
759
+ Failure interrupting.
760
+ You may want to try Ctrl-c again or program specific exit interactive commands.
761
+ """
762
+ )
763
+
764
+ exit_status = get_status(bash_state)
765
+ incremental_text += exit_status
766
+
767
+ return incremental_text, 0
768
+
769
+ before = str(bash_state.before)
770
+
771
+ output = _incremental_text(before, bash_state.pending_output)
772
+ bash_state.set_repl()
773
+
774
+ tokens = enc.encoder(output)
775
+ if max_tokens and len(tokens) >= max_tokens:
776
+ output = "(...truncated)\n" + enc.decoder(tokens[-(max_tokens - 1) :])
777
+
778
+ try:
779
+ exit_status = get_status(bash_state)
780
+ output += exit_status
781
+ except ValueError:
782
+ bash_state.console.print(output)
783
+ bash_state.console.print(traceback.format_exc())
784
+ bash_state.console.print("Malformed output, restarting shell", style="red")
785
+ # Malformed output, restart shell
786
+ bash_state.reset_shell()
787
+ output = "(exit shell has restarted)"
788
+ return output, 0