2
votes

The application I am trying to build is as follows.

Java Client <---> TCL server (say Server.tcl) <---> Some other TCL script (say ABC.tcl)

I am using Socket programming for the communication between Java Client and TCL server. Now my requirement is to call ABC.tcl from Server.tcl at runtime. ABC.tcl while executing will send some questions to Java client through server.tcl. The user sitting at Java client should send the reply to the questions asked and the answer should come back to ABC.tcl. Now while the reply comes back the ABC.tcl should wait. But I can't have a busy waiting loop in ABC.tcl as the control will not come to Server.tcl as it is the once who will talk to the Java client.

ABC.tcl will be using some procedures defined in Server.tcl. The design can be changed though if required.

I was thinking of using Threads in TCL. I was planning to call ABC.tcl as a separate thread, and then keep it waiting for the replies. This way the Server.tcl can execute separately from ABC.tcl.

Wanted opinion on is their any better way to do it ? If Threads is the only better way, what are the challenges I might encounter and how can I take care of them ?

Edited :

I am not using socket communication between Server.tcl and ABC.tcl. In Server.tcl, I am sourcing ABC.tcl and when some communication request comes from the Java client, I am calling a procedure (say proc someProc{}) of ABC.tcl. Now in someProc procedure, some processing is done and a question (say "Do you want to continue ?") is sent back to Java client through Server.tcl. Now in ABC.tcl I want the procedure someProc to wait till the user enters something. And when the user enters the answer (either yes/no), I want the procedure to continue execution from the point where it was waiting for user input.

Now what is happening is I am not able to correctly set that waiting condition as required. I tried using a flag variable that will be updated when the user inputs the answer, and tried keeping a busy waiting loop in someProc, based on the flag variable. but for when user input comes, it does not resume execution from waiting loop onwards. It starts like normal request and goes into the procedure that is registered in Server.tcl to handle the incoming data.

I have the "vwait events" call and also I have " fileevent $sock readable [list svcHandler $sock]" in the Server.tcl.

I am not able to get, how exactly can I wait in the procedure someProc in the ABC.tcl and resume processing from same busy wait, based on user input.

Heading ##Server.tcl File :

source "<path>/serverTest.tcl"


set svcPort [lindex $argv 0]
set filePath <path where all related files are kept>
set replyReady 0
set packet ""

proc writeToFile {fileName data mode} {
set fileId [open $fileName $mode]
puts -nonewline $fileId $data
close $fileId
}

proc writeJavaUTF {sock msg} {
  set date [exec date]
  set msg $date$msg
  set data [encoding convertto utf-8 $msg]
  puts -nonewline $sock [binary format "S" [string length $data]]$data
}

proc readJavaUTF {sock} {
  binary scan [read $sock 2] "S" len
  set data [read $sock [expr {$len & 0xFFFF}]]
  return [encoding convertfrom utf-8 $data]
}

proc sendMessageToUser {sock msg} {
      writeToFile [exec pwd]/CTS/log "\nSending Message Back to Client" "a"
     writeJavaUTF $sock $msg\n
}

proc setReplyReady {} {
        global replyReady
        set replyReady 1
        writeToFile [exec pwd]/CTS/log "\nSetReplyReady" "a"

}

proc resetReplyReady {} {
        global replyReady
        set replyReady 0
        writeToFile [exec pwd]/CTS/log "\nResetReplyReady" "a"
}

proc getReplyReadyStatus {} {
        global replyReady
       writeToFile [exec pwd]/CTS/log "\nReply Ready is : $replyReady" "a"
    return $replyReady
}


proc executeCommand {sock msg} {
    writeToFile [exec pwd]/CTS/log "\nexecuteCommand" "a"
    runServerTest $sock
}

# Handles the input from the client and  client shutdown
proc  svcHandler {sock} {

  global packet replyReady
  set packet [readJavaUTF $sock] ;# get the client packet
  writeToFile [exec pwd]/CTS/log "\nThe packet from the client is $packet" "w"

set packet [string range $packet 0 [expr {[string first "\n" $packet] - 1}]]

set endChar "EOC"

  if {[eof $sock] || ![string compare -nocase $packet $endChar]} {    ;# client gone or finished
      writeToFile [exec pwd]/CTS/log "Closing the socket" "a"
      writeToFile [exec pwd]/CTS/flag "0" "w"
     close $sock        ;# release the servers client channel
     exit
  } else {
    #doService $sock $ipacket
     set typeReply "reply"
     set typeExecute "exec"
     set type [string range $packet 0 [expr {[string first ":" $packet] - 1}]]
     writeToFile [exec pwd]/CTS/log "\nThe type is : $type" "a"
     # If it is not a reply for a request
     if {![string compare -nocase $type $typeExecute]} {
       executeCommand $sock $packet
     } else {
      writeToFile [exec pwd]/CTS/log "\nExecuting the ELSE" "a"
       setReplyReady
     }

  }
}

proc accept {sock addr port}  {

  # Setup handler for future communication on client socket
  fileevent $sock readable [list svcHandler $sock]

  # Read client input in lines, disable blocking I/O
  fconfigure $sock -buffering line -blocking 0 -translation binary

  return $sock
}

set sock [socket -server accept $svcPort]
vwait events    ;# handle events till variable events is set

Heading ## serverTest.tcl (As ABC.tcl)

proc runServerTest {sock} {
global replyReady
writeToFile [exec pwd]/CTS/log "\nInside serverTest.tcl." "a"

resetReplyReady
sendMessageToUser $sock "From Server : The socket is working. Do you want to continue ?"
writeToFile [exec pwd]/CTS/log "\nWaiting for user input" "a"
#Loop untill the reply is read from the socket
while {![getReplyReadyStatus]} {
after 1000
}

set retMessage [getPacket]
resetReplyReady
writeToFile [exec pwd]/CTS/log "\nThe reply from user is : $retMessage" "a"
}

Heading ## Challenge in the code

I send a new request as "exec:Hello" And I send reply as "reply:yes"

As you can see in the proc svcHandler{}, if it is a new request I execute the executeCommand{} proc. Else, I set the replyReady flag. When I execute the executeCommand{} proc, the proc runServerTest{} is called which is in serverTest.tcl. Now in this proc, I send the question back to the user, which I am able to see on the JAVA UI screen on client. And when I try to send the reply, the proc setReplyready{} which is in svcHandler{}, is not executed at all. When I saw the log file, I could see the output of the proc getReplyReadyStatus{}, which is being called from while loop in proc runServerTest{}, after every second. It means the while loop is executing indefinitely. And as the proc setReplyReady{} is not getting called from svcHandler{} may be the while loop is not exiting. Don't really know where the process is waiting. Any workaround ?

Thanks, Peeyush

3
You need to work out exactly what general architecture you're really after, bearing in mind that sockets and threads and coroutines and all that stuff is just part of how you might implement such things.Donal Fellows

3 Answers

6
votes

Don't use threads. Use the event loop.

Read up on fileevent: http://www.tcl.tk/man/tcl8.6/TclCmd/fileevent.htm

In general you set up event handlers for both the Java client socket and the ABC script socket to trigger when they are readable and do what you need to do when the data arrives.

If you need to implement timed operations, for example a watchdog timer, check out the after command.

3
votes

You have more than 1 thing that you want to communicate with asynchronously, the server and the user, but you can only have 1 point of execution (stack frame) per interpreter in Tcl versions less than 8.6. So you can't wait for server communication in one loop and user communication in another. Either rewrite one of them as a handler that does it's job and returns, or use:

  • Coroutines in Tcl 8.6.
  • Multiple interpreters.
  • Multiple processes (start ABC.tcl with it's own tclsh).
  • Multiple threads.

One interpreter/thread per connection?

3
votes

You don't need to use Threads. Instead I suggest using Tcl 8.6 which has coroutines.

Example:

proc listfiles {} {
    set lines 0
    set res {}
    foreach f [glob -tails -directory /foo/bar *] {
         append res $f\n
         if {[incr lines] >= 23} {
             yield $res
             set res {};
             set lines 0
         }
    }
    return $res
}

set corono 0

proc accept {sock host port} {
    fconfigure $sock -blocking 0 -encoding utf-8 -buffering line
    set coro ::coro[incr ::corono]
    puts $sock [coroutine $coro listfiles]
    if {[llength [info commands $coro]]} {
        # still something to do
        puts $sock "Do you want to continue ?"
        fileevent $sock readable [list readsock $coro $sock]
    } else {
        close $sock
    }
}

proc readsock {coro sock} {
    # Tcl treats 1, yes, true and on as true. see http://www.tcl.tk/man/tcl8.6/TclLib/GetInt.htm
    if {[gets $sock line] != -1 && [string is true $line]} {
        fileevent $sock readable {}
        puts $sock [$coro]
        if {[llength [info commands $coro]]} {
            # still something to do
            puts $sock "Do you want to continue ?"
            fileevent $sock readable [list readsock $coro $sock]
        } else {
            close $sock
        }
    } elseif {[string is false $line]} {
         close $sock
    }
}
socket -server accept 12457

The code for the socket handling etc is a little bit long, but the actual calculation is simple: call yield if you have collected enough data, this will suspend the current execution. If you are done, call return.

You should probably add some kind of error handling.