A simple telnet client using Conduit

Hi all, I've written a simple telnet client using Michael Snoyman's Conduit library and was looking for comments as to whether I'm doing it right. In particular, is my usage of a ResourceT to track a thread a good idea, necessary or waste of time. The code is here: https://gist.github.com/1596792 Cheers, Erik -- ---------------------------------------------------------------------- Erik de Castro Lopo http://www.mega-nerd.com/

On line 29, instead of liftIO $ do mapM_ ... runResourceT $ do ... ... why not liftIO $ mapM_ ... ... ... ? Regarding threads, you should use resourceForkIO [1] which has a quite nicer interface. So you telnet would end like: telnet :: String -> Int -> IO () telnet host port = runResourceT $ do (releaseSock, hsock) <- with (connectTo host $ PortNumber $ fromIntegral port) hClose liftIO $ mapM_ (\h -> hSetBuffering h LineBuffering) [ stdin, stdout, hsock ] resourceForkIO $ sourceHandle stdin $$ sinkHandle hsock sourceHandle hsock $$ sinkHandle stdout release releaseSock Disclaimer: code not tested =). Cheers, [1] http://hackage.haskell.org/packages/archive/conduit/0.0.2/doc/html/Control-M... -- Felipe.

Thanks for the input Felipe. Felipe Almeida Lessa wrote:
On line 29, instead of
liftIO $ do mapM_ ... runResourceT $ do
Well that was because that whole block needs to run in IO.
Regarding threads, you should use resourceForkIO [1] which has a quite nicer interface.
I did read about resourceForkIO and it says: Introduce a reference-counting scheme to allow a resource context to be shared by multiple threads. Once the last thread exits, all remaining resources will be released. In my case, I don't have any resources that are shared between threads. All I have is the actual ThreadId returned by forkIO. Since that ThreadId actually isn't used explicitly anywhere (but is implicitly passed to killThread when "release releaseThread" is called). The other thing about your solution is the question of what happens to the ThreadId returned by resourceForkIO. Rewriting your solution to explicity handle the ThreadId I get: telnet :: String -> Int -> IO () telnet host port = runResourceT $ do (releaseSock, hsock) <- with (connectTo host $ PortNumber $ fromIntegral port) hClose liftIO $ mapM_ (\h -> hSetBuffering h LineBuffering) [ stdin, stdout, hsock ] tid <- resourceForkIO $ sourceHandle stdin $$ sinkHandle hsock sourceHandle hsock $$ sinkHandle stdout liftIO $ killThread tid release releaseSock The problem here is that I am not sure if the explicit killThread is actually needed and it is, I think my solution, where killThread happens automatically is actually better. If what happens within the outer call to resourceT is a long running process, your solution (in the absence of the explicit killThread) could leave threads lying around that would have been cleaned up much earlier in my soltuion. Thoughts? Erik -- ---------------------------------------------------------------------- Erik de Castro Lopo http://www.mega-nerd.com/

A new solution that drops two 'runResourceT' calls: telnet :: String -> Int -> IO () telnet host port = runResourceT $ do (releaseSock, hsock) <- with (connectTo host $ PortNumber $ fromIntegral port) hClose liftIO $ mapM_ (\h -> hSetBuffering h LineBuffering) [ stdin, stdout, hsock ] (releaseThread, _) <- with (forkIO $ runResourceT $ sourceHandle stdin $$ sinkHandle hsock) killThread sourceHandle hsock $$ sinkHandle stdout release releaseThread release releaseSock Erik -- ---------------------------------------------------------------------- Erik de Castro Lopo http://www.mega-nerd.com/

On Wed, Jan 11, 2012 at 10:28 PM, Erik de Castro Lopo
Thanks for the input Felipe.
Felipe Almeida Lessa wrote:
On line 29, instead of
liftIO $ do mapM_ ... runResourceT $ do
Well that was because that whole block needs to run in IO.
My point is that the second runResourceT is not necessary.
Regarding threads, you should use resourceForkIO [1] which has a quite nicer interface.
I did read about resourceForkIO and it says:
Introduce a reference-counting scheme to allow a resource context to be shared by multiple threads. Once the last thread exits, all remaining resources will be released.
In my case, I don't have any resources that are shared between threads. All I have is the actual ThreadId returned by forkIO. Since that ThreadId actually isn't used explicitly anywhere (but is implicitly passed to killThread when "release releaseThread" is called).
Actually, hsock is shared between both threads. With your original code, it gets closed regardless of what the other thread is doing. Which in this case is what you want, but you need to be careful.
The other thing about your solution is the question of what happens to the ThreadId returned by resourceForkIO. Rewriting your solution to explicity handle the ThreadId I get:
telnet :: String -> Int -> IO () telnet host port = runResourceT $ do (releaseSock, hsock) <- with (connectTo host $ PortNumber $ fromIntegral port) hClose liftIO $ mapM_ (\h -> hSetBuffering h LineBuffering) [ stdin, stdout, hsock ] tid <- resourceForkIO $ sourceHandle stdin $$ sinkHandle hsock sourceHandle hsock $$ sinkHandle stdout liftIO $ killThread tid release releaseSock
The problem here is that I am not sure if the explicit killThread is actually needed [...]
What about inverting which thread gets to do what? _ <- resourceForkIO $ sourceHandle hsock $$ sinkHandle stdout sourceHandle stdin $$ sinkHandle hsock release releaseSock My reasoning is that: - 'sourceHandle stdin' will close after the user presses ^D, thereby reaching 'release releaseSock'. - After the socket is closed, 'sourceHandle hsock' will close as well, killing the thread. Note that without 'release releaseSock' the socket would not be closed until the other thread died.
[...] and it is, I think my solution, where killThread happens automatically is actually better. If what happens within the outer call to resourceT is a long running process, your solution (in the absence of the explicit killThread) could leave threads lying around that would have been cleaned up much earlier in my soltuion.
Actually, I'm not sure if my solution is better or worse than yours. The question is "how long does it take for the thread to die after hsock gets closed?" If the answer is "right away", then my solution is simpler =). Otherwise, you solution is less resource-hungry. Cheers, -- Felipe.

Felipe Almeida Lessa wrote:
What about inverting which thread gets to do what?
_ <- resourceForkIO $ sourceHandle hsock $$ sinkHandle stdout sourceHandle stdin $$ sinkHandle hsock release releaseSock
Thats an interesting idea. Unfortunately this doesn't work correctly in that if the server disconnects, the client doesn't detect it and hangs with one end of the connection closed.
Actually, I'm not sure if my solution is better or worse than yours. The question is "how long does it take for the thread to die after hsock gets closed?" If the answer is "right away", then my solution is simpler =). Otherwise, you solution is less resource-hungry.
Well GHC runtime threads are very cheap while sockets/file descriptors are in comparison a very much limited resource. That means that code should be written to clean up sockets/fds in preference to cleaning up threads. Cheers, Erik -- ---------------------------------------------------------------------- Erik de Castro Lopo http://www.mega-nerd.com/
participants (2)
-
Erik de Castro Lopo
-
Felipe Almeida Lessa