Success Comunicating With External App Using TCPSocket

Hi all,

A while back on a post a need was discussed to communicate with an external app on windows. Sockets proved to be finicky because of blocking, but I was able to successfully accomplish it by using a timer and an error trap.

Here is the code:

require 'socket'

s = TCPSocket.new('192.168.0.158',11235)
s.puts 'Connected:SketchUp'

stimer = UI.start_timer(0.5,true) { 
  begin
    puts s.read_nonblock(5000)
  rescue
  end
}

And the gif:

It would be interesting to find out if anyone knows, what the resource cost is, for a timer running like that. My CPU usage stayed at 0% with the timer running so I can’t really tell if there is much impact.

I had to set the timer to 0.01 to get the resolution I needed. Also it seems like the max length was 8193 characters.

I’m coming back to this project after letting it lay for some time. Does anyone know of a better way to communicate with an external app on Windows? I’d like to create a C#/WPF solution because I need my app to connect to a PostgreSQL database to read material data and to store and retrieve quote information. My extension will be used by multiple users sharing info from the same database.

If SketchUp ever implements my request to allow HTLM dialogs in the trays I might be going the wrong direction?

Any ideas?

Haven’t tried anything like what you’ve described, but have you looked at the pg gem?

Is it possible to install gems in SketchUp? If so is the process reliable enough for a distributed extension?

Yes, and pg is available as a ‘fat binary’ gem, so compile tools aren’t required. See Gem.install:

Gem.install('pg')

You’ve still got to decide whether to use C#'s ORM tools, roll your own, or add another gem.

gem env can be done in SU via:

require 'rubygems/commands/environment_command'
Gem::Commands::EnvironmentCommand.new.show_environment.gsub("\n", "\r\n")

Re sockets, I haven’t used sockets in SU or with older versions of ruby. Only within the last year has ruby been fully tested on windows, and that’s just trunk. So, if there were any socket issues, they’ve only been backported to 2.3 or 2.4…

1 Like

Sweet. That was easy. Thanks for your help.

Now I’ve still got to decide whether to utilize my C# skills or try to learn HTML/Java-script…

Neil,

I assume you listed a simplified version of your socket code. Regardless, you might have a look at IO#ready? from io/wait. Also, an opened ended rescue leaves a lot of issues ‘untrapped’, and maybe a timer loop running.

Sounds like an interesting project. Given what you’ve said, I’m guessing the C#/socket solution would be easiest. I’ve done sockets in both Ruby & C#…

Greg

Greg,

Thanks for your valuable input. The problem I was having was blocking, because SketchUp doesn’t support Ruby threads. I came up with the timer and the error trap to prevent the following error.

Error: #<IO::EWOULDBLOCKWaitReadable: A non-blocking socket operation could not be completed immediately. - read would block>

The #nread method from your link looked interesting, but I’m not sure how to use it.

Maybe it would be better to ask: What is the best way to detect when information is available to be read, and then to retrieve it?

Neil,

I wouldn’t put all the blame on SU, it’s a combination of Ruby, Windows, and SU. Ruby doesn’t have the callback/event objects that C# has…

I’ll have a look at this later. As to the rescue, what I was referring to was a change something like:

stimer = UI.start_timer(0.5,true) { 
  begin
    puts s.read_nonblock(5000)
  rescue IO::EWOULDBLOCKWaitReadable
  rescue <TCPServer dropped error>
    # do something?
  end
}

Between standard Ruby and adding io/wait and/or io/nonblock, there are a few ways to handle this, and I may even look at threads. Is the data over the socket intermittent (based on UI actions)?

Greg

That is exactly the case. It would basically be the same use as the add_action_callback method.

Thanks to @DanRathbun’s help I’ve managed to implement a much cleaner solution using the Http class rather than a ruby socket.

Works like a charm!!!

Here is the ruby class I created.
class Communicator  
  
  def initialize(connectionstring)
    @connectionstring = connectionstring
    @connected = false
    start_listener
  end
  
  def start_listener
    @connected = true
    @listener = Sketchup::Http::Request.new(@connectionstring, Sketchup::Http::GET)
    @listener.headers = {'Listen'=>'SketchUp'}
    @listener.start do |request, response|
      puts response.body
      if (response.body == "")
        puts "Lost connection or couldn't connect to server."
        close
      else
        start_listener
      end     
    end
  end
  
  def send(msg,key = "msg")
    if key == "Listen" then key == "msg" end #don't let users start a listen by mistake
    
    @request = Sketchup::Http::Request.new(@connectionstring, Sketchup::Http::GET)
    @request.headers = {key=>msg}
    @request.start do |request, response|
      
      #puts "body: #{response.body}"
    end
  end
  
  def close()
    if @request then @request.cancel end
    if @listener then @listener.cancel end
    @connected = false
  end  
  
  def isconnected?
    return @connected
  end
  
end

To start the connection simply use the class like this:

com = Communicator.new("http://localhost:11235/")

To send a message simply do this:

com.send 'My message to server.'

Messages from server are out put to console as they are received.

Here is the C# class if anyone is interested.
 public class WebServer
    {
        public EventHandler OnDataAvailable;
        private readonly HttpListener _listener = new HttpListener();
        private Stack<string> sendmsgs = new Stack<string>();
        public TaskScheduler scheduler = TaskScheduler.FromCurrentSynchronizationContext();

        public WebServer(string host)
        {
            if (!HttpListener.IsSupported)
                throw new NotSupportedException(
                    "Needs Windows XP SP2, Server 2003 or later.");
            _listener.Prefixes.Add(host);
            _listener.Start();
        }

        public void Run()
        {
            ThreadPool.QueueUserWorkItem((o) =>
            {
                try
                {
                    while (_listener.IsListening)
                    {
                        ThreadPool.QueueUserWorkItem((c) =>
                        {
                            var ctx = c as HttpListenerContext;
                            try
                            {
                                string rstr = "";
                                if (ctx.Request.Headers.AllKeys[0] == "Listen")
                                {
                                    while (_listener.IsListening)
                                    {
                                        if (sendmsgs.Count > 0)
                                        {
                                            rstr = sendmsgs.Pop();
                                            break;
                                        }
                                    }
                                }
                                else rstr = RecieveMessage(ctx.Request);
                                byte[] buf = Encoding.UTF8.GetBytes(rstr);
                                ctx.Response.ContentLength64 = buf.Length;
                                ctx.Response.OutputStream.Write(buf, 0, buf.Length);
                            }
                            catch {  } // suppress any exceptions
                            finally
                            {
                                // always close the stream
                                ctx.Response.OutputStream.Close();
                            }
                        }, _listener.GetContext());
                    }
                }
                catch { } // suppress any exceptions
            });
        }

        public void SendMsg(string msg)
        {
            sendmsgs.Push(msg);
        }

        private string RecieveMessage(HttpListenerRequest request)
        {
            string msg = request.Headers[request.Headers.AllKeys[0]].ToString();
            Task task = new Task(() => OnDataAvailable?.Invoke(new string[] { request.Headers.AllKeys[0], msg}, EventArgs.Empty));
            task.Start(scheduler);
            task.Wait();
            return "read";
        }

        public void Stop()
        {
            _listener.Stop();
            _listener.Close();
        }
    }
1 Like

I also noted that with this method there is no need to worry about escaping characters.

It might be possible - if the gem doesn’t need compiling. But there’s also issues with SSL certifies in previous SU versions. Additionally, the OpenSSL version that ships with SU’s Ruby is under Windows causing a severe performance bug eventually. As SU use more memory after loaded models there could be a freeze lasting minutes. One of the reasons we added Sketchup::Http as an alternative to Ruby’s Net::Http.

No, for several reasons:

  1. The broken SSL certificates in older SU versions. (Not that old, SU2014-SU2015 impacted, I think SU2016 had some issues as well.)
  2. The severe performance lag when initiating HTTPS request - which installing gems will trigger.
  3. It sets the stage for clashes with other extensions. Imagine if your extension rely on one version of a gem, but another extension rely on another incompatible version. Which version will be loaded?

Remember that the gem system was designed to be used in a single Ruby application where there is a single app author. But in SketchUp you are sharing the same environment with multiple other products.
The end user experience will not be great if your extension needs to install gems in order to operate. And it will be very prone to errors which will cause more friction for your users and cause yourself more support work.

Instead, you can repackage the gem under your own namespace and bundle it with your extension.

2 Likes

I was doing some testing on SketchUp 2018 and canceling a request creates a bugsplat. Does anyone know of a work around? Leave the request open and forget about it?


@connectionstring = 'http://localhost:12345/'
@listener = Sketchup::Http::Request.new(@connectionstring, Sketchup::Http::GET)
@listener.headers = {'Listen'=>'SketchUp'}
@listener.start do |request, response|
  puts response.body
  if (response.body == "")
    puts "Lost connection or couldn't connect to server."
    close
  else
    #restart_listener
  end     
end

#this makes a bugsplat on SU 2018, 2017 & 2020 returns true - didn't test 2019
if @listener then @listener.cancel end

The block passed to Sketchup::Http::Request#start is executed async, so the code following it is executed immediately. Still probably shouldn’t bugsplat.

I tried a few means of polling for when the connection was finished, and all blocked the request, and hence, froze SU.

I suspect that for http that is more ‘API’ oriented, Net::HTTP is probably the best thing to use. It can handle redirects, retries, etc. Also, with Net::HTTP#start, one can do more than one request, which is often done in ‘API’ http…

1 Like

What happens if you try wrapping the code in a begin..rescue..end block?

Still bug splats.

I’ll try that.

@thomthom Any input?

FYI, Net::HTTP blocks SketchUp, which is the reason the Sketchup::Http classes were implemented.

1 Like

Yes, but su’s implementation gives bugsplats which are difficult to determine when and why they occur. Regarding the native ruby way… the biggest problem I think with them is not that they are synchronous, but the pre 2019 heap walk bugs, causing the slowdown of several minutes one could experience https://rt.openssl.org/m/ticket/show?id=2100