Many hyperlinks are disabled.
Use anonymous login
to enable hyperlinks.
Overview
Comment: | Added a refcounting mechanism to ChannelBuffers. Other edits to stop segfaults in tests iocmd-21.2[12]. |
---|---|
Downloads: | Tarball | ZIP archive | SQL archive |
Timelines: | family | ancestors | descendants | both | core-8-5-branch |
Files: | files | file ages | folders |
SHA1: |
13886141d8b3acc777afc94fb9c24b9e |
User & Date: | dgp 2014-04-21 18:55:41 |
References
2022-04-20
| ||
12:03 | • New ticket [a12ad5c4bd] buffer allocation on every call to Write in tclIO.c. artifact: 9f3f644b4e user: pooryorick | |
Context
2014-04-22
| ||
14:57 | Add refcounting and preservation to [testchannel transform] to stop segfault in test iogt-2.4. check-in: 1f873e8005 user: dgp tags: core-8-5-branch | |
13:25 |
Memory leak after thread exit, fixed (alloc cache released by exit), belong to ticket [3493120]
M... check-in: c251d61424 user: sebres tags: bug-3493120 | |
2014-04-21
| ||
20:01 | merge 8.5 check-in: 621d5cbc6e user: dgp tags: dgp-read-bytes | |
19:04 | Merge refcounting machinery for ChannelBuffer. check-in: 0c1015d94d user: dgp tags: trunk | |
18:55 | Added a refcounting mechanism to ChannelBuffers. Other edits to stop segfaults in tests iocmd-21.2[... check-in: 13886141d8 user: dgp tags: core-8-5-branch | |
2014-04-17
| ||
19:58 | Another test exposing another segfault. check-in: dbd29ee73d user: dgp tags: core-8-5-branch | |
Changes
Changes to generic/tclIO.c.
︙ | ︙ | |||
158 159 160 161 162 163 164 165 166 167 168 169 170 171 | } CloseCallback; /* * Static functions in this file: */ static ChannelBuffer * AllocChannelBuffer(int length); static void ChannelTimerProc(ClientData clientData); static int CheckChannelErrors(ChannelState *statePtr, int direction); static int CheckForDeadChannel(Tcl_Interp *interp, ChannelState *statePtr); static void CheckForStdChannelsBeingClosed(Tcl_Channel chan); static void CleanupChannelHandlers(Tcl_Interp *interp, | > > | 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 | } CloseCallback; /* * Static functions in this file: */ static ChannelBuffer * AllocChannelBuffer(int length); static void PreserveChannelBuffer(ChannelBuffer *bufPtr); static void ReleaseChannelBuffer(ChannelBuffer *bufPtr); static void ChannelTimerProc(ClientData clientData); static int CheckChannelErrors(ChannelState *statePtr, int direction); static int CheckForDeadChannel(Tcl_Interp *interp, ChannelState *statePtr); static void CheckForStdChannelsBeingClosed(Tcl_Channel chan); static void CleanupChannelHandlers(Tcl_Interp *interp, |
︙ | ︙ | |||
348 349 350 351 352 353 354 355 356 357 358 359 360 361 | ChanRead( Channel *chanPtr, char *dst, int dstSize, int *errnoPtr) { if (WillRead(chanPtr) < 0) { return -1; } return chanPtr->typePtr->inputProc(chanPtr->instanceData, dst, dstSize, errnoPtr); } | > | 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 | ChanRead( Channel *chanPtr, char *dst, int dstSize, int *errnoPtr) { if (WillRead(chanPtr) < 0) { *errnoPtr = Tcl_GetErrno(); return -1; } return chanPtr->typePtr->inputProc(chanPtr->instanceData, dst, dstSize, errnoPtr); } |
︙ | ︙ | |||
2212 2213 2214 2215 2216 2217 2218 2219 2220 2221 2222 2223 2224 2225 2226 2227 | n = length + CHANNELBUFFER_HEADER_SIZE + BUFFER_PADDING + BUFFER_PADDING; bufPtr = (ChannelBuffer *) ckalloc((unsigned) n); bufPtr->nextAdded = BUFFER_PADDING; bufPtr->nextRemoved = BUFFER_PADDING; bufPtr->bufLength = length + BUFFER_PADDING; bufPtr->nextPtr = NULL; return bufPtr; } /* *---------------------------------------------------------------------- * * RecycleBuffer -- * * Helper function to recycle input and output buffers. Ensures that two | > > > > > > > > > > > > > > > > > > | 2215 2216 2217 2218 2219 2220 2221 2222 2223 2224 2225 2226 2227 2228 2229 2230 2231 2232 2233 2234 2235 2236 2237 2238 2239 2240 2241 2242 2243 2244 2245 2246 2247 2248 | n = length + CHANNELBUFFER_HEADER_SIZE + BUFFER_PADDING + BUFFER_PADDING; bufPtr = (ChannelBuffer *) ckalloc((unsigned) n); bufPtr->nextAdded = BUFFER_PADDING; bufPtr->nextRemoved = BUFFER_PADDING; bufPtr->bufLength = length + BUFFER_PADDING; bufPtr->nextPtr = NULL; bufPtr->refCount = 1; return bufPtr; } static void PreserveChannelBuffer( ChannelBuffer *bufPtr) { bufPtr->refCount++; } static void ReleaseChannelBuffer( ChannelBuffer *bufPtr) { if (--bufPtr->refCount) { return; } ckfree((char *) bufPtr); } /* *---------------------------------------------------------------------- * * RecycleBuffer -- * * Helper function to recycle input and output buffers. Ensures that two |
︙ | ︙ | |||
2246 2247 2248 2249 2250 2251 2252 | * always. */ { /* * Do we have to free the buffer to the OS? */ if (mustDiscard) { | | | | 2267 2268 2269 2270 2271 2272 2273 2274 2275 2276 2277 2278 2279 2280 2281 2282 2283 2284 2285 2286 2287 2288 2289 2290 2291 2292 | * always. */ { /* * Do we have to free the buffer to the OS? */ if (mustDiscard) { ReleaseChannelBuffer(bufPtr); return; } /* * Only save buffers which are at least as big as the requested buffersize * for the channel. This is to honor dynamic changes of the buffersize * made by the user. */ if ((bufPtr->bufLength - BUFFER_PADDING) < statePtr->bufSize) { ReleaseChannelBuffer(bufPtr); return; } /* * Only save buffers for the input queue if the channel is readable. */ |
︙ | ︙ | |||
2292 2293 2294 2295 2296 2297 2298 | } } /* * If we reached this code we return the buffer to the OS. */ | | | 2313 2314 2315 2316 2317 2318 2319 2320 2321 2322 2323 2324 2325 2326 2327 | } } /* * If we reached this code we return the buffer to the OS. */ ReleaseChannelBuffer(bufPtr); return; keepBuffer: bufPtr->nextRemoved = BUFFER_PADDING; bufPtr->nextAdded = BUFFER_PADDING; bufPtr->nextPtr = NULL; } |
︙ | ︙ | |||
2466 2467 2468 2469 2470 2471 2472 2473 2474 2475 2476 2477 2478 2479 | break; /* Out of the "while (1)". */ } /* * Produce the output on the channel. */ toWrite = BytesLeft(bufPtr); if (toWrite == 0) { written = 0; } else { written = (chanPtr->typePtr->outputProc)(chanPtr->instanceData, RemovePoint(bufPtr), toWrite, &errorCode); } | > | 2487 2488 2489 2490 2491 2492 2493 2494 2495 2496 2497 2498 2499 2500 2501 | break; /* Out of the "while (1)". */ } /* * Produce the output on the channel. */ PreserveChannelBuffer(bufPtr); toWrite = BytesLeft(bufPtr); if (toWrite == 0) { written = 0; } else { written = (chanPtr->typePtr->outputProc)(chanPtr->instanceData, RemovePoint(bufPtr), toWrite, &errorCode); } |
︙ | ︙ | |||
2593 2594 2595 2596 2597 2598 2599 2600 2601 2602 2603 2604 2605 2606 | if (IsBufferEmpty(bufPtr)) { statePtr->outQueueHead = bufPtr->nextPtr; if (statePtr->outQueueHead == NULL) { statePtr->outQueueTail = NULL; } RecycleBuffer(statePtr, bufPtr, 0); } } /* Closes "while (1)". */ /* * If we wrote some data while flushing in the background, we are done. * We can't finish the background flush until we run out of data and the * channel becomes writable again. This ensures that all of the pending * data has been flushed at the system level. | > | 2615 2616 2617 2618 2619 2620 2621 2622 2623 2624 2625 2626 2627 2628 2629 | if (IsBufferEmpty(bufPtr)) { statePtr->outQueueHead = bufPtr->nextPtr; if (statePtr->outQueueHead == NULL) { statePtr->outQueueTail = NULL; } RecycleBuffer(statePtr, bufPtr, 0); } ReleaseChannelBuffer(bufPtr); } /* Closes "while (1)". */ /* * If we wrote some data while flushing in the background, we are done. * We can't finish the background flush until we run out of data and the * channel becomes writable again. This ensures that all of the pending * data has been flushed at the system level. |
︙ | ︙ | |||
2677 2678 2679 2680 2681 2682 2683 | DiscardInputQueued(statePtr, 1); /* * Discard a leftover buffer in the current output buffer field. */ if (statePtr->curOutPtr != NULL) { | | | 2700 2701 2702 2703 2704 2705 2706 2707 2708 2709 2710 2711 2712 2713 2714 | DiscardInputQueued(statePtr, 1); /* * Discard a leftover buffer in the current output buffer field. */ if (statePtr->curOutPtr != NULL) { ReleaseChannelBuffer(statePtr->curOutPtr); statePtr->curOutPtr = NULL; } /* * The caller guarantees that there are no more buffers queued for output. */ |
︙ | ︙ | |||
3562 3563 3564 3565 3566 3567 3568 3569 3570 3571 3572 3573 3574 3575 | DiscardInputQueued(chanPtr->state, 0); ChanSeek(chanPtr, - inputBuffered, SEEK_CUR, &ignore); } } static int WillRead(Channel *chanPtr) { if ((chanPtr->typePtr->seekProc != NULL) && (Tcl_OutputBuffered((Tcl_Channel) chanPtr) > 0)) { if ((chanPtr->state->curOutPtr != NULL) && IsBufferReady(chanPtr->state->curOutPtr)) { SetFlag(chanPtr->state, BUFFER_READY); } if (FlushChannel(NULL, chanPtr, 0) != 0) { | > > > > > | 3585 3586 3587 3588 3589 3590 3591 3592 3593 3594 3595 3596 3597 3598 3599 3600 3601 3602 3603 | DiscardInputQueued(chanPtr->state, 0); ChanSeek(chanPtr, - inputBuffered, SEEK_CUR, &ignore); } } static int WillRead(Channel *chanPtr) { if (chanPtr->typePtr == NULL) { /* Prevent read attempts on a closed channel */ Tcl_SetErrno(EINVAL); return -1; } if ((chanPtr->typePtr->seekProc != NULL) && (Tcl_OutputBuffered((Tcl_Channel) chanPtr) > 0)) { if ((chanPtr->state->curOutPtr != NULL) && IsBufferReady(chanPtr->state->curOutPtr)) { SetFlag(chanPtr->state, BUFFER_READY); } if (FlushChannel(NULL, chanPtr, 0) != 0) { |
︙ | ︙ | |||
3648 3649 3650 3651 3652 3653 3654 3655 3656 3657 3658 3659 3660 3661 3662 3663 3664 3665 3666 3667 3668 3669 3670 3671 3672 3673 3674 | * that we need to stick at the beginning of this buffer. */ memcpy(InsertPoint(bufPtr), safe, (size_t) saved); bufPtr->nextAdded += saved; saved = 0; } dst = InsertPoint(bufPtr); dstLen = SpaceLeft(bufPtr); result = Tcl_UtfToExternal(NULL, encoding, src, srcLimit, statePtr->outputEncodingFlags, &statePtr->outputEncodingState, dst, dstLen + BUFFER_PADDING, &srcRead, &dstWrote, NULL); /* See chan-io-1.[89]. Tcl Bug 506297. */ statePtr->outputEncodingFlags &= ~TCL_ENCODING_START; if ((result != TCL_OK) && (srcRead + dstWrote == 0)) { /* We're reading from invalid/incomplete UTF-8 */ if (total == 0) { Tcl_SetErrno(EINVAL); return -1; } break; } | > > | 3676 3677 3678 3679 3680 3681 3682 3683 3684 3685 3686 3687 3688 3689 3690 3691 3692 3693 3694 3695 3696 3697 3698 3699 3700 3701 3702 3703 3704 | * that we need to stick at the beginning of this buffer. */ memcpy(InsertPoint(bufPtr), safe, (size_t) saved); bufPtr->nextAdded += saved; saved = 0; } PreserveChannelBuffer(bufPtr); dst = InsertPoint(bufPtr); dstLen = SpaceLeft(bufPtr); result = Tcl_UtfToExternal(NULL, encoding, src, srcLimit, statePtr->outputEncodingFlags, &statePtr->outputEncodingState, dst, dstLen + BUFFER_PADDING, &srcRead, &dstWrote, NULL); /* See chan-io-1.[89]. Tcl Bug 506297. */ statePtr->outputEncodingFlags &= ~TCL_ENCODING_START; if ((result != TCL_OK) && (srcRead + dstWrote == 0)) { /* We're reading from invalid/incomplete UTF-8 */ ReleaseChannelBuffer(bufPtr); if (total == 0) { Tcl_SetErrno(EINVAL); return -1; } break; } |
︙ | ︙ | |||
3744 3745 3746 3747 3748 3749 3750 3751 3752 3753 3754 3755 3756 3757 | return -1; } flushed += statePtr->bufSize; if (saved == 0 || src[-1] != '\n') { needNlFlush = 0; } } } if ((flushed < total) && (statePtr->flags & CHANNEL_UNBUFFERED || (needNlFlush && statePtr->flags & CHANNEL_LINEBUFFERED))) { SetFlag(statePtr, BUFFER_READY); if (FlushChannel(NULL, chanPtr, 0) != 0) { return -1; } | > | 3774 3775 3776 3777 3778 3779 3780 3781 3782 3783 3784 3785 3786 3787 3788 | return -1; } flushed += statePtr->bufSize; if (saved == 0 || src[-1] != '\n') { needNlFlush = 0; } } ReleaseChannelBuffer(bufPtr); } if ((flushed < total) && (statePtr->flags & CHANNEL_UNBUFFERED || (needNlFlush && statePtr->flags & CHANNEL_LINEBUFFERED))) { SetFlag(statePtr, BUFFER_READY); if (FlushChannel(NULL, chanPtr, 0) != 0) { return -1; } |
︙ | ︙ | |||
5931 5932 5933 5934 5935 5936 5937 | /* * If discardSavedBuffers is nonzero, must also discard any previously * saved buffer in the saveInBufPtr field. */ if (discardSavedBuffers && statePtr->saveInBufPtr != NULL) { | | | 5962 5963 5964 5965 5966 5967 5968 5969 5970 5971 5972 5973 5974 5975 5976 | /* * If discardSavedBuffers is nonzero, must also discard any previously * saved buffer in the saveInBufPtr field. */ if (discardSavedBuffers && statePtr->saveInBufPtr != NULL) { ReleaseChannelBuffer(statePtr->saveInBufPtr); statePtr->saveInBufPtr = NULL; } } /* *--------------------------------------------------------------------------- * |
︙ | ︙ | |||
6024 6025 6026 6027 6028 6029 6030 | * Check the actual buffersize against the requested buffersize. * Buffers which are smaller than requested are squashed. This is done * to honor dynamic changes of the buffersize made by the user. */ if ((bufPtr != NULL) && (bufPtr->bufLength - BUFFER_PADDING < statePtr->bufSize)) { | | | 6055 6056 6057 6058 6059 6060 6061 6062 6063 6064 6065 6066 6067 6068 6069 | * Check the actual buffersize against the requested buffersize. * Buffers which are smaller than requested are squashed. This is done * to honor dynamic changes of the buffersize made by the user. */ if ((bufPtr != NULL) && (bufPtr->bufLength - BUFFER_PADDING < statePtr->bufSize)) { ReleaseChannelBuffer(bufPtr); bufPtr = NULL; } if (bufPtr == NULL) { bufPtr = AllocChannelBuffer(statePtr->bufSize); } bufPtr->nextPtr = NULL; |
︙ | ︙ | |||
6086 6087 6088 6089 6090 6091 6092 6093 6094 6095 6096 6097 6098 6099 6100 6101 6102 6103 6104 6105 6106 | */ nread = -1; result = EWOULDBLOCK; } else { #endif /* TCL_IO_TRACK_OS_FOR_DRIVER_WITH_BAD_BLOCKING */ nread = ChanRead(chanPtr, InsertPoint(bufPtr), toRead, &result); #ifdef TCL_IO_TRACK_OS_FOR_DRIVER_WITH_BAD_BLOCKING } #endif /* TCL_IO_TRACK_OS_FOR_DRIVER_WITH_BAD_BLOCKING */ if (nread > 0) { bufPtr->nextAdded += nread; /* * If we get a short read, signal up that we may be BLOCKED. We should * avoid calling the driver because on some platforms we will block in * the low level reading code even though the channel is set into * nonblocking mode. | > > | 6117 6118 6119 6120 6121 6122 6123 6124 6125 6126 6127 6128 6129 6130 6131 6132 6133 6134 6135 6136 6137 6138 6139 | */ nread = -1; result = EWOULDBLOCK; } else { #endif /* TCL_IO_TRACK_OS_FOR_DRIVER_WITH_BAD_BLOCKING */ PreserveChannelBuffer(bufPtr); nread = ChanRead(chanPtr, InsertPoint(bufPtr), toRead, &result); #ifdef TCL_IO_TRACK_OS_FOR_DRIVER_WITH_BAD_BLOCKING } #endif /* TCL_IO_TRACK_OS_FOR_DRIVER_WITH_BAD_BLOCKING */ if (nread > 0) { result = 0; bufPtr->nextAdded += nread; /* * If we get a short read, signal up that we may be BLOCKED. We should * avoid calling the driver because on some platforms we will block in * the low level reading code even though the channel is set into * nonblocking mode. |
︙ | ︙ | |||
6118 6119 6120 6121 6122 6123 6124 6125 6126 6127 6128 6129 6130 6131 6132 | */ ResetFlag(statePtr, CHANNEL_HAS_MORE_DATA); } #endif /* TCL_IO_TRACK_OS_FOR_DRIVER_WITH_BAD_BLOCKING */ } else if (nread == 0) { SetFlag(statePtr, CHANNEL_EOF); statePtr->inputEncodingFlags |= TCL_ENCODING_END; } else if (nread < 0) { if ((result == EWOULDBLOCK) || (result == EAGAIN)) { SetFlag(statePtr, CHANNEL_BLOCKED); result = EAGAIN; } Tcl_SetErrno(result); | > < > | | 6151 6152 6153 6154 6155 6156 6157 6158 6159 6160 6161 6162 6163 6164 6165 6166 6167 6168 6169 6170 6171 6172 6173 6174 6175 6176 | */ ResetFlag(statePtr, CHANNEL_HAS_MORE_DATA); } #endif /* TCL_IO_TRACK_OS_FOR_DRIVER_WITH_BAD_BLOCKING */ } else if (nread == 0) { result = 0; SetFlag(statePtr, CHANNEL_EOF); statePtr->inputEncodingFlags |= TCL_ENCODING_END; } else if (nread < 0) { if ((result == EWOULDBLOCK) || (result == EAGAIN)) { SetFlag(statePtr, CHANNEL_BLOCKED); result = EAGAIN; } Tcl_SetErrno(result); } ReleaseChannelBuffer(bufPtr); return result; } /* *---------------------------------------------------------------------- * * Tcl_Seek -- * |
︙ | ︙ |
Changes to generic/tclIO.h.
︙ | ︙ | |||
32 33 34 35 36 37 38 39 40 41 42 43 44 45 | /* * struct ChannelBuffer: * * Buffers data being sent to or from a channel. */ typedef struct ChannelBuffer { int nextAdded; /* The next position into which a character * will be put in the buffer. */ int nextRemoved; /* Position of next byte to be removed from * the buffer. */ int bufLength; /* How big is the buffer? */ struct ChannelBuffer *nextPtr; /* Next buffer in chain. */ | > | 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 | /* * struct ChannelBuffer: * * Buffers data being sent to or from a channel. */ typedef struct ChannelBuffer { int refCount; /* Current uses count */ int nextAdded; /* The next position into which a character * will be put in the buffer. */ int nextRemoved; /* Position of next byte to be removed from * the buffer. */ int bufLength; /* How big is the buffer? */ struct ChannelBuffer *nextPtr; /* Next buffer in chain. */ |
︙ | ︙ |
Changes to generic/tclIOCmd.c.
︙ | ︙ | |||
443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 | Tcl_SetErrorCode(interp, "TCL", "VALUE", "NUMBER", NULL); return TCL_ERROR; } } resultPtr = Tcl_NewObj(); Tcl_IncrRefCount(resultPtr); charactersRead = Tcl_ReadChars(chan, resultPtr, toRead, 0); if (charactersRead < 0) { /* * TIP #219. * Capture error messages put by the driver into the bypass area and * put them into the regular interpreter result. Fall back to the * regular message if nothing was found in the bypass. */ if (!TclChanCaughtErrorBypass(interp, chan)) { Tcl_ResetResult(interp); Tcl_AppendResult(interp, "error reading \"", TclGetString(chanObjPtr), "\": ", Tcl_PosixError(interp), NULL); } Tcl_DecrRefCount(resultPtr); return TCL_ERROR; } /* * If requested, remove the last newline in the channel if at EOF. */ if ((charactersRead > 0) && (newline != 0)) { char *result; int length; result = TclGetStringFromObj(resultPtr, &length); if (result[length - 1] == '\n') { Tcl_SetObjLength(resultPtr, length - 1); } } Tcl_SetObjResult(interp, resultPtr); Tcl_DecrRefCount(resultPtr); return TCL_OK; } /* *---------------------------------------------------------------------- * | > > > | 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 | Tcl_SetErrorCode(interp, "TCL", "VALUE", "NUMBER", NULL); return TCL_ERROR; } } resultPtr = Tcl_NewObj(); Tcl_IncrRefCount(resultPtr); Tcl_Preserve(chan); charactersRead = Tcl_ReadChars(chan, resultPtr, toRead, 0); if (charactersRead < 0) { /* * TIP #219. * Capture error messages put by the driver into the bypass area and * put them into the regular interpreter result. Fall back to the * regular message if nothing was found in the bypass. */ if (!TclChanCaughtErrorBypass(interp, chan)) { Tcl_ResetResult(interp); Tcl_AppendResult(interp, "error reading \"", TclGetString(chanObjPtr), "\": ", Tcl_PosixError(interp), NULL); } Tcl_Release(chan); Tcl_DecrRefCount(resultPtr); return TCL_ERROR; } /* * If requested, remove the last newline in the channel if at EOF. */ if ((charactersRead > 0) && (newline != 0)) { char *result; int length; result = TclGetStringFromObj(resultPtr, &length); if (result[length - 1] == '\n') { Tcl_SetObjLength(resultPtr, length - 1); } } Tcl_SetObjResult(interp, resultPtr); Tcl_Release(chan); Tcl_DecrRefCount(resultPtr); return TCL_OK; } /* *---------------------------------------------------------------------- * |
︙ | ︙ |
Changes to tests/ioCmd.test.
︙ | ︙ | |||
781 782 783 784 785 786 787 | } finalize {} watch {} read { close $chan return a } } set ch [chan create read foo] } -body { | | | > > > > > > > > > > > > > > > > | 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 | } finalize {} watch {} read { close $chan return a } } set ch [chan create read foo] } -body { read $ch 0 } -cleanup { close $ch rename foo {} } -result {} test iocmd-21.22 {[close] in [read] segfaults} -setup { proc foo {method chan args} { switch -- $method initialize { return {initialize finalize watch read} } finalize {} watch {} read { catch {close $chan} return a } } set ch [chan create read foo] } -body { read $ch 1 } -returnCodes error -cleanup { catch {close $ch} rename foo {} } -match glob -result {*invalid argument*} # --- --- --- --------- --------- --------- # Helper commands to record the arguments to handler methods. # Stored in a script so that the threads and interpreters needing this # code do not need their own copy but can access this variable. |
︙ | ︙ |