Using pipes on windows does not work on 64-bit Sketchup, works on 32-bit SketchUp

Using pipes on windows fails on 64-bit SketchUp windows.
This example works perfectly on SketchUp 32-bit windows, but it fails in createPipe.Call on SketchUp 64-bit windows.
I suspect maybe wrong pointer bitsize/type. I get some sort of wrong memory entry point error (in dutch :slight_smile:
also see: CreatePipe function (namedpipeapi.h) - Win32 apps | Microsoft Learn

Any ideas what is going on and/or suggestion for a fix / workaround on 64-bit SketchUp?
Thanks in advance.

To reproduce run the following on 64-bit and 32-bit SketchUp (I tested on windows 7 and windows 10)

require 'Win32API'

NORMAL_PRIORITY_CLASS = 0x00000020
STARTUP_INFO_SIZE = 68
PROCESS_INFO_SIZE = 16
SECURITY_ATTRIBUTES_SIZE = 12

ERROR_SUCCESS = 0x00
FORMAT_MESSAGE_FROM_SYSTEM = 0x1000
FORMAT_MESSAGE_ARGUMENT_ARRAY = 0x2000

HANDLE_FLAG_INHERIT = 1
HANDLE_FLAG_PROTECT_FROM_CLOSE =2

STARTF_USESHOWWINDOW = 0x00000001
STARTF_USESTDHANDLES = 0x00000100

def raise_last_win_32_error
  errorCode = Win32API.new("kernel32", "GetLastError", [], 'L').call
  if errorCode != ERROR_SUCCESS
    params = [
        'L', # IN DWORD dwFlags,
        'P', # IN LPCVOID lpSource,
        'L', # IN DWORD dwMessageId,
        'L', # IN DWORD dwLanguageId,
        'P', # OUT LPSTR lpBuffer,
        'L', # IN DWORD nSize,
        'P', # IN va_list *Arguments
    ]

    formatMessage = Win32API.new("kernel32", "FormatMessage", params, 'L')
    msg = ' ' * 4000
    msgLength = formatMessage.call(FORMAT_MESSAGE_FROM_SYSTEM + FORMAT_MESSAGE_ARGUMENT_ARRAY, '', errorCode, 0, msg, 4000, '')
    puts msg
    msg.gsub!(/\000/, '')
    msg.strip!

    raise msg
  else
    raise 'GetLastError returned ERROR_SUCCESS'
  end
end

def create_pipe # returns read and write handle
  params = [
      'P', # pointer to read handle
      'P', # pointer to write handle
      'P', # pointer to security attributes
      'L'] # pipe size

  createPipe = Win32API.new("kernel32", "CreatePipe", params, 'I')

  read_handle, write_handle = [0].pack('I'), [0].pack('I')
  sec_attrs = [SECURITY_ATTRIBUTES_SIZE, 0, 1].pack('III')

  raise_last_win_32_error if createPipe.Call(read_handle, write_handle, sec_attrs, 0).zero?

  [read_handle.unpack('I')[0], write_handle.unpack('I')[0]]
end

create_pipe

To be complete: it uses this part from the ruby standard library:

# -*- ruby -*-
# for backward compatibility
warn "Warning:#{caller[0].sub(/:in `.*'\z/, '')}: Win32API is deprecated after Ruby 1.9.1; use dl directly instead" if $VERBOSE

require 'dl'

class Win32API
  DLL = {}
  TYPEMAP = {"0" => DL::TYPE_VOID, "S" => DL::TYPE_VOIDP, "I" => DL::TYPE_LONG}
  POINTER_TYPE = DL::SIZEOF_VOIDP == DL::SIZEOF_LONG_LONG ? 'q*' : 'l!*'

  def initialize(dllname, func, import, export = "0", calltype = :stdcall)
    @proto = [import].join.tr("VPpNnLlIi", "0SSI").sub(/^(.)0*$/, '\1')
    handle = DLL[dllname] ||= DL.dlopen(dllname)
    @func = DL::CFunc.new(handle[func], TYPEMAP[export.tr("VPpNnLlIi", "0SSI")], func, calltype)
  rescue DL::DLError => e
    raise LoadError, e.message, e.backtrace
  end

  def call(*args)
    import = @proto.split("")
    args.each_with_index do |x, i|
      args[i], = [x == 0 ? nil : x].pack("p").unpack(POINTER_TYPE) if import[i] == "S"
      args[i], = [x].pack("I").unpack("i") if import[i] == "I"
    end
    ret, = @func.call(args)
    return ret || 0
  end

  alias Call call
end

I get an error about undefined constant when running that snippet:

Error: #<NameError: uninitialized constant SECURITY_ATTRIBUTES_SIZE>
<main>:36:in `create_pipe'
<main>:43:in `<main>'
SketchUp:1:in `eval'

Can you fill the missing pieces?

Oops, sorry.
Added…

I would recommend using a different way to call this function. Either use a Ruby C Extension (I’d recommend this) or use one of the newer libs for calling system functions, such as Fiddle.

There is indeed problems with pointer sizes in that snippet. It also mixes I (ints) for P (pointer) several places.

Then there’s complications due to struct padding. For instance SECURITY_ATTRIBUTES:

        32bit    64bit    Notes
---------------------------------------
DWORD   4        4        Unsigned Long
LPVOID  4        8        Pointer
BOOL    4        4        Unsigned Int
---------------------------------------
Total   12      16

You’d think you’d just need to change the size from 12 to 16 on 64bit, right? That won’t work, because when you do this in a C/C++ app:


#include <assert.h>
#include <iostream>
#include <vector>

#include <Windows.h>


int _tmain(int argc, _TCHAR* argv[])
{
  std::cout << "DWORD: " << sizeof(DWORD) << std::endl;
  std::cout << "LPVOID: " << sizeof(LPVOID) << std::endl;
  std::cout << "BOOL: " << sizeof(BOOL) << std::endl;
  std::cout << "SECURITY_ATTRIBUTES: " << sizeof(SECURITY_ATTRIBUTES) << std::endl;

  return 0;
}

32bit Results:

DWORD: 4
LPVOID: 4
BOOL: 4
SECURITY_ATTRIBUTES: 12

64bit Results:

DWORD: 4
LPVOID: 8
BOOL: 4
SECURITY_ATTRIBUTES: 24
```

This quickly snowballs as you try to byte-pad and work out how each platform do this. (Yes, it's not consistent.)
It's probably doing somehow with the old `Win32API` module, but it's really not worth the time - it'll be time-consuming and very fragile.

TL;DR: Use a Ruby C/C++ extension instead, or try `Fiddle`.

Yes, in actuality, Win32API is alised to DL, … (edit) I see you posted the wrapper script that does this.

… and DL is aliased into the Fiddle library. (Just saying, in newer Ruby versions these wrappers may be removed.)

Have you looked at the Open3 library ?

There is also the library module PTY which can work with IO#pipe:

IO Expect extension:

Doesn’t IO::pipe work on Windows ?


Or are you wanting named pipes ?

A quick search on “pipe” at rubygems reveals this:
https://rubygems.org/gems/win32-pipe
Which is one of Daniel Berger’s extensions to his win32-api replacement for the standard Win32API class. (But you can get some ideas from the source code.)

The win32-pipe library provides an interface for named pipes on Windows. A named pipe is a named, one-way or duplex pipe for communication between the pipe server and one or more pipe clients.

:smile:

Thanks Dan!
Thanks Thomas!

We use Open3 on OSX. On Windows it also works, however, for some reason Open3 is using and causing cmd.exe to briefly popup a console window. I’ve lookup up all kind of solutions on how to launch cmd.exe without popping up. Turns out this can indeed be achieved in several ways ( I might create a new topic on that as it might be interesting to other users as well). I’d rather not bother trying to inherit from standard library Open3 and start changing it’s behaviour.

I will sure look into your other suggestions.

IO::pipe can be used indeed, but my knowledge is limited and I don’t see (yet) how I can pass and access this pipe from an external process. I’d have to find a way to pass along the handles to the pipe along the with the startup of the process. (which is what Open3 provides, but as said, with this nasty console window popup.)

IO::expect I’ve looked it up, but I had never heard of it before, and no idea how that is supposed to work.

Thomas suggested another approach: just use c-extensions. Which is what we are actually doing. from first testing yesterday, that looks promising. Basic communication is working. However, we are then somewhat forced into using Named pipes because we don’t know how to pass on a handle (cpointer) from the c-extension into ruby to access the anonymous pipe from SkethcUp’s ruby side. (Otherwise, we’d have to implement a bunch of extra stuff to transfer our data back and forth into te c- extension) And since we are lazy … :slight_smile: for now we’ll get it up and running first using named pipes, created from a c-extension. I think named pipes should be ok, although we probably have to be more careful about closing handles and stuff.

Lateran, I might still try to change the example above to make use of Fiddle instead of Win32API, but as Thomas said, it might go downhill fast as well if used on different platforms, versions etc.?

Thanks again guys, your feedback provides valuable new point of views!

Jeroen

If you go down the C/C++ route you might want to look into Poco: Class Poco::Pipe

I haven’t used it myself yet, but it’s on my list to incorporate into my extensions to take care a lot of these platform dependant file and system calls. Might save you some work.

Thanks Thomas,

Ok, I’m wrapping my mind around this.

We do have a solution working using NamedPipes created from a c-extension. We’ll use this first so we can het the rest of our code up and running asap.

However…
I’m still looking into Open3.popen though. Would be way better to use that functionality as we could then avoid unnecessary differences between mac and windows. It works on both the mac and windows, but as said on windows it pops a command window. Turns out this boils down to this one call in Open3::popen_run causing the popup:

pid = spawn(*cmd, opts)

If only I could drop in a replacement for just this particular case of using Process::spawn on windows, so that it would act exactly the same, but without relying to cmd.exe causing this popup.

Try this on Skutchup/ruby on windows as an example:
require ‘open3’
test, s = Open3.capture2e(‘ipconfig /all’)

["\nWindows IP-configuratie\n\n Hostnaam . . . . . . . . . . . . : WIN-2RTSU5RFLHI\n Primair DNS-achtervoegsel …

this gets to call this in Open3.rb which I’ve put in two putses to see what is getting into it:

  def popen_run(cmd, opts, child_io, parent_io) # :nodoc:
    puts cmd.inpsect
    puts opts
    pid = spawn(*cmd, opts)
    wait_thr = Process.detach(pid)
    child_io.each {|io| io.close }
    result = [*parent_io, wait_thr]
    if defined? yield
      begin
        return yield(*result)
      ensure
        parent_io.each{|io| io.close unless io.closed?}
        wait_thr.join
      end
    end
    result
  end
  module_function :popen_run
  class << self
    private :popen_run
  end

Now, I do have an example how I can start processes avoiding Process::spawn

  if OS == :WINDOWS
    cmd = 'ipconfig /all'
    require 'win32ole'
    objStartup = WIN32OLE.connect("winmgmts:\\\\.\\root\\cimv2:Win32_ProcessStartup")
    objConfig = objStartup.SpawnInstance_
    objConfig.ShowWindow = 0 #HIDDEN_WINDOW
    objProcess = WIN32OLE.connect("winmgmts:root\\cimv2:Win32_Process")
    errReturn = objProcess.Create(cmd, nil, objConfig, nil)

this works without popup, I’ve tested it with our own exe that does stuff. (In this example, you just don’t see the feedback from ipconfig, but it does work, set objConfig.ShowWindow = 1 to check.
NOW: I don’t (yet) see how I can extend this example to to fully mimic and replace
pid = spawn(*cmd, opts)
That is: I need the pid further along in popen and I guess also need these handles in the opts hash: {:in=>#<IO:fd 7>, [:out, :err]=>#<IO:fd 10>} ?
How do I capture the output?
…
At this point I’m just getting confused and feel I need help / new ideas.
I have a feeling all the part of the puzzle are out there, but beeing all new to this, I just don’t seem to be able make this last click and make it work.

considering Open3 is using Process::spawn which is posix compliant on osx I also found this posix compliant implementation of spawn, but no idea if this might help me in any way: GitHub - rtomayko/posix-spawn: Ruby process spawning library

Then one last thing: Dale Martens showed me just another way to prevent cmd.exe to popup:

params='/c ipconfig.exe /all > '+"\"#{tempfile}\""
shell = WIN32OLE.new('Shell.Application')
shell.ShellExecute('cmd.exe',params,'','open', 0)

…where temple is just a valid path to a text file. The trick here is in the way the /c argument is passed in.
Perhaps this anonymous pipe class could also help in a way, but at this point I’m just a bit oversaturated. hmmm.

Anyway Thomas, thanks for having a look.

Jeroen

Is the ultimate goal to capture info from ipconfig et al? MAC address?

No, the goal is to create communication between ruby and an external c application not a C extention.

If you look at the docs for Kernel.spawn() you see that the actual arguments allow an (optional) hash at position 1, setting / changing environment variables. This turns out to be like block “changer” methods that reset back to the prior state after the block. (See Dir::chdir, Dir::mktmpdir for examples.)

Ie, you can set or change environment vars in the ENV hash, and they’ll reset after the spawn call.

So, there happens to be a "RUBYSHELL" environment var that points at the command processor to use for shell commands, ie, %x strings, which use Kernel.`() [the global backtick method.] The backtick method (on the C-side) checks the "RUBYSHELL" environment var.

In the past, with regard to running shell commands via %x or Kernel.`() strings without the shell window popping up, … I played with setting

ENV["RUBYSHELL"]= "WScript.exe"

… but it seems the C-side Ruby code still appends a “/c” parameter to ALL strings passed to command processors. The issue is that the “Windows Scripting Host” (either via “WScript.exe” or “CScript.exe”) chokes on a “/c” parameter, instead of ignoring it. (It bails out and produces an "unknown parameter" error, which may be a messagebox if not run silently.)

Yesterday, I tried again,… with respect to Kernel.spawn(),… but it seemed to not have any effect. Ie, the spawn() method C code just ignores the command processor path set in ENV["RUBYSHELL"].

What I am describing really should be considered Ruby Core Issues (Bugs.)

Yes, I was going to recommend a workaround using the WIN32OLE class.

I checked the Open3 module code. It is making a call to the global spawn method, inherited via Object mixing in the Kernel module. Even though the documentation refers to “Process::spawn”, introspection also reveals the Process module just aliases Kernel::spawn within it’s singleton proxy class block.

Not exactly nitpicking here. Because the standard method of changing the way core things work for your extension, would be a refinement.

This prevents your monkey patch from effecting any other of your extensions, but more importantly, other author’s or company’s extensions.

Problem is we can only refine classes, not modules. And Kernel, Open3 and Process are all modules. So you normally cannot refine them to redefine the spawn method.

But when Ruby loads, it loads the definition of the Kernel module, and then immediately mixes it into class Object. And [ just about ] everything in Ruby is an object and is derived from class Object, inheriting all of the instance methods from class Object and module Kernel by inclusion.

So (as I explained above,) it is notable that Open3 uses the global spawn method, because your code can refine that method, in several ways at a class level.

Refinements (in Ruby v 2.0,) only work within the current file, so you can separate this little part of your extension into it’s own .rb file, and it will not effect anything else.


EXAMPLE 1: Refining class Object

module Jeroen::SomePlugin::RefinedObject
  refine ::Object do

    def spawn(*args)
      # WIN32OLE implementation
    end

  end
end

using Jeroen::SomePlugin::RefinedObject

require "Open3"

module Jeroen::SomePlugin

  test, s = Open3.capture2e('ipconfig /all')

end

EXAMPLE 2: Refining a custom class inside your plugin modules:

Open3 is actually a combination mixin / library module. If you look at the code, you’ll see all it’s methods are defined as instance methods. This is for mixing into other objects via include, prepend and extend. Then it makes itself a library module by creating singleton copies of each of these methods by calling module_function() upon each (just after defining them.) In this way it’s methods can be called from outside like a library, or mixed into any module or class.

require "Open3"

module Jeroen::SomePlugin

  class Listener
    include Open3 # mixin all Open3's methods
  end

  module RefinedSpawn
    refine Listener do

      def spawn(*args)
        # WIN32OLE implementation
      end

    end
  end

end

using Jeroen::SomePlugin::RefinedSpawn

module Jeroen::SomePlugin

  listen = Listener::new
  test, s = listen.capture2e('ipconfig /all')

end

But obviously, (looking at example 2,) since it’s your custom wrapped class, including Open3’s functionality, you don’t even need to mess with a refinement.

EXAMPLE 3: Just use a platform conditional to decide when to override your internal inherited spawn method:

require "Open3"

module Jeroen::SomePlugin

  class Listener
    include Open3 # mixin all Open3's methods
    if Sketchup::platform == :platform_win rescue RUBY_PLATFORM !~ /(darwin)/i
    # ... otherwise just leave it as is, if running on OSX.
      def spawn(*args)
        # WIN32OLE implementation
      end
    end # conditional method redefinition
  end # class Listener

  listen = Listener::new
  test, s = listen.capture2e('ipconfig /all')

end # plugin module

Thanks Dan! This is a great help!
We will surely be using this.

Just missing one more thing with the WIN32OLE solution now:

So calling Open3::capture2e(‘some_command’) results in popen_run calling Kernel.spawn passing in some crucial options; bindings for :in, :out, :err to anonymous pipes known in the local process. In this case in Hash form like you mentioned, like so:
{:in=>#<IO:fd 8>, [:out, :err]=>#<IO:fd 11>}
The Kernel.spawn() documentation you pointed me to also clarifies this.

My last remaining issue: how can I realize an equivalent to this in the WIN32OLE implementation (as mentioned in my post above)?
The example I posted uses the Create method of the Win32_Process class.
No idea how I can get the standard input, output and error on the newly created process connected to the IO:fd objects passed into from popen_run.

cmd = 'dir' #just an example here, might be 'our_app.exe -some_arguments'
require 'win32ole'
objStartup = WIN32OLE.connect("winmgmts:\\\\.\\root\\cimv2:Win32_ProcessStartup")
objConfig = objStartup.SpawnInstance_
objConfig.ShowWindow = 0 #HIDDEN_WINDOW
objProcess = WIN32OLE.connect("winmgmts:root\\cimv2:Win32_Process")
errReturn = objProcess.Create(cmd, nil, objConfig, nil)

Just to give some use-case context: we are using an application/separate process using SketchUp and Layout’s c-api’s and need it to communicate in both ways with our Skalp extension running on a live Sketchup instance. So being able to use Open3 for this on both mac and windows would really help things a lot for us in the future.

You have no idea how much I appreciate all your efforts so far. Thank you.

Jeroen

I am not sure you can (yet.) Everything I have played with, even using

shell = WIN32OLE::new("WScript.Shell")
pid = shell.Exec(cmd)

still shows the console window, if the cmd is a console io application.

But does not if cmd is a GUI application (like “calc.exe”, etc.)


Actually your ultimate goal, is to use Open3 functionality, by “tweaking” spawn.

I think you’ll get it done faster (knowing C,) by copying and “tweaking” the Ruby core C, code for your custom “Listener” class (as in Example 3 above.)

Do you have the Ruby C source for “2.0.0-p576” ?

In “io.c”, line 5605 gives:

#ifdef _WIN32
#define HAVE_SPAWNV 1
#define spawnv(mode, cmd, args) rb_w32_aspawn((mode), (cmd), (args))
#define spawn(mode, cmd) rb_w32_spawn((mode), (cmd), 0)
#endif

The aliased functions are in “win32/win32.c”:

rb_win32_spawn() line 1180

rb_win32_aspawn() line 1357
which calls:
rb_w32_aspawn_flags() line 1287

which use:
child_result() line 1058
CreatChild() line 1077
… etc.


All of these functions are preceded by the comment:

/* License: Artistic or GPL */

… which indicates this code is all originally from Perl.

I would have helped had the original Perl programmers commented it. (Maybe they have done so since the fork to Ruby ?)

For the record, here’s my test script, which is Open3.popen_run() modified:

###
    def wsh(cmd, opts = {}) # :nodoc:
      #
      # replace:
      #   pid = spawn(*cmd, opts)
      # with:
      ###
        #
        UI.messagebox("Starting WScript Shell...")
        shell = WIN32OLE::new("WScript.Shell")
        UI.messagebox("#{shell.inspect}\n\nStarting WshScriptExec...")
        #puts shell.inspect
        prog  = shell.Exec(cmd)
        pid   = prog.ProcessID
        UI.messagebox("The exec's PID: #{pid}")
        #
      ###
      wait_thr = Process.detach(pid)
      #child_io.each {|io| io.close }
      #
      parent_io = [ prog.StdIn, prog.StdOut, prog.StdErr ]
      result = [ *parent_io, wait_thr ]
      #
      if block_given?
        begin
          return yield(*result)
        ensure
          parent_io.each{|io| io.Close }
          wait_thr.join
        end
      end
      #
      result
      #
    end ###

###

However the WScript objects have differing method names than the standard Ruby IO class’ method names, so is not a direct drop in replacement.

Notice I had to change io.close to io.Close inside the ensure clauses iterator block.

ref:

I tried this example also, but keep getting Type mismatch errors for the ObjProcess.Create() call.
Ie:

Error: #<WIN32OLERuntimeError: (in OLE method `Create': )
    OLE error code:80041005 in SWbemObjectEx
      Type mismatch 
    HRESULT error code:0x80020009
      Exception occurred.>
<main>:41:in `method_missing'
<main>:41:in `obj'
<main>:in `<main>'
SketchUp:1:in `eval'

EDIT: Ok it was some wrong types passed to Create().

BUT whatever, or however I try there is no Process ID returned. And there should be.
The return is 0 for success. It is not worth it. (Better to use Fiddle.)

###
    def obj(*args)

      #{# Create Pipes: from Open3.popen3()

        if Hash === args.last
          opts = args.pop.dup
        else
          opts = {}
        end

        in_r, in_w = IO.pipe
        opts[:in] = in_r
        in_w.sync = true

        out_r, out_w = IO.pipe
        opts[:out] = out_w

        err_r, err_w = IO.pipe
        opts[:err] = err_w

        child_io  = [in_r, out_w, err_w]
        parent_io = [in_w, out_r, err_r]

      #}#
      
      #{# Handle Environment Var hash as first argument:
        if Hash === args.first
          env = args.shift
        else
          env = {}
        end
        if !env.empty?
          prev = {}
          env.each_pair {|key,val|
            if (val.nil? && !ENV[key].nil? ) || val != ENV[key]
              prev[key]= ENV[key] # save to restore
            end
            ENV[key]= val # temporary settings
          }
        end

      #}#

      #{# Convert args array to a command string
        cmd = args.join(' ')
      #}#

      #{# Replace call to global Kernel.spawn()
      
        ### replacing:
        #
        #   pid = spawn(*cmd, opts)
        #
        ### with:
        #
        require 'win32ole'
        #
        # https://msdn.microsoft.com/en-us/library/aa392758(v=vs.85).aspx
        privylist = "ProfileSingleProcess,SystemProfile,Tcb"
        privilege = "{impersonationLevel=impersonate, (#{privylist})}!"
        #
        puts "\nStarting Process Object ..."
        objStartup = WIN32OLE.connect(
          "winmgmts:#{privilege}\\\\.\\root\\cimv2:Win32_ProcessStartup"
        )
          #"winmgmts:#{privilege}root\\cimv2:Win32_ProcessStartup"
        #)
        puts "objStartup.inspect: #{objStartup.inspect}"
        puts "\nConfiguring Process..."
        # Configuring Process:
        objConfig = objStartup.SpawnInstance_
        #objConfig.CreateFlags = 0x200 # New Process group
        objConfig.ShowWindow = 0 # HIDDEN_WINDOW
        #
        objProcess = WIN32OLE.connect("winmgmts:#{privilege}root\\cimv2:Win32_Process")
        #errReturn  = objProcess.Create(cmd, nil, objConfig, nil) # gives error
        #
        # Create a reference for the returned Process ID number:
        # (Create wants a uint32 on C-side. Assuming VT_UI4 is 4byte Unsigned Int.)
        #@pid = WIN32OLE_VARIANT::new(0,WIN32OLE::VARIANT::VT_UI4)
        #@pid = 0 #WIN32OLE_VARIANT::new(0,WIN32OLE::VARIANT::VT_UINT)
        #
        @buf = [0].pack("V")
        #
        if opts[:chdir]
          process_dir = WIN32OLE_VARIANT::new(opts[:chdir])
        else
          process_dir = nil #WIN32OLE::VARIANT::VT_NULL --> causes unknown error 8
        end
        #
        ole_string = WIN32OLE_VARIANT::new( cmd ) # WIN32OLE::VARIANT::VT_BSTR
        #
        errReturn = objProcess.Create(
          ole_string, 
          process_dir,
          objConfig,
          @buf # returned Process ID number always 0 !! WHY ??
        )
        #
        pid = objProcess.ProcessId() # returning nil !! Why ?
        #
        if errReturn == 0
          puts "The Process.Create call successfully returned : #{errReturn.to_s}"
        else
          puts "The Process.Create call returned error code : #{errReturn.to_s}"
        end
        # Successful completion (0)
        # Access denied (2)
        # Insufficient privilege (3)
        # Unknown failure (8)
        # Path not found (9)
        # Invalid parameter (21)
        # Other (22–4294967295)

        @pid = @buf.unpack("V")[0]
        puts "The Process Object's PID: #{@pid}"
        
        puts "The Process Object's PID Information:"
        puts "  inspect: #{objProcess.ProcessId().inspect}"
        puts "    class: #{objProcess.ProcessId().class}"
        puts "      pid: #{objProcess.ProcessId()}"

      #}#
      
      #{# From here, down to rescue, is the same as Open3.popen_run():
      
        wait_thr = Process.detach(@pid)
        child_io.each {|io| io.close }
        #
        result = [ *parent_io, wait_thr ]
        #
        if block_given?
          begin
            return yield(*result)
          ensure
            parent_io.each{|io| io.close unless io.closed? }
            wait_thr.join
          end
        end
        #
        return result

      #}#

      #{#
    rescue => e
      #
      puts e.inspect
      raise
      #
    else
      #
      #
      #
    ensure # that the ENV hash is restored:
      #
      if !env.empty? && !prev.empty?
        prev.each_pair {|key,val|
          ENV[key]= val # restore variables
        }
      end
      #}#
    end ###

###

Came across a gem called Open4 that explicitly keeps a PID reference to the created child process:

It is available at Rubygems

Gem::Install("open4")

All very interesting stuff Dan.

I was out with my family these last few days. I’ll be looking into your feedback now.

“Actually your ultimate goal, is to use Open3 functionality, by “tweaking” spawn.”
Indeed!

Last week I was fiddling with Fiddle…
And I was looking into ‘childprocess’ GitHub - enkessler/childprocess: Cross-platform Ruby library for managing child processes.
But that one uses ‘ffi’ (foreign function interface) GitHub - ffi/ffi: Ruby FFI
I thought maybe I could adapt that to use Fiddle instead.
And then there is one last thing:
a posix compatible spawn implementation.
GitHub - rtomayko/posix-spawn: Ruby process spawning library
Maybe I could just have a look at part of its implementation on windows…

Not sure about the open4 gem, I’ll have a look though.

Jeroen

Fiddle is a Ruby wrapper around “libffi-6.dll”. (Fiddle is a replacement for the Ruby FFI gem, which is no longer maintained.)

So it should be relatively easy to translate to using Fiddle.

After trying almost everything we discovered the popen3 function on ruby on windows works perfect, but when we use the popen3 function on SketchUp the stdout and stderr are lost. Probably this have something to do with how the SketchUp ruby console works on windows. Can anybody help us how we can fix this?

The following example works perfect on standard ruby, but gives no result we you run this on SketchUp:

require 'open3'

command1 = 'dir /B'
command2 = 'sort /R'

reader,writer = IO.pipe
Open3.popen3(command1) {|stdin, stdout, stderr, wait_thr|
  writer.write stdout.read
}
writer.close

stdout, stderr, status = Open3.capture3(command2, :stdin_data => reader.read)
reader.close

puts "status: #{status}"   #pid and exit code
puts "stderr: #{stderr}"   #use this to debug command2 errors
puts "stdout: #{stdout}"