Tcl is designed to use an event-driven model. This means that everything that should happen is either scheduled for a particular time or when an event occurs, such as data being read, a new connection is made, or an external application or database sends a notification. This means that your files, networking, and timers work in the same thread and use events to perform actions when something occurs. For example, in an application with user interface, an action can be bound to user clicking on a button, which causes specific code to be run whenever the user clicks on the button. Similarly, a network server will set up a listening connection and run specific code when a new connection is established. Similarly, whenever application can read or write to a network connection, the particular code can be run.
It is common to develop an application that is event driven. This means that in most cases the application will only do things when an event takes place or will perform scheduled tasks. For example, a server will take actions whenever a new connection comes in and will clean up its information once a day.
Tcl fits this way of creating applications very well. It comes with a built-in event loop that is used by all Tcl components. In addition to this, a large number of packages available for Tcl support working in an event-driven way—for example, many networking clients and servers allow defining what should be done when a process is finished or when a new request comes in. Commands that are invoked when an event takes place are often called callbacks.
Different types of Tcl events are as follows:
In addition to events generated by Tcl itself, packages often provide higher level events. For example, network related packages handle channel related events to handle communication and invoke callbacks whenever it makes sense. A package to manage downloads over the network would invoke a callback whenever a download is completed or an error occurs.
In order for Tcl to process events, it needs to enter the event loop. When Tcl is started up, by default, it either sources the script file it was run with or it goes into interactive mode. In the majority of cases, this does not cause Tcl to enter the event loop. The best way to make sure that Tcl goes into the event loop once your application is initialized is to add the following command at the end of your script:
vwait forever
Normally, the command vwait
enters the event loop and waits for the variable specified, because its argument is modified—in this case, it means that Tcl will be processing events until the variable forever
is modified. However, vwait forever
is an idiom that many programmers add to the end of their script when they actually don't want to wait for a variable, but just want to enter the event loop.
In some cases when your script exits, Tcl will enter the event loop anyway, so it might not be required. Usually, this means Tk applications and running your applications from within TkCon. We can add the following check to see if Tk is loaded, and only enter event loop if it is not available:
if {[info commands tk] == ""} { vwait forever }
This checks if the tk
command exists, and only invokes vwait
if no Tk is available. If your application sources additional scripts or plugins, these should not enter the event loop themselves, because then actual loading of the script is blocked. It should only be invoked in the main script of your application.
All commands that are run as different types of events are always run at the global level—this is the same level that your application is loaded at. It also means that if an event initialization was done in a specific procedure or with various local variables, when the event is invoked, these variables will not be present. For example:
proc myFunction {} { set value 100 puts "Current value: $value" after 1000 {puts "Value: $value"} } myFunction vwait forever
This will schedule the puts "Value: $value"
command to be executed one second after myFunction
command is called. The after
command causes a command to be evaluated after a specified time in milliseconds and is described in more detail in the coming sections of this chapter. The code will fail because the variable value is not accessible at this time and the value
global variable was never defined. It is correct to use namespace variables and preferably a separate procedure for the actual event:
Namespace eval example {} proc example::eventfunction {} { variable value puts "Value: $value" } proc example::function {} { variable value set value 100 puts "Current value: $value" after 1000 example::eventfunction } example::function vwait forever
This will cause the texts Current value: 100
and Value: 100
to be written to standard output as namespace variables are used in this case.
Channel events are triggered whenever a channel can be read from or written to. Usually this is used for network connections, Unix sockets, and running processes using the open
command. It is also used with non-blocking channel in majority of cases.
Channel events allow us to set up certain commands to be run whenever there is something to read or whenever we can write to a channel so that our code does not block other events from being processed—for example, so that while we are waiting for more data from a remote peer, scheduled events can still run.
Setting up events is done using the fileevent
command. It accepts a channel as the first argument and an event type as the second argument—which can be either readable
or writable
, depending on which event we are interested in. If run with those two parameters, it returns the currently configured command to be run. We can also pass a new command that should be run whenever the specified event for this channel occurs. Usually this is combined with building commands as lists so that we can pass additional arguments such as the channel name to the command that will be run.
The following is an example of an echo server that sends each line back to the remote connection by using events:
proc echoHandler {chan} { if {[eof $chan]} { close $chan } else { gets $chan line puts $chan $line flush $chan } } proc echoAccept {chan remotehost remoteport} { # set buffering to line and non-blocking mode fconfigure $chan -buffering line -blocking 0 # initialize readable event fileevent $chan readable [list echohandler $chan] } socket -server echoAccept 12345
This example also initializes a TCP server on TCP port 12345. Networking is discussed in more detail in Chapter 6. However, for the purpose of this example, we need to know two things—socket -server
command sets up a listening connection and runs command, as yet another channel related event, to echoAccept
command, adding new channel, remote host and remote port information as additional parameters. Next, this command sets up an event that runs echoHandler
with the channel passed as an argument. It checks whether an end of file has been received, which happens when the remote peer closes the connection, and closes this channel if that happened. Otherwise, the command sends the same line that was read back to the remote peer.
Similarly the writable
event can be used to send large chunks of data to a channel—if we wanted to send more data than our channel buffer can send, Tcl would need to wait until this data is sent—in which case all other events would not be processed until this operation was completed. Using the writable
event, we can receive an event whenever we can write more data to a channel and send it in smaller chunks.
For both types of events, it is important to delete an event if we no longer want to handle this—for instance, if we won't read or write when an event occurs, Tcl will keep on invoking our event handler until we've read/written data. You can delete an event handler by invoking fileevent
with the channel, event type, and an empty string as the third parameter.
For example, the command to send large data to a channel by temporarily setting an event can be done in this way:
proc sendMoreData {chan data} { # write first 4096 bytes of data to channel puts nonewline $chan [string range $data 0 4095] flush $chan # trim data to contain remaining part of original data set data [string range $data 4096 end] if {[string length $data] > 0} { # if we have more data to write, set up next writable event fileevent $chan writable [list sendMoreData $chan $data] } else { # otherwise remove an event fileevent $chan writable "" } } proc sendData {chan data} { # set up initial event - we do not write now # since channel might not be accepting more data at the moment fileevent $chan writable [list sendMoreData $chan $data] }
The previous example uses the writable event to write data whenever it is possible.
In many cases we will want to send data from one channel to another—for example, sending file contents over a socket. Tcl offers a command that allows synchronous (blocking) or asynchronous (using event system) copying of data from one channel to another. This can be done using the fcopy
command. It accepts an input channel and an output channel as the first two arguments. If no other argument is specified, it copies data from the input channel to the output channel in blocking mode. Adding the -size
option after those arguments we can specify the number of bytes that should be copied, in which case either all of the file is copied or only the specified number of bytes is copied, whichever comes first.
If we want fcopy
to work in background, we need to specify a -command
option along with a command as parameter. In this case fcopy
will return immediately, set up sockets to work in non-blocking mode and start copying data. As soon as the copying is complete or an error occurs, the specified command is run, with one or two arguments appended. First argument specifies number of bytes copied. If no error occurred, only one argument is appended. If an error has occurred, the command is run with the error string added as the second argument.
Being able to perform certain tasks periodically is important, for example, periodically removing old entries from a database, sending daily reports. Tcl and its event loop offers a convenient way to run commands after specified time—it is possible to add a command to be run, remove it from the event queue, and query items currently in event queue. This functionality is accessible using after command
.
In order to schedule a command to be run after some time, append the time (in milliseconds from now) and the command to be run, for example:
after 2000 {puts "Welcome back"} puts "Hello" vwait forever
What will happen is first a Hello
text will be printed out, followed by a Welcome back message two seconds later.
Such invocation of after
returns a unique identifier for this event. This can be used to cancel a timer event from running by invoking after cancel
with the id. For example:
set id [after 2000 {puts "Welcome back"}] puts "Hello" after cancel $id vwait forever
What will happen is the Hello message will be printed out, but Welcome back will not be printed as it has been cancelled. after cancel
also accepts the command that was scheduled, and if it is found in the timer event queue, it is removed. For example:
proc myCommand {} { puts "My Command was invoked" } after 1000 myCommand after cancel myCommand
In this example, myCommand
will never be invoked—even though we did not store what the first after command returned, after cancel
has removed the actual myCommand
invocation from timer event queue.
If we want to add something to queue so that it is processed whenever the event loop is processing events, we can use the after idle
command. It accepts a command to be run, similar to scheduling something to be run after specified period of time. For example:
after idle myCommand
The after
command can also be used to wait for a specified amount of time without entering the event loop—it might be useful if our script needs to wait for something or a period of time, and we do not need to process other events during that time. In this case, the time in milliseconds needs to be added as the only argument to the command. For example, to wait for 5 seconds, we need to run this command:
after 5000
In many cases, what our application actually wants is to run a command at a specified time. In this case, we need to use the clock scan
and clock seconds
commands, and calculate after what time our command should be run. For example, to run a command at 8 p.m. we can do the following:
set seconds [clock scan "20:00"] set seconds [expr {$seconds—[clock seconds]}] if {$seconds < 0} { incr seconds 86400 } after [expr {$seconds * 1000}] myTaskCommand
In order to periodically run a command the following code should be inside the scheduled command so it will reschedule the next event when done. In many cases, such as old data cleanup, it is okay to run the command when an application starts and schedule our command to after the entire application is started by adding after 1000 ourCommand
. If the function should only be run at proper schedules, for example when doing daily reports, scheduling should be split from the actual task. For example:
proc myTaskCommand {} { puts "Task is being performed..." scheduleMyTaskCommand } proc scheduleMyTaskCommand {} { set seconds [clock scan "03:00"] set seconds [expr {$seconds [clock seconds]}] if {$seconds < 0} { incr seconds 86400 } after [expr {$seconds * 1000}] myTaskCommand }
The Tcl community offers a package that provides robust scheduling capability called tclcron
. It uses the after
and clock scan
commands internally. It allows us to register commands to be run in more intuitive ways.
The tclcron manual page can be found at http://dqsoftware.sourceforge.net/tclcron_man.html; download information can also be found on this page.
To add a command we want to run, we need to invoke the tclcron::schedule
command, passing a command to be run as the first argument, the type of schedule as the second parameter, and arguments after that. The two most basic types of schedules are once
and every
. The first one schedules a task only once and second one invokes it repeatedly. For example:
proc everyCommand {} { puts "Every 30 seconds" } proc onceCommand {} { puts "Once in 40 seconds" } tclcron::schedule everyCommand every 30 seconds tclcron::schedule onceCommand once 40 seconds
This will cause Every 30 seconds to be printed out after half a minute has passed. Once in 40 seconds will be printed out 10 seconds after that and then first text will be repeated every 30 seconds.
Both schedule types accept all valid clock scan input which makes them very powerful while being intuitive to read. For example:
tclcron::schedule databaseCleanup every 03:00 tclcron::schedule weeklyReport every sunday 04:00 tclcron::schedule cacheCleanup every 15 minutes
This will cause databaseCleanup
to be invoked every day at 3 A.M. and weeklyReport
to be invoked each Sunday at 4 A.M. The command cacheCleanup
will be invoked every 15 minutes.
Similar to after, tclcron::schedule
returns a unique identifier that can be used to remove a command from being invoked by running tclcron::unschedule
. This identifier can also be used to query the next planned invocations of a scheduled command by using the tclcron::schedules
command, returning Unix timestamp of each time the command would be run. It needs an identifier as the first argument. We can also specify the maximum number of timestamps we want to receive as the second argument. In addition to this, we can specify a timestamp after which we will no longer be interested in the schedule as a third argument.
For example, to find out how a particular schedule would be run and then remove it from the schedule, we can do the following:
set id [tclcron::schedule {} every 2 days] foreach time [tclcron::schedules $id 100 [clock scan "+3 months"]] { puts "Next planned run: [clock format $time]" } tclcron::unschedule $id
3.139.83.199