Following on from
Websocket on TclHttpd I was reading that the RFB protocol used by VNC could be layered on top of WebSocket. Searching the Wiki,
Mac Cody had written
TclRFB which is a
pure-Tcl Client and Server for the RFB protocol in 2003. On the
TclRFB homepage it mentions one of the Future Goals was
"Combine TclRFB with tclhttpd to allow serving of the Java VNC client to a browser. Alternately, the Java VNC client could be replaced with a TclRFB client that would run on the Tcl/Tk plugin." Well 11 years later the "Java VNC client" can be replaced by
noVNC, a VNC client using HTML5 WebSockets written in Javascript.
I thought if I could combine
TclHttpd with
TclRFB, WebSocket and
noVNC this would be an interesting project to test the WebSocket library.
I wanted to run the
TclRFB server in the same event loop as
TclHttpd and WebSocket. So I decided to run the
TclRFB Server in a Slave Interpreter and redirect its socket I/O to the WebSocket Library via alias commands. This worked out well. Testing was done using the rfbcounter demo that came with
TclRFB. The
TclRFB homepage says the server needs the VNC client to support BGR233. I had to modify rfbcounter.tcl as
noVNC works with 24bit True Colour.
I have created a
Starkit with the latest
TclHttpd from the Fossil repository. This works with Tcl8.6 and Tcl8.5. It's available from [
1] (Right click on the link and select "Save target as...")
Point your browser to
http://127.0.0.1:8015
and click on "WebSocket
TclRFB noVNC Test" on the homepage.
TclRFB looks to be very interesting. It's a shame it never progressed.
Below is the tclRFB-novnc.tcl file in the custom directory of
TclHttpd
Url_PrefixInstall /novnc [list ::novnc::domain /novnc]
package require websocket
namespace eval ::novnc {
}
namespace eval ::rfbcounter {
}
proc ::novnc::domain {prefix sock suffix} {
upvar #0 Httpd$sock data
set session [Session_Create Rfb 0]
::websocket::server $sock
::websocket::live $sock /novnc [list ::novnc::TclRFB $session]
set wstest [::websocket::test $sock $sock /novnc $data(headerlist) $data(query)]
if {$wstest == 1} {
Httpd_Suspend $sock 0
::rfbcounter::Setup $sock $session rfbcounterNovnc.tcl -clock 1 000000 0000ff
::websocket::upgrade $sock
} else {
Httpd_ReturnData $sock text/html "Not a valid Websocket connection!"
Session_Destroy $session
}
}
proc ::novnc::TclRFB {session sock type msg} {
upvar #0 Session:$session state
set interp $state(interp)
switch $type {
request {
set rfbClientAddr [lindex [fconfigure $sock -peername] 0]
set rfbClientPort [lindex [fconfigure $sock -peername] 2]
$interp eval ::rfb::AcceptServerSocket $sock $rfbClientAddr $rfbClientPort
}
close {
return
}
disconnect {
Session_Destroy $session
unset ::Httpd$sock
return
}
binary {
set state(lwsockmsg) 1
set state(wsockmsg) $msg
while {$state(lwsockmsg) > 0} {
$interp eval ::rfb::ServerConnectionStateMachine $sock
}
return
}
text {
return
}
}
}
proc ::rfbcounter::Setup { sock session sfile args } {
upvar #0 Session:$session state
set interp $state(interp)
foreach var {::v ::auto_path} {
$interp eval [list set $var [set $var]]
}
interp eval $interp {rename puts real_puts}
interp alias $interp puts {} ::rfbcounter::Puts $interp
interp eval $interp {rename read real_read}
interp alias $interp read {} ::rfbcounter::Read $sock $session
interp eval $interp {rename close real_close}
interp alias $interp close {} ::rfbcounter::Close $interp
interp eval $interp {rename fconfigure real_fconfigure}
interp alias $interp fconfigure {} ::rfbcounter::Fconfigure $interp
interp eval $interp {rename socket real_socket}
interp alias $interp socket {} ::rfbcounter::Socket $interp $sock
interp eval $interp {rename fileevent real_fileevent}
interp alias $interp fileevent {} ::rfbcounter::Fileevent $interp
interp share {} $sock $interp
interp eval $interp "set argc [llength $args]"
set cmdargv "set argv [list $args]"
interp eval $interp $cmdargv
$interp eval [list set sock $sock]
$interp eval [list set tclhttpdport [lindex [fconfigure $sock -sockname] 2]]
set cmd [list source [file join $starkit::topdir bin $sfile]]
$interp eval $cmd
}
proc ::rfbcounter::Puts { interp args } {
if {[string match "-nonewline" [lindex $args 0]]} {
set flag -nonewline
set args [lrange $args 1 end]
} else {
set flag ""
}
if {[llength $args] == 1} {
set chan stdout
return [$interp eval real_puts $chan $args]
} elseif {[llength $args] == 2} {
if {[string match "sock*" [lindex $args 0]]} {
set sock [lindex $args 0]
set msg [lindex $args 1]
::websocket::send $sock binary $msg
} else {
return [$interp eval real_puts $flag $args]
}
} else {
return [$interp error "wrong # args: should be \"puts ?-nonewline? ?channelId? string\""]
}
}
proc ::rfbcounter::Read {sock session args} {
upvar #0 Session:$session state
set interp $state(interp)
if { [llength $args] == 2 && [string match "sock*" [lindex $args 0]] } {
set range [expr [lindex $args 1]-1]
set wsockdata [string range $state(wsockmsg) 0 $range]
set state(wsockmsg) [string range $state(wsockmsg) [lindex $args 1] end]
set state(lwsockmsg) [string length $state(wsockmsg)]
return $wsockdata
} else {
return [$interp eval real_read $args]
}
}
proc ::rfbcounter::Close {interp args} {
if {[string match "sock*" [lindex $args 0]]} {
::websocket::close $sock
} else {
return [$interp eval real_close $args]
}
}
proc ::rfbcounter::Fconfigure {interp args} {
if {[string match "-buffering" [lindex $args 1]]} {
return
} else {
return [$interp eval real_fconfigure $args]
}
}
proc ::rfbcounter::Socket {interp sock args} {
if {[string match "-server" [lindex $args 0]]} {
return $sock
} else {
return [$interp eval real_socket $args]
}
}
proc ::rfbcounter::Fileevent {interp args} {
return
}
This is the rfbcounterNovnc.tcl in the bin directory of
TclHttpd
package require tclRFB
set ::rfb::rfb($sock,clientBEIfLittle) 1
set ndx 0
if {[string match {-clock} [lindex $argv 0]]} {
set showClock 1
incr ndx
} else {
set showClock 0
}
if {[regexp -- {^[1-9]*[0-9]$} [lindex $argv $ndx]]} {
set port [expr 5900 + [lindex $argv $ndx]]
incr ndx
} else {
puts {Incorrect command line options!!!}
puts {rfbcouter.tcl [-clock] display-number [fg [bg]]}
exit
}
if {$argc - $showClock > 1} {
if {[regexp -- {^[0-9a-f]{6}$} [lindex $argv $ndx]]} {
set fg [lindex $argv $ndx]
incr ndx
} else {
puts {Incorrect command line options}
puts {rfbcouter.tcl [-clock] display-number [fg [bg]]}
exit
}
} else {
set fg 000000
}
if {$argc - $showClock > 2} {
if {[regexp -- {^[0-9a-f]{6}$} [lindex $argv $ndx]]} {
set bg [lindex $argv $ndx]
incr ndx
} else {
puts {Incorrect command line options}
puts {rfbcouter.tcl [-clock] display-number [fg [bg]]}
exit
}
} else {
set bg ffffff
}
set sevenseg(0) {a b c d e f}
set sevenseg(1) {b c}
set sevenseg(2) {a b d e g}
set sevenseg(3) {a b c d g}
set sevenseg(4) {b c f g}
set sevenseg(5) {a c d f g}
set sevenseg(6) {a c d e f g}
set sevenseg(7) {a b c}
set sevenseg(8) {a b c d e f g}
set sevenseg(9) {a b c d f g}
set sevenseg(a) {25 10 50 10}
set sevenseg(b) {75 20 10 50}
set sevenseg(c) {75 80 10 50}
set sevenseg(d) {25 130 50 10}
set sevenseg(e) {15 80 10 50}
set sevenseg(f) {15 20 10 50}
set sevenseg(g) {25 70 50 10}
set sevenseg(colonH) {0 40 10 10}
set sevenseg(colonL) {0 100 10 10}
proc ServerSetup {port} {
set optList [list serverVersionMajor 3]
set optList [concat $optList [list serverVersionMinor 3]]
set optList [concat $optList [list serverBPP 32]]
set optList [concat $optList [list serverDepth 24]]
set optList [concat $optList [list serverBE 1]]
set optList [concat $optList [list serverTC 1]]
set optList [concat $optList [list serverRmax 255]]
set optList [concat $optList [list serverGmax 255]]
set optList [concat $optList [list serverBmax 255]]
set optList [concat $optList [list serverRshift 16]]
set optList [concat $optList [list serverGshift 8]]
set optList [concat $optList [list serverBshift 0]]
set optList [concat $optList [list serverShared client]]
set optList [concat $optList [list passwd {}]]
set optList [concat $optList [list passfile /home/mcody/.vnc/passwd]]
set optList [concat $optList [list scheme 1]]
set optList [concat $optList [list width 600]]
set optList [concat $optList [list height 150]]
set optList [concat $optList [list name "TclRFB Clock Server $::sock"]]
set optList [concat $optList [list updaterequest ProcessUpdateRequest]]
set optList [concat $optList [list keyevent ProcessKeyEvent]]
set optList [concat $optList [list pointerevent ProcessPointerEvent]]
set optList [concat $optList [list servercuttext ProcessServerCutText]]
set optList [concat $optList [list lval -1]]
return [::rfb::CreateServerSocket $port $optList]
}
proc ProcessUpdateRequest {sock inc x y width height} {
global showClock bg fg sevenseg
if {$::rfb::rfb($sock,clientBEIfLittle)} {
if {$::rfb::rfb($sock,clientBE) == 0} {
set bgRed [string range $bg 0 1]
set bgGreen [string range $bg 2 3]
set bgBlue [string range $bg 4 5]
set bg $bgBlue$bgGreen$bgRed
set fgRed [string range $fg 0 1]
set fgGreen [string range $fg 2 3]
set fgBlue [string range $fg 4 5]
set fg $fgBlue$fgGreen$fgRed
set ::rfb::rfb($sock,clientBEIfLittle) 0
} else {
set ::rfb::rfb($sock,clientBEIfLittle) 0
}
}
if {$::rfb::rfb($sock,state) eq {halted}} { return 0 }
if {$showClock} {
set cval [clock format [clock seconds] -format %H%M%S]
} else {
set cval [string trimleft $::rfb::rfb($sock,lval) 0]
if {$cval != {}} {
incr cval
} else {
set cval 1
}
if {$cval > 1000000} {
set cval 0
}
set cval [format %06d $cval]
}
if {![string match $::rfb::rfb($sock,lval) $cval] || !$inc} {
set i 0
foreach ndx {0 100 200 300 400 500} {
set num [string index $cval $i]
if {($num ne [string index $::rfb::rfb($sock,lval) $i]) || !$inc} {
set rreList [list 2 $ndx 0 100 150 $bg]
set rectList {}
foreach elm $sevenseg($num) {
lappend rectList [concat $sevenseg($elm) $fg]
}
lappend rreList $rectList
lappend encodeList $rreList
}
incr i
}
if {$showClock} {
set rectList [list [concat $sevenseg(colonH) $fg]]
lappend rectList [concat $sevenseg(colonL) $fg]
lappend encodeList [list 2 195 0 10 150 $bg $rectList]
lappend encodeList [list 2 395 0 10 150 $bg $rectList]
}
::rfb::SendFramebufferUpdate $sock $encodeList
set ::rfb::rfb($sock,lval) $cval
} else {
::rfb::SendFramebufferUpdate $sock {}
}
return 1
}
proc ProcessKeyEvent {sock downflag keysym} {
if {$downflag} {
::rfb::SendBell $sock
}
return 1
}
proc ProcessPointerEvent {sock buttonmask x y} {
if {$buttonmask} {
::rfb::SendBell $sock
}
return 1
}
proc ProcessServerCutText {sock text} {
::rfb::SendBell $sock
return 1
}
ServerSetup $tclhttpdport