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