udev is the device manager of linux. It is event-based and there usually is a nice command that is able to output events on the stdout as these occur. The following package is a wrapper around this command, supporting handlers that will be called whenever matching events will occur. Note that an updated version (under version control) of this can now be found within a project on bitbucket
namespace eval ::udev {
variable UDEV
if { ![info exists UDEV]} {
array set UDEV {
idgene 0
methods_evt {get}
methods_udev {handler}
-monitor "udevadm monitor --property"
}
variable version 0.1
}
namespace export monitor handler get
namespace ensemble create
}
# ::udev::monitor -- Create monitoring context
#
# Creates a monitoring context and return an identifier for the
# monitoring. The identifier returned is also a command that
# can be used for further operations with this module.
#
# Arguments:
# args Dash-led options and values, only supported is -monitor
#
# Results:
# A context, that also is a command
#
# Side Effects:
# None.
proc ::udev::monitor { args } {
variable UDEV
set cx [namespace current]::udev:[incr UDEV(idGene)]
interp alias {} $cx {} ::udev::Dispatch $cx
upvar \#0 $cx CX
array set OPTS $args
foreach k [array names UDEV -*] {
set CX($k) $UDEV($k)
if { [info exists OPTS($k)] } {
set CX($k) $OPTS($k)
}
}
set CX(init) 0
set CX(handlers) {}
set CX(lines) {}
set CX(fd) ""
return $cx
}
# ::udev::handler -- Register a handler
#
# Register a handler to be called whenever an event matching the
# arguments occur. The arguments should be a list of key
# patterns and value patterns. This pattern matcher is applied
# onto the event and only events that match will triggered the
# command registered as a handler. The command will be called
# with an additional argument, the identifier of the event.
# This is an array, but also a command that can be used for
# further operations on the event. Event are shortlived, they
# are removed automatically as soon as all relevant handlers
# have been triggered.
#
# Arguments:
# cx Context, as returned by procedure abovea
# cmd Command to call on matching event.
# args Matcher description, empty will capture all events.
#
# Results:
# None.
#
# Side Effects:
# Will fork the -monitor command and arrange for reading,
# understanding and filtering its output.
proc ::udev::handler { cx cmd args } {
upvar \#0 $cx CX
lappend CX(handlers) $cmd $args
if { $CX(fd) eq "" } {
# Create command pipe
set monitor "|"
append monitor $CX(-monitor)
# Start the pipe and arrange to read all output
if { [catch {open $monitor} fd] == 0 } {
fconfigure $fd -buffering line -blocking off
set CX(fd) $fd
fileevent $CX(fd) readable [list [namespace current]::Read $cx]
}
}
}
# ::udev::get -- Get key from event
#
# Get the key from an event, this is a convenience procedure.
#
# Arguments:
# ev Identifier of the event
# key Key to get
#
# Results:
# Value of key or empty string if key does not exists.
#
# Side Effects:
# None.
proc ::udev::get { ev key } {
upvar \#0 $ev EVT
if { [info exists EVT($key)] } {
return $EVT($key)
}
return ""
}
# ::udev::Dispatch -- Command dispatcher
#
# Dispatch command for object-like API. The dispatcher is aware
# of both types of objects/commands that are used within this
# module.
#
# Arguments:
# obj Identifier of object
# method Method to dispatch
# args Arguments to method
#
# Results:
# Will call the relevant procedure
#
# Side Effects:
# Errors on unknown types or methods.
proc ::udev::Dispatch { obj method args } {
variable UDEV
# Guess type out of object identifier
foreach {type id} [split [namespace tail $obj] :] break
# Check that the method is allowed for that type.
if { [info exists UDEV(methods_$type)] } {
if {[lsearch -exact $UDEV(methods_$type) $method] < 0} {
return -code error \
"Bad method $method for a $type:\
must be one of [join $UDEV(methods_$type) ,]"
}
} else {
return -code error "$type isn't a recognised object type"
}
# Dispatch
if {[catch {eval [linsert $args 0 $method $obj]} msg]} {
return -code error $msg
}
return $msg
}
# ::udev::Trigger -- Trigger relevant handlers
#
# Trigger all handlers that match the event that has just been
# captured.
#
# Arguments:
# cx Monitoring context
# evt Identifier of event
#
# Results:
# Number of commands that were successfully triggered
#
# Side Effects:
# Call external handlers!
proc ::udev::Trigger { cx evt } {
upvar \#0 $cx CX
upvar \#0 $evt EVT
# Traverse all known handlers, looking for matching ones.
set triggers 0
foreach {cmd args} $CX(handlers) {
# Count down matches based on the number of field patterns.
set matches [expr {[llength $args]/2}]
if { $matches > 0 } {
# Match the key patterns against the value patterns. They
# all have to match for us to trigger.
foreach {k v} $args {
set found 0
foreach i [array names EVT] {
if { [string match $k $i] && [string match $v $EVT($i)] } {
set found 1
}
}
# We have a match (at least) account for it.
if { $found } {
incr matches -1
}
}
}
# If we've managed to match all criterias, trigger the handler
# registered with the identifier of the event as an additional
# argument.
if { $matches <= 0 } {
if { [catch {eval [linsert $cmd end $evt]}] == 0 } {
incr triggers
}
}
}
return $triggers
}
# ::udev::Read -- Read output from 'udevadm monitor'
#
# Read output from udevadm monitor and create events once they
# are isolated. Trigger relevant handlers for those events.
#
# Arguments:
# cx Monitoring context
#
# Results:
# None.
#
# Side Effects:
# None.
proc ::udev::Read { cx } {
variable UDEV
upvar \#0 $cx CX
set line [string trim [gets $CX(fd)]]
if { ! $CX(init) } {
# We are still reading the header, wait for the empty line and
# once found, remember that we are now done with the header
# and should continue further to reading events.
if { $line eq "" } {
set CX(init) 1
}
} else {
# Events are separated by one or more empty lines. We
# accumulate all lines that we've found and once we read an
# empty line, loop through the accumulated lines to form the
# event and trigger.
if { $line eq "" } {
if { [llength $CX(lines)] > 0 } {
# Create event
set evt [namespace current]::evt:[incr UDEV(idGene)]
interp alias {} $evt {} ::udev::Dispatch $evt
upvar \#0 $evt EVT
foreach l $CX(lines) {
foreach {k v} [split $l "="] break
set EVT([string trim $k]) [string trim $v]
}
set CX(lines) {}
# Trigger relevant commands and cleanup
Trigger $cx $evt
interp alias {} $evt {}
unset $evt
}
} else {
# Make sure we ignore the first line that gives a general
# summary of the event. There might be better and more
# flexible ways of doing this.
set ignore 0
foreach ptn [list {UDEV*\[[0-9]*} {KERNEL\[[0-9]*}] {
if { [string match $ptn $line] } {
set ignore 1
}
}
if { !$ignore } {
lappend CX(lines) $line
}
}
}
}
package provide udev $::udev::versionTo exercise it and show it capabilities, you can try the following script. As soon as you insert an USB key in a port, it will print out the device for the USB key, and where it is mounted (most modern linux automatically mount keys on insertion). It will also print a message when you remove the key.
package require udev
proc ::findme { cx id count } {
set dev ""
foreach f [glob -nocomplain -directory /dev/disk/by-id *${id}*part*] {
set dev [file normalize [file join /dev/disk/by-id [file readlink $f]]]
break
}
if { $dev eq "" } {
incr count -1
if { $count > 0 } {
after 1000 [list ::findme $cx $id $count]
}
} else {
puts "Inserted disk available at: $dev, waiting for removal"
set mounts [exec mount]
foreach l [split $mounts "\n"] {
if { [lindex $l 0] eq $dev } {
puts "USB disk is mounted on [lindex $l 2]"
}
}
$cx handler [list ::printme $cx] \
ACTION remove ID_SERIAL_SHORT * DEVNAME $dev
}
}
proc ::printme { cx e } {
switch [$e get ACTION] {
"add" {
after 1000 [list ::findme $cx [$e get ID_SERIAL] 10]
}
"remove" {
puts "[$e get DEVNAME] removed from USB port"
}
}
}
set cx [udev monitor]
$cx handler [list ::printme $cx] ACTION add ID_SERIAL_SHORT * SUBSYSTEM usb
vwait forever