tabby-bianbu-mcp 0.8.1 → 0.8.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.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,14 @@
2
2
 
3
3
  All notable changes to `tabby-bianbu-mcp` will be documented in this file.
4
4
 
5
+ ## [0.8.2] - 2026-03-23
6
+
7
+ ### Fixed
8
+ - **PTY helper SIGCHLD race condition**: replaced `SIGCHLD` signal handler with non-blocking `waitpid(WNOHANG)` poll — `.bashrc` subprocesses (e.g. `dircolors`, `lesspipe`) were triggering SIGCHLD, setting `running=False`, and killing the PTY before bash was ready
9
+ - **PTY helper stdin EOF exit**: stdin EOF no longer terminates the helper; instead it stops watching stdin and lets master_fd drain until bash actually exits
10
+ - **PTY stderr visibility**: Python helper stderr is now forwarded to the terminal output as red diagnostic text instead of being silently discarded
11
+ - **select() timeout tuned**: changed from 50ms to 500ms to reduce CPU usage during idle PTY sessions
12
+
5
13
  ## [0.8.1] - 2026-03-23
6
14
 
7
15
  ### Fixed
@@ -3,7 +3,7 @@
3
3
  "sourceFile": "bianbu_agent_proxy.sh",
4
4
  "scriptVersion": "1.4.0",
5
5
  "serverVersion": "1.4.0",
6
- "sha256": "0d6a3d7f6b4701f0cce61e0401cf18a08795e90e0fc1023fd5034a20ef900102",
7
- "bytes": 85800,
8
- "generatedAt": "2026-03-23T09:28:13.423Z"
6
+ "sha256": "273cfef8c453138dffd0a66e9e35ade821314e02c5247167f23a798f4d8c645d",
7
+ "bytes": 87190,
8
+ "generatedAt": "2026-03-23T09:53:28.729Z"
9
9
  }
@@ -791,78 +791,94 @@ else:
791
791
  stdin_fd = sys.stdin.fileno()
792
792
  old_stdin_flags = fcntl.fcntl(stdin_fd, fcntl.F_GETFL)
793
793
  fcntl.fcntl(stdin_fd, fcntl.F_SETFL, old_stdin_flags | os.O_NONBLOCK)
794
- running = True
795
- def on_sigchld(signum, frame):
796
- nonlocal running
797
- running = False
798
- signal.signal(signal.SIGCHLD, on_sigchld)
794
+ # Ignore SIGCHLD to prevent select() interruption from .bashrc subprocesses etc.
795
+ signal.signal(signal.SIGCHLD, signal.SIG_DFL)
796
+ child_exited = False
799
797
  stdin_buf = b''
800
- while running:
798
+ while True:
799
+ fds_to_watch = [master_fd]
800
+ if not child_exited:
801
+ fds_to_watch.append(stdin_fd)
801
802
  try:
802
- readable, _, _ = select.select([master_fd, stdin_fd], [], [], 0.05)
803
+ readable, _, _ = select.select(fds_to_watch, [], [], 0.5)
803
804
  except (select.error, InterruptedError, ValueError):
804
- if not running:
805
- break
806
- continue
807
- if master_fd in readable:
808
- try:
809
- data = os.read(master_fd, 65536)
810
- if not data:
811
- break
812
- send_msg({'type': 'output', 'data': base64.b64encode(data).decode('ascii')})
813
- except OSError:
814
- break
815
- if stdin_fd in readable:
816
- try:
817
- chunk = os.read(stdin_fd, 65536)
818
- if not chunk:
805
+ pass
806
+ else:
807
+ if master_fd in readable:
808
+ try:
809
+ data = os.read(master_fd, 65536)
810
+ if data:
811
+ send_msg({'type': 'output', 'data': base64.b64encode(data).decode('ascii')})
812
+ else:
813
+ break
814
+ except OSError:
819
815
  break
820
- stdin_buf += chunk
821
- while b'\n' in stdin_buf:
822
- line, stdin_buf = stdin_buf.split(b'\n', 1)
823
- line = line.strip()
824
- if not line:
825
- continue
826
- try:
827
- cmd = json.loads(line.decode('utf-8'))
828
- if cmd['type'] == 'input':
829
- raw = base64.b64decode(cmd['data'])
830
- os.write(master_fd, raw)
831
- elif cmd['type'] == 'resize':
832
- set_winsize(master_fd, cmd['rows'], cmd['cols'])
816
+ if stdin_fd in readable:
817
+ try:
818
+ chunk = os.read(stdin_fd, 65536)
819
+ if chunk:
820
+ stdin_buf += chunk
821
+ while b'\n' in stdin_buf:
822
+ line, stdin_buf = stdin_buf.split(b'\n', 1)
823
+ line = line.strip()
824
+ if not line:
825
+ continue
833
826
  try:
834
- os.kill(pid, signal.SIGWINCH)
835
- except OSError:
827
+ cmd = json.loads(line.decode('utf-8'))
828
+ if cmd['type'] == 'input':
829
+ raw = base64.b64decode(cmd['data'])
830
+ os.write(master_fd, raw)
831
+ elif cmd['type'] == 'resize':
832
+ set_winsize(master_fd, cmd['rows'], cmd['cols'])
833
+ try:
834
+ os.kill(pid, signal.SIGWINCH)
835
+ except OSError:
836
+ pass
837
+ elif cmd['type'] == 'close':
838
+ child_exited = True
839
+ break
840
+ except (json.JSONDecodeError, KeyError, OSError):
836
841
  pass
837
- elif cmd['type'] == 'close':
838
- running = False
839
- break
840
- except (json.JSONDecodeError, KeyError, OSError):
841
- pass
842
- except OSError:
843
- break
844
- try:
845
- os.kill(pid, signal.SIGTERM)
846
- except OSError:
847
- pass
848
- exit_code = -1
849
- try:
850
- for _ in range(50):
851
- rpid, status = os.waitpid(pid, os.WNOHANG)
852
- if rpid != 0:
853
- exit_code = os.WEXITSTATUS(status) if os.WIFEXITED(status) else -1
854
- break
855
- import time; time.sleep(0.1)
856
- else:
842
+ except OSError:
843
+ pass
844
+ # Check if child process has exited (non-blocking)
845
+ if not child_exited:
857
846
  try:
858
- os.kill(pid, signal.SIGKILL)
859
- os.waitpid(pid, 0)
847
+ rpid, status = os.waitpid(pid, os.WNOHANG)
848
+ if rpid != 0:
849
+ child_exited = True
850
+ except ChildProcessError:
851
+ child_exited = True
852
+ # If child exited, drain remaining master_fd output then break
853
+ if child_exited:
854
+ import time
855
+ time.sleep(0.1)
856
+ try:
857
+ while True:
858
+ leftover = os.read(master_fd, 65536)
859
+ if not leftover:
860
+ break
861
+ send_msg({'type': 'output', 'data': base64.b64encode(leftover).decode('ascii')})
860
862
  except OSError:
861
863
  pass
864
+ break
865
+ exit_code = -1
866
+ if not child_exited:
867
+ try:
868
+ os.kill(pid, signal.SIGTERM)
869
+ except OSError:
870
+ pass
871
+ try:
872
+ rpid, status = os.waitpid(pid, os.WNOHANG if child_exited else 0)
873
+ if rpid != 0:
874
+ exit_code = os.WEXITSTATUS(status) if os.WIFEXITED(status) else -1
862
875
  except ChildProcessError:
863
876
  pass
864
877
  send_msg({'type': 'exit', 'code': exit_code})
865
- os.close(master_fd)
878
+ try:
879
+ os.close(master_fd)
880
+ except OSError:
881
+ pass
866
882
  `;
867
883
  }
868
884
 
@@ -934,7 +950,20 @@ function createPtySession(id, cwd, asRoot, cols, rows) {
934
950
  } catch {}
935
951
  }
936
952
  });
937
- child.stderr.on('data', () => {});
953
+ child.stderr.on('data', (chunk) => {
954
+ const msg = chunk.toString().trim();
955
+ if (msg) {
956
+ // Inject stderr as a synthetic output line so the client can see errors
957
+ const buf = Buffer.from(`\r\n\x1b[31m[pty-helper stderr] ${msg}\x1b[0m\r\n`);
958
+ session.outputBuffer.push(buf);
959
+ session.outputBufferSize += buf.length;
960
+ session.updatedAt = Date.now();
961
+ for (const waiter of session.waiters) {
962
+ waiter();
963
+ }
964
+ session.waiters = [];
965
+ }
966
+ });
938
967
  child.on('exit', () => {
939
968
  session.alive = false;
940
969
  session.updatedAt = Date.now();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tabby-bianbu-mcp",
3
- "version": "0.8.1",
3
+ "version": "0.8.2",
4
4
  "description": "Tabby plugin for Bianbu Cloud shell-like and file-manager-like tabs over MCP",
5
5
  "author": "niver2002",
6
6
  "license": "MIT",