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 | |
---|
18 | itcl::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 | # ---------------------------------------------------------------------- |
---|
93 | itcl::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 | # |
---|
151 | itcl::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 | # |
---|
162 | itcl::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 | # |
---|
185 | itcl::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 | # |
---|
205 | itcl::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 | # |
---|
273 | itcl::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 | # |
---|
287 | itcl::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 | # |
---|
299 | itcl::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 | # |
---|
333 | itcl::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 | # |
---|
347 | itcl::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 | # |
---|
365 | itcl::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 | # |
---|
396 | itcl::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 | # |
---|
420 | itcl::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 | # |
---|
448 | itcl::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 | # |
---|
486 | itcl::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 | # |
---|
501 | itcl::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 | # |
---|
517 | itcl::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 | # |
---|
532 | itcl::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 | |
---|