tcltk: don't discard piped stdin buffered before the stdin-channel swap
magic replaces Tcl's stdin channel with its own (TerminalInputProc) during
startup (_magic_startup). The replacement builds a brand-new Tcl channel over
the same fd; the new channel starts with an empty buffer. If anything had
already read() stdin bytes out of the kernel into the *old* channel's buffer
before the swap -- e.g. a script/commands piped in fast enough that Tcl buffered
them during startup -- those bytes were silently dropped, and every command
after that point vanished. This was timing-dependent, so it surfaced only
intermittently ("piped input discarded sometimes").
Fix: before creating the replacement channel, drain any bytes still buffered in
the old channel (Tcl_InputBuffered / Tcl_ReadChars) and hand them to
TerminalInputProc via TxBuffer, which it already serves ahead of any fresh
read(). The swap is now lossless by construction rather than by luck. On the
normal path Tcl_InputBuffered() is 0, so the new branch is inert.
How to reproduce / observe the original loss:
Temporarily force the old channel to buffer input just before the swap, right
after `fsOrig = Tcl_GetChannelInstanceData(oldchannel);` in _magic_startup():
{ Tcl_Obj *l = Tcl_NewObj(); Tcl_IncrRefCount(l);
Tcl_GetsObj(oldchannel, l); /* consume 1 line, buffering the rest */
fprintf(stderr, "buffered=%d\n", (int)Tcl_InputBuffered(oldchannel));
Tcl_DecrRefCount(l); }
then pipe a burst of commands (more than one line):
printf 'puts a\nputs b\n...\nquit -noprompt\n' | ./run_magicnull.sh
Comment the drain out and every command after the first disappears; restore it
and they all survive. Measured with 218 bytes buffered: 0/20 commands survive
without the drain, 19/20 with it (the one line the probe consumed aside).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
3f1747b1fb
commit
7d11b8e51b
|
|
@ -960,6 +960,52 @@ _magic_startup(ClientData clientData,
|
|||
oldchannel = Tcl_GetStdChannel(TCL_STDIN); // Get existing stdin
|
||||
fsOrig = Tcl_GetChannelInstanceData(oldchannel);
|
||||
|
||||
/* We are about to replace stdin with a brand-new channel over the */
|
||||
/* same file descriptor. The new channel starts with an empty */
|
||||
/* buffer, so any bytes the old channel had already read() out of the */
|
||||
/* kernel into *its* buffer would be silently dropped by the swap -- */
|
||||
/* e.g. commands piped in fast enough that Tcl buffered them during */
|
||||
/* startup. Drain them here and hand them to TerminalInputProc via */
|
||||
/* TxBuffer, which it serves ahead of any fresh read(), so no piped */
|
||||
/* input is ever lost regardless of startup timing. */
|
||||
{
|
||||
int nqueued = Tcl_InputBuffered(oldchannel);
|
||||
if (nqueued > 0)
|
||||
{
|
||||
Tcl_Obj *qobj = Tcl_NewObj();
|
||||
Tcl_IncrRefCount(qobj);
|
||||
if (Tcl_ReadChars(oldchannel, qobj, nqueued, 0) > 0)
|
||||
{
|
||||
#if TCL_MAJOR_VERSION < 9
|
||||
int qlen;
|
||||
#else
|
||||
Tcl_Size qlen;
|
||||
#endif
|
||||
char *qstr = Tcl_GetStringFromObj(qobj, &qlen);
|
||||
if (qlen > 0)
|
||||
{
|
||||
if (TxBuffer == NULL)
|
||||
{
|
||||
TxBuffer = Tcl_Alloc(qlen + 1);
|
||||
memcpy(TxBuffer, qstr, qlen);
|
||||
TxBuffer[qlen] = '\0';
|
||||
}
|
||||
else /* prepend the drained bytes ahead of TxBuffer */
|
||||
{
|
||||
size_t blen = strlen(TxBuffer);
|
||||
char *merged = Tcl_Alloc(qlen + blen + 1);
|
||||
memcpy(merged, qstr, qlen);
|
||||
memcpy(merged + qlen, TxBuffer, blen);
|
||||
merged[qlen + blen] = '\0';
|
||||
Tcl_Free(TxBuffer);
|
||||
TxBuffer = merged;
|
||||
}
|
||||
}
|
||||
}
|
||||
Tcl_DecrRefCount(qobj);
|
||||
}
|
||||
}
|
||||
|
||||
/* Copy the structure from the old to the new channel */
|
||||
stdChannel = (Tcl_ChannelType *)Tcl_GetChannelType(oldchannel);
|
||||
memcpy(&inChannel, stdChannel, sizeof(Tcl_ChannelType));
|
||||
|
|
|
|||
Loading…
Reference in New Issue