Event-driven programming

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.

Tcl event types

Different types of Tcl events are as follows:

  • File/channel events—occur when a file or a channel (such as pipe, network connection, or serial port) can be read or written to
  • Timer events—occur after specified time, used for periodic activity
  • Idle events—these events are run as soon as there are no different types of events in the event queue
  • GUI events—used in applications that use Tk for their user interface, these events are generated when the user generates an action—such as clicking on a button

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.

Entering the event loop

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.

Events and stack frames

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

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.

Timer and idle events

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
}

Robust scheduling with tclcron

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
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.139.83.199