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": "
|
|
7
|
-
"bytes":
|
|
8
|
-
"generatedAt": "2026-03-23T09:28
|
|
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
|
-
|
|
795
|
-
|
|
796
|
-
|
|
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
|
|
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(
|
|
803
|
+
readable, _, _ = select.select(fds_to_watch, [], [], 0.5)
|
|
803
804
|
except (select.error, InterruptedError, ValueError):
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
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
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
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
|
-
|
|
835
|
-
|
|
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
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
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.
|
|
859
|
-
|
|
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
|
-
|
|
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();
|