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