source: trunk/gui/scripts/visviewer.tcl @ 1247

Last change on this file since 1247 was 1247, checked in by gah, 16 years ago

Fix for interrupted nanovis session

File size: 16.0 KB
Line 
1#!/usr/bin/wish
2# ----------------------------------------------------------------------
3
4
5#  VisViewer -
6#
7#  This class is the base class for the various visualization viewers
8#  that use the nanoserver render farm.
9#
10# ======================================================================
11#  AUTHOR:  Michael McLennan, Purdue University
12#  Copyright (c) 2004-2005  Purdue Research Foundation
13#
14#  See the file "license.terms" for information on usage and
15#  redistribution of this file, and for a DISCLAIMER OF ALL WARRANTIES.
16# ======================================================================
17
18itcl::class ::Rappture::VisViewer {
19    inherit itk::Widget
20
21    itk_option define -sendcommand sendCommand SendCommand ""
22    itk_option define -receivecommand receiveCommand ReceiveCommand ""
23
24    private common servers_         ;# array of visualization server lists
25    set servers_(nanovis) ""
26    set servers_(pymol)   ""
27
28    private variable sid_ ""        ;# socket connection to server
29    private common done_            ;# Used to indicate status of send.
30    private variable buffer_        ;# buffer for incoming/outgoing commands
31
32    # Number of milliseconds to wait before idle timeout.
33    # If greater than 0, automatically disconnect from the visualization
34    # server when idle timeout is reached.
35    private variable idleTimeout_ 43200000; # 12 hours
36    #private variable idleTimeout_ 5000;    # 5 seconds
37    #private variable idleTimeout_ 0;       # No timeout
38
39    protected variable _dispatcher ""   ;# dispatcher for !events
40    protected variable _hosts ""    ;# list of hosts for server
41    protected variable _parser ""   ;# interpreter for incoming commands
42    protected variable _image
43
44    constructor { hostlist args } {
45        # defined below
46    }
47    destructor {
48        # defined below
49    }
50    # Used internally only.
51    private method _Shuffle { hostlist }
52    private method _ReceiveHelper {}
53    private method _ServerDown {}
54    private method _SendHelper {}
55    private method _SendHelper.old {}
56    private method _CheckConnection {}
57
58    protected method SendEcho { channel {data ""} }
59    protected method ReceiveEcho { channel {data ""} }
60    protected method Connect { hostlist }
61    protected method Disconnect {}
62    protected method IsConnected {}
63    protected method SendBytes { bytes }
64    protected method ReceiveBytes { nbytes }
65    protected method Flush {}
66    protected method Color2RGB { color }
67    protected method Euler2XYZ { theta phi psi }
68
69    private proc _CheckNameList { namelist }  {
70        set pattern {^[a-zA-Z0-9\.]+:[0-9]+(,[a-zA-Z0-9\.]+:[0-9]+)*$}
71        if { ![regexp $pattern $namelist match] } {
72            error "bad visualization server address \"$namelist\": should be host:port,host:port,..."
73        }
74    }
75    public proc GetServerList { tag } {
76        return $servers_($tag)
77    }
78    public proc SetServerList { tag namelist } {
79        _CheckNameList $namelist
80        set servers_($tag) $namelist
81    }
82    public proc SetPymolServerList { namelist } {
83        SetServerList "pymol" $namelist
84    }
85    public proc SetNanovisServerList { namelist } {
86        SetServerList "nanovis" $namelist
87    }
88}
89
90# ----------------------------------------------------------------------
91# CONSTRUCTOR
92# ----------------------------------------------------------------------
93itcl::body Rappture::VisViewer::constructor { hostlist args } {
94
95    Rappture::dispatcher _dispatcher
96    $_dispatcher register !serverDown
97    $_dispatcher dispatch $this !serverDown "[itcl::code $this _ServerDown]; list"
98    $_dispatcher register !timeout
99    $_dispatcher dispatch $this !timeout "[itcl::code $this Disconnect]; list"
100
101    _CheckNameList $hostlist
102    set _hostlist $hostlist
103    set buffer_(in) ""
104    set buffer_(out) ""
105    #
106    # Create a parser to handle incoming requests
107    #
108    set _parser [interp create -safe]
109    foreach cmd [$_parser eval {info commands}] {
110        $_parser hide $cmd
111    }
112
113    #
114    # Set up the widgets in the main body
115    #
116    option add hull.width hull.height
117    pack propagate $itk_component(hull) no
118
119    itk_component add controls {
120        frame $itk_interior.cntls
121    } {
122        usual
123        rename -background -controlbackground controlBackground Background
124    }
125    pack $itk_component(controls) -side right -fill y
126
127    #
128    # RENDERING AREA
129    #
130    itk_component add area {
131        frame $itk_interior.area
132    }
133    pack $itk_component(area) -expand yes -fill both
134
135    set _image(plot) [image create photo]
136    itk_component add 3dview {
137        label $itk_component(area).vol -image $_image(plot) \
138            -highlightthickness 0 -width 1 -height 1
139    } {
140        usual
141        ignore -highlightthickness
142    }
143    pack $itk_component(3dview) -expand yes -fill both
144
145    eval itk_initialize $args
146}
147
148#
149# destructor --
150#
151itcl::body Rappture::VisViewer::destructor {} {
152    $_dispatcher cancel !timeout
153    interp delete $_parser
154    array unset done_ $this
155}
156
157#
158# _Shuffle --
159#
160#   Shuffle the list of server hosts.
161#
162itcl::body Rappture::VisViewer::_Shuffle { hostlist } {
163    set hosts [split $hostlist ,]
164    set random_hosts {}
165    set ticks [clock clicks]
166    expr {srand($ticks)}
167    for { set i [llength $hosts] } { $i > 0 } { incr i -1 } {
168        set index [expr {round(rand()*$i - 0.5)}]
169        if { $index == $i } {
170            set index [expr $i - 1]
171        }
172        lappend random_hosts [lindex $hosts $index]
173        set hosts [lreplace $hosts $index $index]
174    }
175    return $random_hosts
176}
177
178#
179# _ServerDown --
180#
181#    Used internally to let the user know when the connection to the
182#    visualization server has been lost.  Puts up a tip encouraging the
183#    user to press any control to reconnect.
184#
185itcl::body Rappture::VisViewer::_ServerDown {} {
186    if { [info exists itk_component(area)] } {
187        set x [expr {[winfo rootx $itk_component(area)]+10}]
188        set y [expr {[winfo rooty $itk_component(area)]+10}]
189    } else {
190        set x 0; set y 0
191    }
192    Rappture::Tooltip::cue @$x,$y "Lost connection to visualization server.  This happens sometimes when there are too many users and the system runs out of memory.\n\nTo reconnect, reset the view or press any other control.  Your picture should come right back up."
193}
194
195#
196# Connect --
197#
198#    Connect to the visualization server (e.g. nanovis, pymolproxy). 
199#    Creates an event callback that is triggered when we are idle
200#    (no I/O with the server) for some specified time. Sends the server
201#    some estimate of the size of our job [soon to be deprecated]. 
202#    If it's too busy, that server may forward us to another [this
203#    was been turned off in nanoscale].
204#
205itcl::body Rappture::VisViewer::Connect { hostlist } {
206    blt::busy hold $itk_component(hull) -cursor watch
207    update
208
209    # Shuffle the list of servers so as to pick random
210    set servers [_Shuffle $hostlist]
211
212    set memorySize 10000
213    # Get the first server
214    foreach {hostname port} [split [lindex $servers 0] :] break
215    set servers [lrange $servers 1 end]
216
217    while {1} {
218        SendEcho <<line "connecting to $hostname:$port..."
219        if { [catch {socket $hostname $port} sid_] != 0 } {
220            if {[llength $servers] == 0} {
221                blt::busy release $itk_component(hull)
222                return 0
223            }
224            # Get the next server
225            foreach {hostname port} [split [lindex $servers 0] :] break
226            set servers [lrange $servers 1 end]
227            continue
228        }
229        fconfigure $sid_ -translation binary -encoding binary
230
231        # Send memory requirement to the load balancer
232        puts -nonewline $sid_ [binary format I $memorySize]
233        flush $sid_
234
235        # Read back a reconnection order
236        set data [read $sid_ 4]
237        if {[binary scan $data cccc b1 b2 b3 b4] != 4} {
238            blt::busy release $itk_component(hull)
239            error "couldn't read redirection request"
240        }
241        set addr [format "%u.%u.%u.%u" \
242            [expr {$b1 & 0xff}] \
243            [expr {$b2 & 0xff}] \
244            [expr {$b3 & 0xff}] \
245            [expr {$b4 & 0xff}]]
246
247        if { [string equal $addr "0.0.0.0"] } {
248            # We're connected. Cancel any pending serverDown events and
249            # release the busy window over the hull.
250            $_dispatcher cancel !serverDown
251            if { $idleTimeout_ > 0 } {
252                $_dispatcher event -after $idleTimeout_ !timeout
253            }
254            blt::busy release $itk_component(hull)
255            fconfigure $sid_ -buffering line
256            fileevent $sid_ readable [itcl::code $this _ReceiveHelper]
257            return 1
258        }
259        set hostname $addr
260    }
261    #NOTREACHED
262    blt::busy release $itk_component(hull)
263    return 0
264}
265
266
267#
268# Disconnect --
269#
270#    Clients use this method to disconnect from the current rendering
271#    server.  Cancel any pending idle timeout events.
272#
273itcl::body Rappture::VisViewer::Disconnect {} {
274    $_dispatcher cancel !timeout
275    if { [IsConnected] } {
276        catch {close $sid_}
277        set sid_ ""
278    }
279    set buffer_(in) ""
280}
281
282#
283# IsConnected --
284#
285#    Indicates if we are currently connected to a server.
286#
287itcl::body Rappture::VisViewer::IsConnected {} {
288    return [expr {"" != $sid_}]
289}
290
291#
292# CheckConection --
293#
294#   This routine is called whenever we're about to send/recieve data on
295#   the socket connection to the visualization server.  If we're connected,
296#   then reset the timeout event.  Otherwise try to reconnect to the
297#   visualization server.
298#
299itcl::body Rappture::VisViewer::_CheckConnection {} {
300    if { [IsConnected] } {
301        if { [eof $sid_] } {
302            error "unexpected eof on socket"
303        }
304        $_dispatcher cancel !timeout
305        if { $idleTimeout_ > 0 } {
306            $_dispatcher event -after $idleTimeout_ !timeout
307        }
308        return 1
309    }
310    # If we aren't connected, assume it's because the connection to the
311    # visualization server broke. Try to open a connection and trigger a
312    # rebuild.
313    $_dispatcher cancel !serverDown
314    set x [expr {[winfo rootx $itk_component(area)]+10}]
315    set y [expr {[winfo rooty $itk_component(area)]+10}]
316    Rappture::Tooltip::cue @$x,$y "Connecting..."
317    set code [catch { Connect } ok]
318    if { $code == 0 && $ok} {
319        $_dispatcher event -idle !rebuild
320        Rappture::Tooltip::cue hide
321        return 1
322    } else {
323        Rappture::Tooltip::cue @$x,$y "Can't connect to visualization server.  This may be a network problem.  Wait a few moments and try resetting the view."
324        return 0
325    }
326}
327
328#
329# Flush --
330#
331#    Flushes the socket.
332#
333itcl::body Rappture::VisViewer::Flush {} {
334    if { [_CheckConnection] } {
335        flush $sid_
336    }
337}
338
339
340#
341# _SendHelper --
342#
343#   Helper routine called from a file event to send data when the
344#   connection is writable (i.e. not blocked).  Sets a magic
345#   variable done_($this) when we're done.
346#
347itcl::body Rappture::VisViewer::_SendHelper {} {
348    if { ![_CheckConnection] } {
349        return 0
350    }
351    puts $sid_ $buffer_(out)
352    flush $sid_
353    set done_($this) 1;     # Success
354}
355
356#
357# _SendHelper.old --
358#
359#   Helper routine called from a file event to send data when the
360#   connection is writable (i.e. not blocked).  Sends data in chunks
361#   of 8k (or less).  Sets magic variable done_($this) to indicate
362#   that we're either finished (success) or could not send bytes to
363#   the server (failure).
364#
365itcl::body Rappture::VisViewer::_SendHelper.old {} {
366    if { ![_CheckConnection] } {
367        return 0
368    }
369    set bytesLeft [string length $buffer_(out)]
370    if { $bytesLeft > 0} {
371        set chunk [string range $buffer_(out) 0 8095]
372        set buffer_(out)  [string range $buffer_(out) 8096 end]
373        incr bytesLeft -8096
374        set code [catch {
375            if { $bytesLeft > 0 } {
376                puts -nonewline $sid_ $chunk
377            } else {
378                puts $sid_ $chunk
379            }
380        } err]
381        if { $code != 0 } {
382            puts stderr "error sending data to $sid_: $err"
383            Disconnect
384            set done_($this) 0;     # Failure
385        }
386    } else {
387        set done_($this) 1;     # Success
388    }
389}
390
391#
392# SendBytes --
393#
394#   Send a a string to the visualization server.
395#
396itcl::body Rappture::VisViewer::SendBytes { bytes } {
397    SendEcho >>line $bytes
398
399    if { ![_CheckConnection] } {
400        return 0
401    }
402    # Even though the data is sent in only 1 "puts", we need to verify that
403    # the server is ready first.  Wait for the socket to become writable
404    # before sending anything.
405    set done_($this) 1
406    set buffer_(out) $bytes
407    fileevent $sid_ writable [itcl::code $this _SendHelper]
408    tkwait variable ::Rappture::VisViewer::done_($this)
409    fileevent $sid_ writable ""
410    flush $sid_
411    set buffer_(out) ""
412    return $done_($this)
413}
414
415#
416# ReceiveBytes --
417#
418#    Read some number of bytes from the visualization server.
419#
420itcl::body Rappture::VisViewer::ReceiveBytes { size } {
421    if { ![_CheckConnection] } {
422        return 0
423    }
424    set bytes [read $sid_ $size]
425    ReceiveEcho <<line "<read $size bytes"
426    return $bytes
427}
428
429#
430# _ReceiveHelper --
431#
432#   Helper routine called from a file event when the connection is
433#   readable (i.e. a command response has been sent by the rendering
434#   server.  Reads the incoming command and executes it in a safe
435#   interpreter to handle the action.
436#
437#       Note: This routine currently only handles command responses from
438#         the visualization server.  It doesn't handle non-blocking
439#         reads from the visualization server.
440#
441#       nv>image -bytes 100000      yes
442#       ...following 100000 bytes...    no
443#
444#   Note: All commands from the render server are on one line.
445#         This is because the render server can send anything
446#         as an error message (restricted again to one line).
447#
448itcl::body Rappture::VisViewer::_ReceiveHelper {} {
449    if { ![_CheckConnection] } {
450        return 0
451    }
452    if { [gets $sid_ line] < 0 } {
453        Disconnect
454    }
455    set line [string trim $line]
456    if { $line == "" } {
457        return
458    }
459    if { [string compare -length 3 $line "nv>"] == 0 } {
460        ReceiveEcho <<line $line
461        append buffer_(in) [string range $line 3 end]
462        append buffer_(in) "\n"
463        if {[info complete $buffer_(in)]} {
464            set request $buffer_(in)
465            set buffer_(in) ""
466            $_parser eval $request
467        }
468    } elseif { [string compare -length 20 $line "NanoVis Server Error:"] == 0} {
469        # this shows errors coming back from the engine
470        ReceiveEcho <<error $line
471        puts stderr "Render Server Error: $line\n"
472    } else {
473        # this shows errors coming back from the engine
474        ReceiveEcho <<error $line
475        puts stderr "Garbled message: $line\n"
476    }
477}
478
479
480#
481# Color2RGB --
482#
483#   Converts a color name to a list of r,g,b values needed for the
484#   engine.  Each r/g/b component is scaled in the # range 0-1.
485#
486itcl::body Rappture::VisViewer::Color2RGB {color} {
487    foreach {r g b} [winfo rgb $itk_component(hull) $color] break
488    set r [expr {$r/65535.0}]
489    set g [expr {$g/65535.0}]
490    set b [expr {$b/65535.0}]
491    return [list $r $g $b]
492}
493
494#
495# Euler2XYZ --
496#
497#   Converts euler angles for the camera placement the to angles of
498#   rotation about the x/y/z axes, used by the engine.  Returns a list:
499#   {xangle, yangle, zangle}.
500#
501itcl::body Rappture::VisViewer::Euler2XYZ {theta phi psi} {
502    set xangle [expr {$theta-90.0}]
503    set yangle [expr {180-$phi}]
504    set zangle $psi
505    return [list $xangle $yangle $zangle]
506}
507
508
509#
510# SendEcho --
511#
512#     Used internally to echo sent data to clients interested in this widget.
513#     If the -sendcommand option is set, then it is invoked in the global scope
514#     with the <channel> and <data> values as arguments.  Otherwise, this does
515#     nothing.
516#
517itcl::body Rappture::VisViewer::SendEcho {channel {data ""}} {
518    #puts stderr ">>$data"
519    if {[string length $itk_option(-sendcommand)] > 0} {
520        uplevel #0 $itk_option(-sendcommand) [list $channel $data]
521    }
522}
523
524#
525# ReceiveEcho --
526#
527#     Echoes received data to clients interested in this widget.  If the
528#     -receivecommand option is set, then it is # invoked in the global
529#     scope with the <channel> and <data> values # as arguments.  Otherwise,
530#     this does nothing.
531#
532itcl::body Rappture::VisViewer::ReceiveEcho {channel {data ""}} {
533    #puts stderr "<<line $data"
534    if {[string length $itk_option(-receivecommand)] > 0} {
535        uplevel #0 $itk_option(-receivecommand) [list $channel $data]
536    }
537}
538
Note: See TracBrowser for help on using the repository browser.