1#!/usr/bin/python 2# 3# Urwid web (CGI/Asynchronous Javascript) display module 4# Copyright (C) 2004-2007 Ian Ward 5# 6# This library is free software; you can redistribute it and/or 7# modify it under the terms of the GNU Lesser General Public 8# License as published by the Free Software Foundation; either 9# version 2.1 of the License, or (at your option) any later version. 10# 11# This library is distributed in the hope that it will be useful, 12# but WITHOUT ANY WARRANTY; without even the implied warranty of 13# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 14# Lesser General Public License for more details. 15# 16# You should have received a copy of the GNU Lesser General Public 17# License along with this library; if not, write to the Free Software 18# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 19# 20# Urwid web site: http://excess.org/urwid/ 21 22from __future__ import division, print_function 23 24""" 25Urwid web application display module 26""" 27import os 28import sys 29import signal 30import random 31import select 32import socket 33import glob 34 35from urwid import util 36_js_code = r""" 37// Urwid web (CGI/Asynchronous Javascript) display module 38// Copyright (C) 2004-2005 Ian Ward 39// 40// This library is free software; you can redistribute it and/or 41// modify it under the terms of the GNU Lesser General Public 42// License as published by the Free Software Foundation; either 43// version 2.1 of the License, or (at your option) any later version. 44// 45// This library is distributed in the hope that it will be useful, 46// but WITHOUT ANY WARRANTY; without even the implied warranty of 47// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 48// Lesser General Public License for more details. 49// 50// You should have received a copy of the GNU Lesser General Public 51// License along with this library; if not, write to the Free Software 52// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 53// 54// Urwid web site: http://excess.org/urwid/ 55 56colours = new Object(); 57colours = { 58 '0': "black", 59 '1': "#c00000", 60 '2': "green", 61 '3': "#804000", 62 '4': "#0000c0", 63 '5': "#c000c0", 64 '6': "teal", 65 '7': "silver", 66 '8': "gray", 67 '9': "#ff6060", 68 'A': "lime", 69 'B': "yellow", 70 'C': "#8080ff", 71 'D': "#ff40ff", 72 'E': "aqua", 73 'F': "white" 74}; 75 76keycodes = new Object(); 77keycodes = { 78 8: "backspace", 9: "tab", 13: "enter", 27: "esc", 79 33: "page up", 34: "page down", 35: "end", 36: "home", 80 37: "left", 38: "up", 39: "right", 40: "down", 81 45: "insert", 46: "delete", 82 112: "f1", 113: "f2", 114: "f3", 115: "f4", 83 116: "f5", 117: "f6", 118: "f7", 119: "f8", 84 120: "f9", 121: "f10", 122: "f11", 123: "f12" 85 }; 86 87var conn = null; 88var char_width = null; 89var char_height = null; 90var screen_x = null; 91var screen_y = null; 92 93var urwid_id = null; 94var send_conn = null; 95var send_queue_max = 32; 96var send_queue = new Array(send_queue_max); 97var send_queue_in = 0; 98var send_queue_out = 0; 99 100var check_font_delay = 1000; 101var send_more_delay = 100; 102var poll_again_delay = 500; 103 104var document_location = null; 105 106var update_method = "multipart"; 107 108var sending = false; 109var lastkeydown = null; 110 111function setup_connection() { 112 if (window.XMLHttpRequest) { 113 conn = new XMLHttpRequest(); 114 } else if (window.ActiveXObject) { 115 conn = new ActiveXObject("Microsoft.XMLHTTP"); 116 } 117 118 if (conn == null) { 119 set_status("Connection Failed"); 120 alert( "Can't figure out how to send request." ); 121 return; 122 } 123 try{ 124 conn.multipart = true; 125 }catch(e){ 126 update_method = "polling"; 127 } 128 conn.onreadystatechange = handle_recv; 129 conn.open("POST", document_location, true); 130 conn.setRequestHeader("X-Urwid-Method",update_method); 131 conn.setRequestHeader("Content-type","text/plain"); 132 conn.send("window resize " +screen_x+" "+screen_y+"\n"); 133} 134 135function do_poll() { 136 if (urwid_id == null){ 137 alert("that's unpossible!"); 138 return; 139 } 140 if (window.XMLHttpRequest) { 141 conn = new XMLHttpRequest(); 142 } else if (window.ActiveXObject) { 143 conn = new ActiveXObject("Microsoft.XMLHTTP"); 144 } 145 conn.onreadystatechange = handle_recv; 146 conn.open("POST", document_location, true); 147 conn.setRequestHeader("X-Urwid-Method","polling"); 148 conn.setRequestHeader("X-Urwid-ID",urwid_id); 149 conn.setRequestHeader("Content-type","text/plain"); 150 conn.send("eh?"); 151} 152 153function handle_recv() { 154 if( ! conn ){ return;} 155 if( conn.readyState != 4) { 156 return; 157 } 158 if( conn.status == 404 && urwid_id != null) { 159 set_status("Connection Closed"); 160 return; 161 } 162 if( conn.status == 403 && update_method == "polling" ) { 163 set_status("Server Refused Connection"); 164 alert("This server does not allow polling clients.\n\n" + 165 "Please use a web browser with multipart support " + 166 "such as Mozilla Firefox"); 167 return; 168 } 169 if( conn.status == 503 ) { 170 set_status("Connection Failed"); 171 alert("The server has reached its maximum number of "+ 172 "connections.\n\nPlease try again later."); 173 return; 174 } 175 if( conn.status != 200) { 176 set_status("Connection Failed"); 177 alert("Error from server: "+conn.statusText); 178 return; 179 } 180 if( urwid_id == null ){ 181 urwid_id = conn.getResponseHeader("X-Urwid-ID"); 182 if( send_queue_in != send_queue_out ){ 183 // keys waiting 184 do_send(); 185 } 186 if(update_method=="polling"){ 187 set_status("Polling"); 188 }else if(update_method=="multipart"){ 189 set_status("Connected"); 190 } 191 192 } 193 194 if( conn.responseText == "" ){ 195 if(update_method=="polling"){ 196 poll_again(); 197 } 198 return; // keepalive 199 } 200 if( conn.responseText == "Z" ){ 201 set_status("Connection Closed"); 202 update_method = null; 203 return; 204 } 205 206 var text = document.getElementById('text'); 207 208 var last_screen = Array(text.childNodes.length); 209 for( var i=0; i<text.childNodes.length; i++ ){ 210 last_screen[i] = text.childNodes[i]; 211 } 212 213 var frags = conn.responseText.split("\n"); 214 var ln = document.createElement('span'); 215 var k = 0; 216 for( var i=0; i<frags.length; i++ ){ 217 var f = frags[i]; 218 if( f == "" ){ 219 var br = document.getElementById('br').cloneNode(true); 220 ln.appendChild( br ); 221 if( text.childNodes.length > k ){ 222 text.replaceChild(ln, text.childNodes[k]); 223 }else{ 224 text.appendChild(ln); 225 } 226 k = k+1; 227 ln = document.createElement('span'); 228 }else if( f.charAt(0) == "<" ){ 229 line_number = parseInt(f.substr(1)); 230 if( line_number == k ){ 231 k = k +1; 232 continue; 233 } 234 var clone = last_screen[line_number].cloneNode(true); 235 if( text.childNodes.length > k ){ 236 text.replaceChild(clone, text.childNodes[k]); 237 }else{ 238 text.appendChild(clone); 239 } 240 k = k+1; 241 }else{ 242 var span=make_span(f.substr(2),f.charAt(0),f.charAt(1)); 243 ln.appendChild( span ); 244 } 245 } 246 for( var i=k; i < text.childNodes.length; i++ ){ 247 text.removeChild(last_screen[i]); 248 } 249 250 if(update_method=="polling"){ 251 poll_again(); 252 } 253} 254 255function poll_again(){ 256 if(conn.status == 200){ 257 setTimeout("do_poll();",poll_again_delay); 258 } 259} 260 261 262function load_web_display(){ 263 if( document.documentURI ){ 264 document_location = document.documentURI; 265 }else{ 266 document_location = document.location; 267 } 268 269 document.onkeypress = body_keypress; 270 document.onkeydown = body_keydown; 271 document.onresize = body_resize; 272 273 body_resize(); 274 send_queue_out = send_queue_in; // don't queue the first resize 275 276 set_status("Connecting"); 277 setup_connection(); 278 279 setTimeout("check_fontsize();",check_font_delay); 280} 281 282function set_status( status ){ 283 var s = document.getElementById('status'); 284 var t = document.createTextNode(status); 285 s.replaceChild(t, s.firstChild); 286} 287 288function make_span(s, fg, bg){ 289 d = document.createElement('span'); 290 d.style.backgroundColor = colours[bg]; 291 d.style.color = colours[fg]; 292 d.appendChild(document.createTextNode(s)); 293 294 return d; 295} 296 297function body_keydown(e){ 298 if (conn == null){ 299 return; 300 } 301 if (!e) var e = window.event; 302 if (e.keyCode) code = e.keyCode; 303 else if (e.which) code = e.which; 304 305 var mod = ""; 306 var key; 307 308 if( e.ctrlKey ){ mod = "ctrl " + mod; } 309 if( e.altKey || e.metaKey ){ mod = "meta " + mod; } 310 if( e.shiftKey && e.charCode == 0 ){ mod = "shift " + mod; } 311 312 key = keycodes[code]; 313 314 if( key != undefined ){ 315 lastkeydown = key; 316 send_key( mod + key ); 317 stop_key_event(e); 318 return false; 319 } 320} 321 322function body_keypress(e){ 323 if (conn == null){ 324 return; 325 } 326 327 if (!e) var e = window.event; 328 if (e.keyCode) code = e.keyCode; 329 else if (e.which) code = e.which; 330 331 var mod = ""; 332 var key; 333 334 if( e.ctrlKey ){ mod = "ctrl " + mod; } 335 if( e.altKey || e.metaKey ){ mod = "meta " + mod; } 336 if( e.shiftKey && e.charCode == 0 ){ mod = "shift " + mod; } 337 338 if( e.charCode != null && e.charCode != 0 ){ 339 key = String.fromCharCode(e.charCode); 340 }else if( e.charCode == null ){ 341 key = String.fromCharCode(code); 342 }else{ 343 key = keycodes[code]; 344 if( key == undefined || lastkeydown == key ){ 345 lastkeydown = null; 346 stop_key_event(e); 347 return false; 348 } 349 } 350 351 send_key( mod + key ); 352 stop_key_event(e); 353 return false; 354} 355 356function stop_key_event(e){ 357 e.cancelBubble = true; 358 if( e.stopPropagation ){ 359 e.stopPropagation(); 360 } 361 if( e.preventDefault ){ 362 e.preventDefault(); 363 } 364} 365 366function send_key( key ){ 367 if( (send_queue_in+1)%send_queue_max == send_queue_out ){ 368 // buffer overrun 369 return; 370 } 371 send_queue[send_queue_in] = key; 372 send_queue_in = (send_queue_in+1)%send_queue_max; 373 374 if( urwid_id != null ){ 375 if (send_conn == undefined || send_conn.ready_state != 4 ){ 376 send_more(); 377 return; 378 } 379 do_send(); 380 } 381} 382 383function do_send() { 384 if( ! urwid_id ){ return; } 385 if( ! update_method ){ return; } // connection closed 386 if( send_queue_in == send_queue_out ){ return; } 387 if( sending ){ 388 //var queue_delta = send_queue_in - send_queue_out; 389 //if( queue_delta < 0 ){ queue_delta += send_queue_max; } 390 //set_status("Sending (queued "+queue_delta+")"); 391 return; 392 } 393 try{ 394 sending = true; 395 //set_status("starting send"); 396 if( send_conn == null ){ 397 if (window.XMLHttpRequest) { 398 send_conn = new XMLHttpRequest(); 399 } else if (window.ActiveXObject) { 400 send_conn = new ActiveXObject("Microsoft.XMLHTTP"); 401 } 402 }else if( send_conn.status != 200) { 403 alert("Error from server: "+send_conn.statusText); 404 return; 405 }else if(send_conn.readyState != 4 ){ 406 alert("not ready on send connection"); 407 return; 408 } 409 } catch(e) { 410 alert(e); 411 sending = false; 412 return; 413 } 414 send_conn.open("POST", document_location, true); 415 send_conn.onreadystatechange = send_handle_recv; 416 send_conn.setRequestHeader("Content-type","text/plain"); 417 send_conn.setRequestHeader("X-Urwid-ID",urwid_id); 418 var tmp_send_queue_in = send_queue_in; 419 var out = null; 420 if( send_queue_out > tmp_send_queue_in ){ 421 out = send_queue.slice(send_queue_out).join("\n") 422 if( tmp_send_queue_in > 0 ){ 423 out += "\n" + send_queue.slice(0,tmp_send_queue_in).join("\n"); 424 } 425 }else{ 426 out = send_queue.slice(send_queue_out, 427 tmp_send_queue_in).join("\n"); 428 } 429 send_queue_out = tmp_send_queue_in; 430 //set_status("Sending"); 431 send_conn.send( out +"\n" ); 432} 433 434function send_handle_recv() { 435 if( send_conn.readyState != 4) { 436 return; 437 } 438 if( send_conn.status == 404) { 439 set_status("Connection Closed"); 440 update_method = null; 441 return; 442 } 443 if( send_conn.status != 200) { 444 alert("Error from server: "+send_conn.statusText); 445 return; 446 } 447 448 sending = false; 449 450 if( send_queue_out != send_queue_in ){ 451 send_more(); 452 } 453} 454 455function send_more(){ 456 setTimeout("do_send();",send_more_delay); 457} 458 459function check_fontsize(){ 460 body_resize() 461 setTimeout("check_fontsize();",check_font_delay); 462} 463 464function body_resize(){ 465 var t = document.getElementById('testchar'); 466 var t2 = document.getElementById('testchar2'); 467 var text = document.getElementById('text'); 468 469 var window_width; 470 var window_height; 471 if (window.innerHeight) { 472 window_width = window.innerWidth; 473 window_height = window.innerHeight; 474 }else{ 475 window_width = document.documentElement.clientWidth; 476 window_height = document.documentElement.clientHeight; 477 //var z = "CI:"; for(var i in bod){z = z + " " + i;} alert(z); 478 } 479 480 char_width = t.offsetLeft / 44; 481 var avail_width = window_width-18; 482 var avail_width_mod = avail_width % char_width; 483 var x_size = (avail_width - avail_width_mod)/char_width; 484 485 char_height = t2.offsetTop - t.offsetTop; 486 var avail_height = window_height-text.offsetTop-10; 487 var avail_height_mod = avail_height % char_height; 488 var y_size = (avail_height - avail_height_mod)/char_height; 489 490 text.style.width = x_size*char_width+"px"; 491 text.style.height = y_size*char_height+"px"; 492 493 if( screen_x != x_size || screen_y != y_size ){ 494 send_key("window resize "+x_size+" "+y_size); 495 } 496 screen_x = x_size; 497 screen_y = y_size; 498} 499 500""" 501 502ALARM_DELAY = 60 503POLL_CONNECT = 3 504MAX_COLS = 200 505MAX_ROWS = 100 506MAX_READ = 4096 507BUF_SZ = 16384 508 509_code_colours = { 510 'black': "0", 511 'dark red': "1", 512 'dark green': "2", 513 'brown': "3", 514 'dark blue': "4", 515 'dark magenta': "5", 516 'dark cyan': "6", 517 'light gray': "7", 518 'dark gray': "8", 519 'light red': "9", 520 'light green': "A", 521 'yellow': "B", 522 'light blue': "C", 523 'light magenta': "D", 524 'light cyan': "E", 525 'white': "F", 526} 527 528# replace control characters with ?'s 529_trans_table = "?" * 32 + "".join([chr(x) for x in range(32, 256)]) 530 531_css_style = """ 532body { margin: 8px 8px 8px 8px; border: 0; 533 color: black; background-color: silver; 534 font-family: fixed; overflow: hidden; } 535 536form { margin: 0 0 8px 0; } 537 538#text { position: relative; 539 background-color: silver; 540 width: 100%; height: 100%; 541 margin: 3px 0 0 0; border: 1px solid #999; } 542 543#page { position: relative; width: 100%;height: 100%;} 544""" 545 546# HTML Initial Page 547_html_page = [ 548"""<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" 549 "http://www.w3.org/TR/html4/loose.dtd"> 550<html> 551<head> 552<title>Urwid Web Display - ""","""</title> 553<style type="text/css"> 554""" + _css_style + """ 555</style> 556</head> 557<body id="body" onload="load_web_display()"> 558<div style="position:absolute; visibility:hidden;"> 559<br id="br"\> 560<pre>The quick brown fox jumps over the lazy dog.<span id="testchar">X</span> 561<span id="testchar2">Y</span></pre> 562</div> 563Urwid Web Display - <b>""","""</b> - 564Status: <span id="status">Set up</span> 565<script type="text/javascript"> 566//<![CDATA[ 567""" + _js_code +""" 568//]]> 569</script> 570<pre id="text"></pre> 571</body> 572</html> 573"""] 574 575class Screen: 576 def __init__(self): 577 self.palette = {} 578 self.has_color = True 579 self._started = False 580 581 started = property(lambda self: self._started) 582 583 def register_palette( self, l ): 584 """Register a list of palette entries. 585 586 l -- list of (name, foreground, background) or 587 (name, same_as_other_name) palette entries. 588 589 calls self.register_palette_entry for each item in l 590 """ 591 592 for item in l: 593 if len(item) in (3,4): 594 self.register_palette_entry( *item ) 595 continue 596 assert len(item) == 2, "Invalid register_palette usage" 597 name, like_name = item 598 if like_name not in self.palette: 599 raise Exception("palette entry '%s' doesn't exist"%like_name) 600 self.palette[name] = self.palette[like_name] 601 602 def register_palette_entry( self, name, foreground, background, 603 mono=None): 604 """Register a single palette entry. 605 606 name -- new entry/attribute name 607 foreground -- foreground colour 608 background -- background colour 609 mono -- monochrome terminal attribute 610 611 See curses_display.register_palette_entry for more info. 612 """ 613 if foreground == "default": 614 foreground = "black" 615 if background == "default": 616 background = "light gray" 617 self.palette[name] = (foreground, background, mono) 618 619 def set_mouse_tracking(self, enable=True): 620 """Not yet implemented""" 621 pass 622 623 def tty_signal_keys(self, *args, **vargs): 624 """Do nothing.""" 625 pass 626 627 def start(self): 628 """ 629 This function reads the initial screen size, generates a 630 unique id and handles cleanup when fn exits. 631 632 web_display.set_preferences(..) must be called before calling 633 this function for the preferences to take effect 634 """ 635 global _prefs 636 637 if self._started: 638 return util.StoppingContext(self) 639 640 client_init = sys.stdin.read(50) 641 assert client_init.startswith("window resize "),client_init 642 ignore1,ignore2,x,y = client_init.split(" ",3) 643 x = int(x) 644 y = int(y) 645 self._set_screen_size( x, y ) 646 self.last_screen = {} 647 self.last_screen_width = 0 648 649 self.update_method = os.environ["HTTP_X_URWID_METHOD"] 650 assert self.update_method in ("multipart","polling") 651 652 if self.update_method == "polling" and not _prefs.allow_polling: 653 sys.stdout.write("Status: 403 Forbidden\r\n\r\n") 654 sys.exit(0) 655 656 clients = glob.glob(os.path.join(_prefs.pipe_dir,"urwid*.in")) 657 if len(clients) >= _prefs.max_clients: 658 sys.stdout.write("Status: 503 Sever Busy\r\n\r\n") 659 sys.exit(0) 660 661 urwid_id = "%09d%09d"%(random.randrange(10**9), 662 random.randrange(10**9)) 663 self.pipe_name = os.path.join(_prefs.pipe_dir,"urwid"+urwid_id) 664 os.mkfifo(self.pipe_name+".in",0o600) 665 signal.signal(signal.SIGTERM,self._cleanup_pipe) 666 667 self.input_fd = os.open(self.pipe_name+".in", 668 os.O_NONBLOCK | os.O_RDONLY) 669 self.input_tail = "" 670 self.content_head = ("Content-type: " 671 "multipart/x-mixed-replace;boundary=ZZ\r\n" 672 "X-Urwid-ID: "+urwid_id+"\r\n" 673 "\r\n\r\n" 674 "--ZZ\r\n") 675 if self.update_method=="polling": 676 self.content_head = ( 677 "Content-type: text/plain\r\n" 678 "X-Urwid-ID: "+urwid_id+"\r\n" 679 "\r\n\r\n") 680 681 signal.signal(signal.SIGALRM,self._handle_alarm) 682 signal.alarm( ALARM_DELAY ) 683 self._started = True 684 685 return util.StoppingContext(self) 686 687 def stop(self): 688 """ 689 Restore settings and clean up. 690 """ 691 if not self._started: 692 return 693 694 # XXX which exceptions does this actually raise? EnvironmentError? 695 try: 696 self._close_connection() 697 except Exception: 698 pass 699 signal.signal(signal.SIGTERM,signal.SIG_DFL) 700 self._cleanup_pipe() 701 self._started = False 702 703 def set_input_timeouts(self, *args): 704 pass 705 706 def run_wrapper(self,fn): 707 """ 708 Run the application main loop, calling start() first 709 and stop() on exit. 710 """ 711 try: 712 self.start() 713 return fn() 714 finally: 715 self.stop() 716 717 718 def _close_connection(self): 719 if self.update_method == "polling child": 720 self.server_socket.settimeout(0) 721 sock, addr = self.server_socket.accept() 722 sock.sendall("Z") 723 sock.close() 724 725 if self.update_method == "multipart": 726 sys.stdout.write("\r\nZ" 727 "\r\n--ZZ--\r\n") 728 sys.stdout.flush() 729 730 def _cleanup_pipe(self, *args): 731 if not self.pipe_name: return 732 # XXX which exceptions does this actually raise? EnvironmentError? 733 try: 734 os.remove(self.pipe_name+".in") 735 os.remove(self.pipe_name+".update") 736 except Exception: 737 pass 738 739 def _set_screen_size(self, cols, rows ): 740 """Set the screen size (within max size).""" 741 742 if cols > MAX_COLS: 743 cols = MAX_COLS 744 if rows > MAX_ROWS: 745 rows = MAX_ROWS 746 self.screen_size = cols, rows 747 748 def draw_screen(self, size, r ): 749 """Send a screen update to the client.""" 750 751 (cols, rows) = size 752 753 if cols != self.last_screen_width: 754 self.last_screen = {} 755 756 sendq = [self.content_head] 757 758 if self.update_method == "polling": 759 send = sendq.append 760 elif self.update_method == "polling child": 761 signal.alarm( 0 ) 762 try: 763 s, addr = self.server_socket.accept() 764 except socket.timeout: 765 sys.exit(0) 766 send = s.sendall 767 else: 768 signal.alarm( 0 ) 769 send = sendq.append 770 send("\r\n") 771 self.content_head = "" 772 773 assert r.rows() == rows 774 775 if r.cursor is not None: 776 cx, cy = r.cursor 777 else: 778 cx = cy = None 779 780 new_screen = {} 781 782 y = -1 783 for row in r.content(): 784 y += 1 785 row = list(row) 786 787 l = [] 788 789 sig = tuple(row) 790 if y == cy: sig = sig + (cx,) 791 new_screen[sig] = new_screen.get(sig,[]) + [y] 792 old_line_numbers = self.last_screen.get(sig, None) 793 if old_line_numbers is not None: 794 if y in old_line_numbers: 795 old_line = y 796 else: 797 old_line = old_line_numbers[0] 798 send( "<%d\n"%old_line ) 799 continue 800 801 col = 0 802 for (a, cs, run) in row: 803 run = run.translate(_trans_table) 804 if a is None: 805 fg,bg,mono = "black", "light gray", None 806 else: 807 fg,bg,mono = self.palette[a] 808 if y == cy and col <= cx: 809 run_width = util.calc_width(run, 0, 810 len(run)) 811 if col+run_width > cx: 812 l.append(code_span(run, fg, bg, 813 cx-col)) 814 else: 815 l.append(code_span(run, fg, bg)) 816 col += run_width 817 else: 818 l.append(code_span(run, fg, bg)) 819 820 send("".join(l)+"\n") 821 self.last_screen = new_screen 822 self.last_screen_width = cols 823 824 if self.update_method == "polling": 825 sys.stdout.write("".join(sendq)) 826 sys.stdout.flush() 827 sys.stdout.close() 828 self._fork_child() 829 elif self.update_method == "polling child": 830 s.close() 831 else: # update_method == "multipart" 832 send("\r\n--ZZ\r\n") 833 sys.stdout.write("".join(sendq)) 834 sys.stdout.flush() 835 836 signal.alarm( ALARM_DELAY ) 837 838 839 def clear(self): 840 """ 841 Force the screen to be completely repainted on the next 842 call to draw_screen(). 843 844 (does nothing for web_display) 845 """ 846 pass 847 848 849 def _fork_child(self): 850 """ 851 Fork a child to run CGI disconnected for polling update method. 852 Force parent process to exit. 853 """ 854 daemonize( self.pipe_name +".err" ) 855 self.input_fd = os.open(self.pipe_name+".in", 856 os.O_NONBLOCK | os.O_RDONLY) 857 self.update_method = "polling child" 858 s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) 859 s.bind( self.pipe_name+".update" ) 860 s.listen(1) 861 s.settimeout(POLL_CONNECT) 862 self.server_socket = s 863 864 def _handle_alarm(self, sig, frame): 865 assert self.update_method in ("multipart","polling child") 866 if self.update_method == "polling child": 867 # send empty update 868 try: 869 s, addr = self.server_socket.accept() 870 s.close() 871 except socket.timeout: 872 sys.exit(0) 873 else: 874 # send empty update 875 sys.stdout.write("\r\n\r\n--ZZ\r\n") 876 sys.stdout.flush() 877 signal.alarm( ALARM_DELAY ) 878 879 880 def get_cols_rows(self): 881 """Return the screen size.""" 882 return self.screen_size 883 884 def get_input(self, raw_keys=False): 885 """Return pending input as a list.""" 886 l = [] 887 resized = False 888 889 try: 890 iready,oready,eready = select.select( 891 [self.input_fd],[],[],0.5) 892 except select.error as e: 893 # return on interruptions 894 if e.args[0] == 4: 895 if raw_keys: 896 return [],[] 897 return [] 898 raise 899 900 if not iready: 901 if raw_keys: 902 return [],[] 903 return [] 904 905 keydata = os.read(self.input_fd, MAX_READ) 906 os.close(self.input_fd) 907 self.input_fd = os.open(self.pipe_name+".in", 908 os.O_NONBLOCK | os.O_RDONLY) 909 #sys.stderr.write( repr((keydata,self.input_tail))+"\n" ) 910 keys = keydata.split("\n") 911 keys[0] = self.input_tail + keys[0] 912 self.input_tail = keys[-1] 913 914 for k in keys[:-1]: 915 if k.startswith("window resize "): 916 ign1,ign2,x,y = k.split(" ",3) 917 x = int(x) 918 y = int(y) 919 self._set_screen_size(x, y) 920 resized = True 921 else: 922 l.append(k) 923 if resized: 924 l.append("window resize") 925 926 if raw_keys: 927 return l, [] 928 return l 929 930 931def code_span( s, fg, bg, cursor = -1): 932 code_fg = _code_colours[ fg ] 933 code_bg = _code_colours[ bg ] 934 935 if cursor >= 0: 936 c_off, _ign = util.calc_text_pos(s, 0, len(s), cursor) 937 c2_off = util.move_next_char(s, c_off, len(s)) 938 939 return ( code_fg + code_bg + s[:c_off] + "\n" + 940 code_bg + code_fg + s[c_off:c2_off] + "\n" + 941 code_fg + code_bg + s[c2_off:] + "\n") 942 else: 943 return code_fg + code_bg + s + "\n" 944 945 946def html_escape(text): 947 """Escape text so that it will be displayed safely within HTML""" 948 text = text.replace('&','&') 949 text = text.replace('<','<') 950 text = text.replace('>','>') 951 return text 952 953 954def is_web_request(): 955 """ 956 Return True if this is a CGI web request. 957 """ 958 return 'REQUEST_METHOD' in os.environ 959 960def handle_short_request(): 961 """ 962 Handle short requests such as passing keystrokes to the application 963 or sending the initial html page. If returns True, then this 964 function recognised and handled a short request, and the calling 965 script should immediately exit. 966 967 web_display.set_preferences(..) should be called before calling this 968 function for the preferences to take effect 969 """ 970 global _prefs 971 972 if not is_web_request(): 973 return False 974 975 if os.environ['REQUEST_METHOD'] == "GET": 976 # Initial request, send the HTML and javascript. 977 sys.stdout.write("Content-type: text/html\r\n\r\n" + 978 html_escape(_prefs.app_name).join(_html_page)) 979 return True 980 981 if os.environ['REQUEST_METHOD'] != "POST": 982 # Don't know what to do with head requests etc. 983 return False 984 985 if 'HTTP_X_URWID_ID' not in os.environ: 986 # If no urwid id, then the application should be started. 987 return False 988 989 urwid_id = os.environ['HTTP_X_URWID_ID'] 990 if len(urwid_id)>20: 991 #invalid. handle by ignoring 992 #assert 0, "urwid id too long!" 993 sys.stdout.write("Status: 414 URI Too Long\r\n\r\n") 994 return True 995 for c in urwid_id: 996 if c not in "0123456789": 997 # invald. handle by ignoring 998 #assert 0, "invalid chars in id!" 999 sys.stdout.write("Status: 403 Forbidden\r\n\r\n") 1000 return True 1001 1002 if os.environ.get('HTTP_X_URWID_METHOD',None) == "polling": 1003 # this is a screen update request 1004 s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) 1005 try: 1006 s.connect( os.path.join(_prefs.pipe_dir, 1007 "urwid"+urwid_id+".update") ) 1008 data = "Content-type: text/plain\r\n\r\n"+s.recv(BUF_SZ) 1009 while data: 1010 sys.stdout.write(data) 1011 data = s.recv(BUF_SZ) 1012 return True 1013 except socket.error: 1014 sys.stdout.write("Status: 404 Not Found\r\n\r\n") 1015 return True 1016 1017 # this is a keyboard input request 1018 try: 1019 fd = os.open((os.path.join(_prefs.pipe_dir, 1020 "urwid"+urwid_id+".in")), os.O_WRONLY) 1021 except OSError: 1022 sys.stdout.write("Status: 404 Not Found\r\n\r\n") 1023 return True 1024 1025 # FIXME: use the correct encoding based on the request 1026 keydata = sys.stdin.read(MAX_READ) 1027 os.write(fd,keydata.encode('ascii')) 1028 os.close(fd) 1029 sys.stdout.write("Content-type: text/plain\r\n\r\n") 1030 1031 return True 1032 1033 1034class _Preferences: 1035 app_name = "Unnamed Application" 1036 pipe_dir = "/tmp" 1037 allow_polling = True 1038 max_clients = 20 1039 1040_prefs = _Preferences() 1041 1042def set_preferences( app_name, pipe_dir="/tmp", allow_polling=True, 1043 max_clients=20 ): 1044 """ 1045 Set web_display preferences. 1046 1047 app_name -- application name to appear in html interface 1048 pipe_dir -- directory for input pipes, daemon update sockets 1049 and daemon error logs 1050 allow_polling -- allow creation of daemon processes for 1051 browsers without multipart support 1052 max_clients -- maximum concurrent client connections. This 1053 pool is shared by all urwid applications 1054 using the same pipe_dir 1055 """ 1056 global _prefs 1057 _prefs.app_name = app_name 1058 _prefs.pipe_dir = pipe_dir 1059 _prefs.allow_polling = allow_polling 1060 _prefs.max_clients = max_clients 1061 1062 1063class ErrorLog: 1064 def __init__(self, errfile ): 1065 self.errfile = errfile 1066 def write(self, err): 1067 open(self.errfile,"a").write(err) 1068 1069 1070def daemonize( errfile ): 1071 """ 1072 Detach process and become a daemon. 1073 """ 1074 pid = os.fork() 1075 if pid: 1076 os._exit(0) 1077 1078 os.setsid() 1079 signal.signal(signal.SIGHUP, signal.SIG_IGN) 1080 os.umask(0) 1081 1082 pid = os.fork() 1083 if pid: 1084 os._exit(0) 1085 1086 os.chdir("/") 1087 for fd in range(0,20): 1088 try: 1089 os.close(fd) 1090 except OSError: 1091 pass 1092 1093 sys.stdin = open("/dev/null","r") 1094 sys.stdout = open("/dev/null","w") 1095 sys.stderr = ErrorLog( errfile ) 1096 1097