Success Comunicating With External App Using TCPSocket

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.

image

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

Now… if this is the core of your app, i would eventually go with a custom ruby c++ extension, or, make it in such a way that an html dialog is an important aspect of the app, and do http in javascript.

And the OpenSSL bugs were also discussed in depth in the following API Issue tracker discussion …

… as well as at least one enhancement request to make the SketchUp API classes more usable …


Please add more fix and feature requests for these HTTP classes in SketchUp’s API.

Somewhere I thought that I had brought up the fact that SketchUp’s classes did not have any timeout controls. But I don’t see that I opened an issue for this.


@Neil_Burkholder, not sure if you realize that the Sketchup::Http::Request#start block argument is for a callback. Which I believe will not get called until the response is returned.

So I think if you wanted a monitor proc for the request, you’d need to set up a UI.start_timer block to keep testing the @request.status every so many seconds.

I also think that when I last used this, I had a @response reference initially set to nil that would get set in the @request's #start callback block. And so testing if the @response reference was “falsey” was an indicator that the request hadn’t yet generated a response.

1 Like

Since Su19 is ruby 2.5 I honestly stopped caring about them and started using the ruby default module again. You have to wait for responses, that is true, but for all the apps I worked on, it was part of the userflow, so waiting was not a problem. For this kind of app, i would go with c++ and native threads.

1 Like

Agreed. It just irks me to no end that API classes get partially done and then abandoned.

2 Likes

Above I stated ’Net::HTTP is probably the best thing to use’

There’s a lot of things that can qualify that statement. Sketchup::Http works just fine as long as one handles the response in an async manner.

In stand-alone Ruby code, I use Net::HTTP#start all the time, and often more than one request is done within the start block. I don’t think that can be done with Sketchup::Http.

Communicating between apps is messy. I’ve moved data from SU to another app using sockets, but I’ve never done any heavy bi-directional communication. Often, bi-directional implies a listener event loop of some type, and that may not work in SU.

For that I have had good experiences with a sketchup ruby timer combined with a native std::queue and a struct with a mutex locked ‘finished’ field.