right_popen 1.0.2-x86-mswin32-60 → 1.0.5-x86-mswin32-60

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/README.rdoc CHANGED
@@ -41,8 +41,13 @@ to report issues.
41
41
 
42
42
  EM.run do
43
43
  EM.next_tick do
44
- cmd = "ruby -e \"puts 'some stdout text'; $stderr.puts 'some stderr text'\; exit 99\""
45
- RightScale.popen3(cmd, self, :on_read_stdout, :on_read_stderr, :on_exit)
44
+ command = "ruby -e \"puts 'some stdout text'; $stderr.puts 'some stderr text'\; exit 99\""
45
+ RightScale.popen3(:command => command,
46
+ :target => self,
47
+ :environment => nil,
48
+ :stdout_handler => :on_read_stdout,
49
+ :stderr_handler => :on_read_stderr,
50
+ :exit_handler => :on_exit)
46
51
  end
47
52
  timer = EM::PeriodicTimer.new(0.1) do
48
53
  if @exit_status
@@ -90,7 +95,7 @@ The build can be tested using the RSpec gem. Create a link to the installed
90
95
  under Windows) and run the following command from the gem directory to execute
91
96
  the RightPopen tests:
92
97
 
93
- spec spec/right_popen_spec.rb
98
+ rake spec
94
99
 
95
100
 
96
101
  == LICENSE
data/Rakefile CHANGED
@@ -46,3 +46,8 @@ task :reinstall_gem do
46
46
  sh "gem uninstall right_popen"
47
47
  sh "rake install_gem"
48
48
  end
49
+
50
+ desc 'Runs all spec tests'
51
+ task :spec do
52
+ sh "spec spec/*_spec.rb"
53
+ end
@@ -1,24 +1,24 @@
1
- ///////////////////////////////////////////////////////////////////////////////
2
- // Copyright (c) 2010 RightScale Inc
3
- //
4
- // Permission is hereby granted, free of charge, to any person obtaining
5
- // a copy of this software and associated documentation files (the
6
- // "Software"), to deal in the Software without restriction, including
7
- // without limitation the rights to use, copy, modify, merge, publish,
8
- // distribute, sublicense, and/or sell copies of the Software, and to
9
- // permit persons to whom the Software is furnished to do so, subject to
10
- // the following conditions:
11
- //
12
- // The above copyright notice and this permission notice shall be
13
- // included in all copies or substantial portions of the Software.
14
- //
15
- // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16
- // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
17
- // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18
- // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
19
- // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
20
- // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
21
- // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
1
+ ///////////////////////////////////////////////////////////////////////////////
2
+ // Copyright (c) 2010 RightScale Inc
3
+ //
4
+ // Permission is hereby granted, free of charge, to any person obtaining
5
+ // a copy of this software and associated documentation files (the
6
+ // "Software"), to deal in the Software without restriction, including
7
+ // without limitation the rights to use, copy, modify, merge, publish,
8
+ // distribute, sublicense, and/or sell copies of the Software, and to
9
+ // permit persons to whom the Software is furnished to do so, subject to
10
+ // the following conditions:
11
+ //
12
+ // The above copyright notice and this permission notice shall be
13
+ // included in all copies or substantial portions of the Software.
14
+ //
15
+ // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16
+ // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
17
+ // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18
+ // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
19
+ // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
20
+ // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
21
+ // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22
22
  ///////////////////////////////////////////////////////////////////////////////
23
23
 
24
24
  #include "right_popen.h"
@@ -60,6 +60,7 @@ static const DWORD CHILD_PROCESS_EXIT_WAIT_MSECS = 500; // 0.5 secs
60
60
 
61
61
  static Open3ProcessData* win32_process_data_list = NULL;
62
62
  static DWORD win32_named_pipe_serial_number = 1;
63
+ static HMODULE hUserEnvLib = NULL;
63
64
 
64
65
  // Summary:
65
66
  // allocates a new Ruby I/O object.
@@ -219,6 +220,10 @@ static VALUE right_popen_close_io_array(VALUE vRubyIoObjectArray)
219
220
  // bShowWindow
220
221
  // true if process window is initially visible, false if process has no UI or is invisible
221
222
  //
223
+ // pszEnvironmentStrings
224
+ // enviroment strings in a double-nul terminated "str1\0str2\0...\0"
225
+ // block to give child process or NULL.
226
+ //
222
227
  // Returns:
223
228
  // true if successful, false otherwise (call GetLastError() for more information)
224
229
  static BOOL win32_create_process(char* szCommand,
@@ -227,10 +232,12 @@ static BOOL win32_create_process(char* szCommand,
227
232
  HANDLE hStderr,
228
233
  HANDLE* phProcess,
229
234
  rb_pid_t* pPid,
230
- BOOL bShowWindow)
235
+ BOOL bShowWindow,
236
+ char* pszEnvironmentStrings)
231
237
  {
232
238
  PROCESS_INFORMATION pi;
233
239
  STARTUPINFO si;
240
+ BOOL bResult = FALSE;
234
241
 
235
242
  ZeroMemory(&si, sizeof(STARTUPINFO));
236
243
 
@@ -246,7 +253,7 @@ static BOOL win32_create_process(char* szCommand,
246
253
  NULL,
247
254
  TRUE,
248
255
  0,
249
- NULL,
256
+ pszEnvironmentStrings,
250
257
  NULL,
251
258
  &si,
252
259
  &pi))
@@ -256,11 +263,11 @@ static BOOL win32_create_process(char* szCommand,
256
263
 
257
264
  // Return process handle
258
265
  *phProcess = pi.hProcess;
259
- *pPid = (rb_pid_t)pi.dwProcessId;
260
- return TRUE;
266
+ *pPid = (rb_pid_t)pi.dwProcessId;
267
+ bResult = TRUE;
261
268
  }
262
269
 
263
- return FALSE;
270
+ return bResult;
264
271
  }
265
272
 
266
273
  // Summary:
@@ -710,12 +717,16 @@ static VALUE ruby_create_io_object(rb_pid_t pid, int iFileMode, HANDLE hFile, BO
710
717
  // false to read synchronously, true to read asynchronously. see
711
718
  // also RightPopen::async_read() (defaults to Qfalse).
712
719
  //
720
+ // pszEnvironmentStrings
721
+ // enviroment strings in a double-nul terminated "str1\0str2\0...\0"
722
+ // block to give child process or NULL.
723
+ //
713
724
  // Returns:
714
725
  // a Ruby array containing [stdin write, stdout read, stderr read, pid]
715
726
  //
716
727
  // Throws:
717
728
  // raises a Ruby RuntimeError on failure
718
- static VALUE win32_popen4(char* szCommand, int iMode, BOOL bShowWindow, BOOL bAsynchronousOutput)
729
+ static VALUE win32_popen4(char* szCommand, int iMode, BOOL bShowWindow, BOOL bAsynchronousOutput, char* pszEnvironmentStrings)
719
730
  {
720
731
  VALUE vReturnArray = Qnil;
721
732
  HANDLE hProcess = NULL;
@@ -733,7 +744,8 @@ static VALUE win32_popen4(char* szCommand, int iMode, BOOL bShowWindow, BOOL bAs
733
744
  pData->childStderrPair.hWrite,
734
745
  &hProcess,
735
746
  &pid,
736
- bShowWindow))
747
+ bShowWindow,
748
+ pszEnvironmentStrings))
737
749
  {
738
750
  DWORD dwLastError = GetLastError();
739
751
  win32_free_process_data(pData);
@@ -819,10 +831,12 @@ static VALUE right_popen_popen4(int argc, VALUE *argv, VALUE klass)
819
831
  VALUE vReturnArray = Qnil;
820
832
  VALUE vShowWindowFlag = Qfalse;
821
833
  VALUE vAsynchronousOutputFlag = Qfalse;
834
+ VALUE vEnvironmentStrings = Qnil;
822
835
  int iMode = 0;
823
836
  char* mode = "t";
837
+ char* pszEnvironmentStrings = NULL;
824
838
 
825
- rb_scan_args(argc, argv, "13", &vCommand, &vMode, &vShowWindowFlag, &vAsynchronousOutputFlag);
839
+ rb_scan_args(argc, argv, "14", &vCommand, &vMode, &vShowWindowFlag, &vAsynchronousOutputFlag, &vEnvironmentStrings);
826
840
 
827
841
  if (!NIL_P(vMode))
828
842
  {
@@ -840,11 +854,16 @@ static VALUE right_popen_popen4(int argc, VALUE *argv, VALUE klass)
840
854
  {
841
855
  iMode = _O_BINARY;
842
856
  }
857
+ if (!NIL_P(vEnvironmentStrings))
858
+ {
859
+ pszEnvironmentStrings = StringValuePtr(vEnvironmentStrings);
860
+ }
843
861
 
844
862
  vReturnArray = win32_popen4(StringValuePtr(vCommand),
845
863
  iMode,
846
864
  Qfalse != vShowWindowFlag,
847
- Qfalse != vAsynchronousOutputFlag);
865
+ Qfalse != vAsynchronousOutputFlag,
866
+ pszEnvironmentStrings);
848
867
 
849
868
  // ensure handles are closed in block form.
850
869
  if (rb_block_given_p())
@@ -985,6 +1004,193 @@ static VALUE right_popen_async_read(VALUE vSelf, VALUE vRubyIoObject)
985
1004
  }
986
1005
  }
987
1006
 
1007
+ // Summary:
1008
+ // scans the given nul-terminated block of nul-terminated Unicode strings to
1009
+ // convert the block from Unicode to Multi-Byte and determine the correct
1010
+ // length of the converted block. the converted block is then used to create
1011
+ // a Ruby string value containing multiple nul-terminated strings. in Ruby,
1012
+ // the block must be scanned again using index(0.chr, ...) logic.
1013
+ //
1014
+ // Returns:
1015
+ // a Ruby string representing the environment block
1016
+ static VALUE win32_unicode_environment_block_to_ruby(const void* pvEnvironmentBlock)
1017
+ {
1018
+ const WCHAR* pszStart = (const WCHAR*)pvEnvironmentBlock;
1019
+ const WCHAR* pszEnvString = pszStart;
1020
+
1021
+ VALUE vResult = Qnil;
1022
+
1023
+ while (*pszEnvString != 0)
1024
+ {
1025
+ const int iEnvStringLength = wcslen(pszEnvString);
1026
+
1027
+ pszEnvString += iEnvStringLength + 1;
1028
+ }
1029
+
1030
+ // convert from wide to multi-byte.
1031
+ {
1032
+ int iBlockLength = (int)(pszEnvString - pszStart);
1033
+ DWORD dwBufferLength = WideCharToMultiByte(CP_ACP, 0, pszStart, iBlockLength, NULL, 0, NULL, NULL);
1034
+ char* pszBuffer = (char*)malloc(dwBufferLength + 2);
1035
+
1036
+ // FIX: the Ruby kernel appears to use the CP_ACP code page for
1037
+ // in-memory conversion, but I still have not seen a definitive
1038
+ // statement on which code page should be used.
1039
+ ZeroMemory(pszBuffer, dwBufferLength + 2);
1040
+ WideCharToMultiByte(CP_ACP, 0, pszStart, iBlockLength, pszBuffer, dwBufferLength + 2, NULL, NULL);
1041
+ vResult = rb_str_new(pszBuffer, dwBufferLength + 1);
1042
+ free(pszBuffer);
1043
+ pszBuffer = NULL;
1044
+ }
1045
+
1046
+ return vResult;
1047
+ }
1048
+
1049
+ // Summary:
1050
+ // scans the given nul-terminated block of nul-terminated Multi-Byte strings
1051
+ // to determine the correct length of the converted block. the converted block
1052
+ // is then used to create a Ruby string value containing multiple nul-
1053
+ // terminated strings. in Ruby, the block must be scanned again using
1054
+ // index(0.chr, ...) logic.
1055
+ //
1056
+ // Returns:
1057
+ // a Ruby string representing the environment block
1058
+ static VALUE win32_multibyte_environment_block_to_ruby(const void* pvEnvironmentBlock)
1059
+ {
1060
+ const char* pszStart = (const char*)pvEnvironmentBlock;
1061
+ const char* pszEnvString = pszStart;
1062
+
1063
+ while (*pszEnvString != 0)
1064
+ {
1065
+ const int iEnvStringLength = strlen(pszEnvString);
1066
+
1067
+ pszEnvString += iEnvStringLength + 1;
1068
+ }
1069
+
1070
+ // convert from wide to multi-byte.
1071
+ {
1072
+ int iBlockLength = (int)(pszEnvString - pszStart);
1073
+
1074
+ return rb_str_new(pszStart, iBlockLength + 1);
1075
+ }
1076
+ }
1077
+
1078
+ // Summary:
1079
+ // gets the environment strings from the registry for the current thread/process user.
1080
+ //
1081
+ // Returns:
1082
+ // nul-terminated block of nul-terminated environment strings as a Ruby string value.
1083
+ static VALUE right_popen_get_current_user_environment(VALUE vSelf)
1084
+ {
1085
+ typedef BOOL (STDMETHODCALLTYPE FAR * LPFN_CREATEENVIRONMENTBLOCK)(LPVOID* lpEnvironment, HANDLE hToken, BOOL bInherit);
1086
+ typedef BOOL (STDMETHODCALLTYPE FAR * LPFN_DESTROYENVIRONMENTBLOCK)(LPVOID lpEnvironment);
1087
+
1088
+ HANDLE hToken = NULL;
1089
+
1090
+ // dynamically load "userenv.dll" once (because the MSVC 6.0 compiler
1091
+ // doesn't have the .h or .lib for it).
1092
+ if (NULL == hUserEnvLib)
1093
+ {
1094
+ // note we will intentionally leave library loaded for efficiency
1095
+ // reasons even though it is proper to call FreeLibrary().
1096
+ hUserEnvLib = LoadLibrary("userenv.dll");
1097
+ if (NULL == hUserEnvLib)
1098
+ {
1099
+ rb_raise(rb_eRuntimeError, "LoadLibrary() failed: %s", win32_error_description(GetLastError()));
1100
+ }
1101
+ }
1102
+
1103
+ // get the calling thread's access token.
1104
+ if (FALSE == OpenThreadToken(GetCurrentThread(), TOKEN_QUERY, TRUE, &hToken))
1105
+ {
1106
+ switch (GetLastError())
1107
+ {
1108
+ case ERROR_NO_TOKEN:
1109
+ // retry against process token if no thread token exists.
1110
+ if (FALSE == OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &hToken))
1111
+ {
1112
+ rb_raise(rb_eRuntimeError, "OpenProcessToken() failed: %s", win32_error_description(GetLastError()));
1113
+ }
1114
+ break;
1115
+ default:
1116
+ rb_raise(rb_eRuntimeError, "OpenThreadToken() failed: %s", win32_error_description(GetLastError()));
1117
+ }
1118
+ }
1119
+
1120
+ // get user's environment (from registry) without inheriting from the
1121
+ // current process environment.
1122
+ {
1123
+ LPVOID lpEnvironment = NULL;
1124
+ BOOL bResult = FALSE;
1125
+ {
1126
+ LPFN_CREATEENVIRONMENTBLOCK lpfnCreateEnvironmentBlock = (LPFN_CREATEENVIRONMENTBLOCK)GetProcAddress(hUserEnvLib, "CreateEnvironmentBlock");
1127
+
1128
+ if (NULL != lpfnCreateEnvironmentBlock)
1129
+ {
1130
+ bResult = lpfnCreateEnvironmentBlock(&lpEnvironment, hToken, FALSE);
1131
+ }
1132
+ }
1133
+
1134
+ // check result.
1135
+ {
1136
+ DWORD dwLastError = GetLastError();
1137
+
1138
+ CloseHandle(hToken);
1139
+ hToken = NULL;
1140
+ SetLastError(dwLastError);
1141
+ if (FALSE == bResult || NULL == lpEnvironment)
1142
+ {
1143
+ rb_raise(rb_eRuntimeError, "OpenThreadToken() failed: %s", win32_error_description(GetLastError()));
1144
+ }
1145
+ }
1146
+
1147
+ // merge environment.
1148
+ //
1149
+ // note that there is only a unicode form of this API call (instead of
1150
+ // the usual _A and _W pair) and that the environment strings appear to
1151
+ // always be Unicode (which the docs only hint at indirectly).
1152
+ {
1153
+ VALUE value = win32_unicode_environment_block_to_ruby(lpEnvironment);
1154
+ LPFN_DESTROYENVIRONMENTBLOCK lpfnDestroyEnvironmentBlock = (LPFN_DESTROYENVIRONMENTBLOCK)GetProcAddress(hUserEnvLib, "DestroyEnvironmentBlock");
1155
+
1156
+ if (NULL != lpfnDestroyEnvironmentBlock)
1157
+ {
1158
+ lpfnDestroyEnvironmentBlock(lpEnvironment);
1159
+ lpEnvironment = NULL;
1160
+ }
1161
+ CloseHandle(hToken);
1162
+ hToken = NULL;
1163
+
1164
+ return value;
1165
+ }
1166
+ }
1167
+ }
1168
+
1169
+ // Summary:
1170
+ // gets the environment strings for the current process.
1171
+ //
1172
+ // Returns:
1173
+ // nul-terminated block of nul-terminated environment strings as a Ruby string value.
1174
+ static VALUE right_popen_get_process_environment(VALUE vSelf)
1175
+ {
1176
+ char* lpEnvironment = GetEnvironmentStringsA();
1177
+
1178
+ if (NULL == lpEnvironment)
1179
+ {
1180
+ rb_raise(rb_eRuntimeError, "GetEnvironmentStringsA() failed: %s", win32_error_description(GetLastError()));
1181
+ }
1182
+
1183
+ // create a Ruby string from block.
1184
+ {
1185
+ VALUE value = win32_multibyte_environment_block_to_ruby(lpEnvironment);
1186
+
1187
+ FreeEnvironmentStrings(lpEnvironment);
1188
+ lpEnvironment = NULL;
1189
+
1190
+ return value;
1191
+ }
1192
+ }
1193
+
988
1194
  // Summary:
989
1195
  // 'RightPopen' module entry point
990
1196
  void Init_right_popen()
@@ -993,4 +1199,6 @@ void Init_right_popen()
993
1199
 
994
1200
  rb_define_module_function(vModule, "popen4", (VALUE(*)(ANYARGS))right_popen_popen4, -1);
995
1201
  rb_define_module_function(vModule, "async_read", (VALUE(*)(ANYARGS))right_popen_async_read, 1);
1202
+ rb_define_module_function(vModule, "get_current_user_environment", (VALUE(*)(ANYARGS))right_popen_get_current_user_environment, 0);
1203
+ rb_define_module_function(vModule, "get_process_environment", (VALUE(*)(ANYARGS))right_popen_get_process_environment, 0);
996
1204
  }
@@ -1,24 +1,24 @@
1
- ///////////////////////////////////////////////////////////////////////////////
2
- // Copyright (c) 2010 RightScale Inc
3
- //
4
- // Permission is hereby granted, free of charge, to any person obtaining
5
- // a copy of this software and associated documentation files (the
6
- // "Software"), to deal in the Software without restriction, including
7
- // without limitation the rights to use, copy, modify, merge, publish,
8
- // distribute, sublicense, and/or sell copies of the Software, and to
9
- // permit persons to whom the Software is furnished to do so, subject to
10
- // the following conditions:
11
- //
12
- // The above copyright notice and this permission notice shall be
13
- // included in all copies or substantial portions of the Software.
14
- //
15
- // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16
- // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
17
- // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18
- // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
19
- // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
20
- // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
21
- // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
1
+ ///////////////////////////////////////////////////////////////////////////////
2
+ // Copyright (c) 2010 RightScale Inc
3
+ //
4
+ // Permission is hereby granted, free of charge, to any person obtaining
5
+ // a copy of this software and associated documentation files (the
6
+ // "Software"), to deal in the Software without restriction, including
7
+ // without limitation the rights to use, copy, modify, merge, publish,
8
+ // distribute, sublicense, and/or sell copies of the Software, and to
9
+ // permit persons to whom the Software is furnished to do so, subject to
10
+ // the following conditions:
11
+ //
12
+ // The above copyright notice and this permission notice shall be
13
+ // included in all copies or substantial portions of the Software.
14
+ //
15
+ // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16
+ // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
17
+ // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18
+ // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
19
+ // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
20
+ // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
21
+ // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22
22
  ///////////////////////////////////////////////////////////////////////////////
23
23
 
24
24
  #include "ruby.h"
data/lib/right_popen.rb CHANGED
@@ -1,4 +1,4 @@
1
- #
1
+ #--
2
2
  # Copyright (c) 2009 RightScale Inc
3
3
  #
4
4
  # Permission is hereby granted, free of charge, to any person obtaining
@@ -19,7 +19,7 @@
19
19
  # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
20
20
  # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
21
21
  # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22
- #
22
+ #++
23
23
 
24
24
  # RightScale.popen3 allows running external processes aynchronously
25
25
  # while still capturing their standard and error outputs.
@@ -30,3 +30,36 @@ if RUBY_PLATFORM =~ /mswin/
30
30
  else
31
31
  require File.expand_path(File.join(File.dirname(__FILE__), 'linux', 'right_popen'))
32
32
  end
33
+
34
+ module RightScale
35
+
36
+ # Spawn process to run given command asynchronously, hooking all three
37
+ # standard streams of the child process.
38
+ #
39
+ # Streams the command's stdout and stderr to the given handlers. Time-
40
+ # ordering of bytes sent to stdout and stderr is not preserved.
41
+ #
42
+ # Calls given exit handler upon command process termination, passing in the
43
+ # resulting Process::Status.
44
+ #
45
+ # All handlers must be methods exposed by the given target.
46
+ #
47
+ # === Parameters
48
+ # options[:command](String):: Command to execute, including any arguments
49
+ # options[:environment](Hash):: Hash of environment variables values keyed by name
50
+ # options[:target](Object):: object defining handler methods to be called, optional (no handlers can be defined if not specified)
51
+ # options[:stdout_handler](String):: Stdout handler method name, optional
52
+ # options[:stderr_handler](String):: Stderr handler method name, optional
53
+ # options[:exit_handler](String):: Exit handler method name, optional
54
+ #
55
+ # === Returns
56
+ # true:: Always returns true
57
+ def self.popen3(options)
58
+ raise "EventMachine reactor must be started" unless EM.reactor_running?
59
+ raise "Missing command" unless options[:command]
60
+ raise "Missing target" unless options[:target] || !options[:stdout_handler] && !options[:stderr_handler] && !options[:exit_handler]
61
+ RightScale.popen3_imp(options)
62
+ true
63
+ end
64
+
65
+ end
@@ -1,4 +1,4 @@
1
- #
1
+ #--
2
2
  # Copyright (c) 2009 RightScale Inc
3
3
  #
4
4
  # Permission is hereby granted, free of charge, to any person obtaining
@@ -19,17 +19,13 @@
19
19
  # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
20
20
  # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
21
21
  # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22
- #
23
-
24
- # RightScale.popen3 allows running external processes aynchronously
25
- # while still capturing their standard and error outputs.
26
- # It relies on EventMachine for most of its internal mechanisms.
22
+ #++
27
23
 
28
24
  require 'rubygems'
29
25
  begin
30
26
  gem 'eventmachine', '=0.12.8.1' # patched version for Windows-only socket close fix
31
27
  rescue Gem::LoadError
32
- gem 'eventmachine', '=0.12.8' # notify_readable is deprecated in 0.12.10
28
+ gem 'eventmachine', '=0.12.8' # notify_readable is deprecated, so currently cannot use >=0.12.10 in Windows gem
33
29
  end
34
30
  require 'eventmachine'
35
31
  require 'win32/process'
@@ -81,15 +77,10 @@ module RightScale
81
77
 
82
78
  # === Parameters
83
79
  # target(Object):: Object defining handler methods to be called.
84
- #
85
80
  # stdout_handler(String):: Token for stdout handler method name.
86
- #
87
81
  # exit_handler(String):: Token for exit handler method name.
88
- #
89
82
  # stderr_eventable(Connector):: EM object representing stderr handler.
90
- #
91
83
  # stream_out(IO):: Standard output stream.
92
- #
93
84
  # pid(Integer):: Child process ID.
94
85
  def initialize(target, stdout_handler, exit_handler, stderr_eventable, stream_out, pid)
95
86
  @target = target
@@ -186,34 +177,20 @@ module RightScale
186
177
  # Creates a child process and connects event handlers to the standard output
187
178
  # and error streams used by the created process. Connectors use named pipes
188
179
  # and asynchronous I/O in the native Windows implementation.
189
- #
190
- # Streams the command's stdout and stderr to the given handlers. Time-
191
- # ordering of bytes sent to stdout and stderr is not preserved.
192
- #
193
- # Calls given exit handler upon command process termination, passing in the
194
- # resulting Process::Status.
195
- #
196
- # All handlers must be methods exposed by the given target.
197
180
  #
198
- # === Parameters
199
- # cmd(String): command to execute, including any arguments.
200
- #
201
- # target(Object): object defining handler methods to be called.
202
- #
203
- # stdout_handler(String): token for stdout handler method name.
204
- #
205
- # stderr_handler(String): token for stderr handler method name.
206
- #
207
- # exit_handler(String): token for exit handler method name.
208
- def self.popen3(cmd, target, stdout_handler = nil, stderr_handler = nil, exit_handler = nil)
181
+ # See RightScale.popen3
182
+ def self.popen3_imp(options)
209
183
  raise "EventMachine reactor must be started" unless EM.reactor_running?
210
184
 
211
- # launch cmd and request asynchronous output (which is only provided by
212
- # the RightScale version of win32/open3 gem).
185
+ # merge and format environment strings, if necessary.
186
+ environment_hash = options[:environment] || {}
187
+ environment_strings = RightPopenEx.merge_environment(environment_hash)
188
+
189
+ # launch cmd and request asynchronous output.
213
190
  mode = "t"
214
191
  show_window = false
215
192
  asynchronous_output = true
216
- stream_in, stream_out, stream_err, pid = RightPopen.popen4(cmd, mode, show_window, asynchronous_output)
193
+ stream_in, stream_out, stream_err, pid = RightPopen.popen4(options[:command], mode, show_window, asynchronous_output, environment_strings)
217
194
 
218
195
  # close input immediately.
219
196
  stream_in.close
@@ -221,8 +198,8 @@ module RightScale
221
198
  # attach handlers to event machine and let it monitor incoming data. the
222
199
  # streams aren't used directly by the connectors except that they are closed
223
200
  # on unbind.
224
- stderr_eventable = EM.attach(stream_err, StdErrHandler, target, stderr_handler, stream_err) if stderr_handler
225
- EM.attach(stream_out, StdOutHandler, target, stdout_handler, exit_handler, stderr_eventable, stream_out, pid)
201
+ stderr_eventable = EM.attach(stream_err, StdErrHandler, options[:target], options[:stderr_handler], stream_err) if options[:stderr_handler]
202
+ EM.attach(stream_out, StdOutHandler, options[:target], options[:stdout_handler], options[:exit_handler], stderr_eventable, stream_out, pid)
226
203
 
227
204
  # note that control returns to the caller, but the launched cmd continues
228
205
  # running and sends output to the handlers. the caller is not responsible
@@ -231,4 +208,222 @@ module RightScale
231
208
  # sent to the exit_handler on process termination.
232
209
  end
233
210
 
211
+ protected
212
+
213
+ module RightPopenEx
214
+ # Key class for case-insensitive hash insertion/lookup.
215
+ class NoCaseKey
216
+ # Internal key
217
+ attr_reader :key
218
+
219
+ # Stringizes object to be used as key
220
+ def initialize key
221
+ @key = key.to_s
222
+ end
223
+
224
+ # Hash code
225
+ def hash
226
+ @key.downcase.hash
227
+ end
228
+
229
+ # Equality for hash
230
+ def eql? other
231
+ @key.downcase.hash == other.key.downcase.hash
232
+ end
233
+
234
+ # Sort operator
235
+ def <=> other
236
+ @key.downcase <=> other.key.downcase
237
+ end
238
+
239
+ # Stringizer
240
+ def to_s
241
+ @key
242
+ end
243
+
244
+ # Inspector
245
+ def inspect
246
+ "\"#{@key}\""
247
+ end
248
+ end
249
+
250
+ # Hash of known environment variable keys to special merge method proc.
251
+ SPECIAL_MERGE_ENV_KEY_HASH = {
252
+ NoCaseKey.new('PATH') => lambda { |from_value, to_value| merge_environment_path_value(from_value, to_value) }
253
+ }
254
+
255
+ # Merges the given environment hash with the current environment for this
256
+ # process and the current environment for the current thread user from the
257
+ # registry. The result is a nul-terminated block of nul-terminated strings
258
+ # suitable for use in creating the child process.
259
+ #
260
+ # === Parameters
261
+ # environment_hash(Hash):: Hash of environment key/value pairs or empty to
262
+ # only merge the current process and currend thread user environment.
263
+ #
264
+ # === Returns
265
+ # merged string block
266
+ def self.merge_environment(environment_hash)
267
+ current_user_environment_hash = get_current_user_environment
268
+ result_environment_hash = get_process_environment
269
+
270
+ # user environment from registry supercedes process.
271
+ merge_environment2(current_user_environment_hash, result_environment_hash)
272
+
273
+ # caller's environment supercedes all.
274
+ merge_environment2(environment_hash, result_environment_hash)
275
+
276
+ return environment_hash_to_string_block(result_environment_hash)
277
+ end
278
+
279
+ # Merges from hash to another with special handling for known env vars.
280
+ #
281
+ # === Parameters
282
+ # from_hash(Hash):: hash of string or environment keys to environment values
283
+ # to_hash(Hash):: resulting hash or environment keys to environment values
284
+ #
285
+ # === Returns
286
+ # to_hash(Hash):: merged 'to' hash
287
+ def self.merge_environment2(from_hash, to_hash)
288
+ from_hash.each do |from_key, from_value|
289
+ to_key = from_key.kind_of?(NoCaseKey) ?
290
+ from_key :
291
+ NoCaseKey.new(from_key)
292
+ to_value = to_hash[to_key]
293
+ if to_value
294
+ special_merge_proc = SPECIAL_MERGE_ENV_KEY_HASH[to_key]
295
+ if special_merge_proc
296
+ # special merge
297
+ to_hash[to_key] = special_merge_proc.call(from_value, to_value)
298
+ else
299
+ # 'from' value supercedes existing 'to' value
300
+ to_hash[to_key] = from_value
301
+ end
302
+ else
303
+ # 'from' value replaces missing 'to' value
304
+ to_hash[to_key] = from_value
305
+ end
306
+ end
307
+ end
308
+
309
+ # Merges a PATH-style variable by appending any missing subpaths on the
310
+ # 'to' side to the value on the 'from' side in order of appearance. note
311
+ # that the ordering of paths on the 'to' side is not preserved when some of
312
+ # the paths also appear on the 'from' side. This is because paths on the
313
+ # 'from' side always take precedence. This is an issue if two paths
314
+ # reference similarly named executables and swapping the order of paths
315
+ # would cause the wrong executable to be invoked. To resolve this, the
316
+ # higher precedence path can be changed to ensure that the conflicting paths
317
+ # are both specified in the proper order. There is no trivial algorithm
318
+ # which can predict the proper ordering of such paths.
319
+ #
320
+ # === Parameters
321
+ # from_value(String):: value to merge from
322
+ # to_value(String):: value to merge to
323
+ #
324
+ # === Returns
325
+ # merged_value(String):: merged value
326
+ def self.merge_environment_path_value(from_value, to_value)
327
+ # normalize to backslashes for Windows-style PATH variable.
328
+ from_value = from_value.gsub(File::SEPARATOR, File::ALT_SEPARATOR)
329
+ to_value = to_value.gsub(File::SEPARATOR, File::ALT_SEPARATOR)
330
+
331
+ # quick outs.
332
+ return from_value if to_value.empty?
333
+ return to_value if from_value.empty?
334
+
335
+ # Windows paths are case-insensitive, so we want to match paths efficiently
336
+ # while being case-insensitive. we will make use of NoCaseKey again.
337
+ from_value_hash = {}
338
+ from_value.split(File::PATH_SEPARATOR).each { |path| from_value_hash[NoCaseKey.new(path)] = true }
339
+ appender = ""
340
+ to_value.split(File::PATH_SEPARATOR).each do |path|
341
+ if not from_value_hash[NoCaseKey.new(path)]
342
+ appender += File::PATH_SEPARATOR + path
343
+ end
344
+ end
345
+
346
+ return from_value + appender
347
+ end
348
+
349
+ # Queries the environment strings from the current thread/process user's
350
+ # environment (which is stored in the registry on Windows as a combination of
351
+ # system and user-specific environment variables). The resulting hash
352
+ # represents any variables set for the persisted user context but any set
353
+ # dynamically in the current process context.
354
+ #
355
+ # === Returns
356
+ # environment_hash(Hash):: hash of environment key (String) to value (String).
357
+ def self.get_current_user_environment
358
+ environment_strings = RightPopen.get_current_user_environment
359
+
360
+ return string_block_to_environment_hash(environment_strings)
361
+ end
362
+
363
+ # Queries the environment strings from the process environment (which is kept
364
+ # in memory for each process and generally begins life as a copy of the
365
+ # process user's environment context plus any changes made by ancestral
366
+ # processes).
367
+ #
368
+ # === Returns
369
+ # environment_hash(Hash):: hash of environment key (String) to value (String).
370
+ def self.get_process_environment
371
+ environment_strings = RightPopen.get_process_environment
372
+
373
+ return string_block_to_environment_hash(environment_strings)
374
+ end
375
+
376
+ # Converts a nul-terminated block of nul-terminated strings to a hash by
377
+ # splitting the block on nul characters until the empty string is found.
378
+ # splits substrings on the '=' character which is used to delimit key from
379
+ # value in Windows environment blocks.
380
+ #
381
+ # === Paramters
382
+ # string_block(String):: string containing nul-terminated substrings followed
383
+ # by a nul-terminator.
384
+ #
385
+ # === Returns
386
+ # string_hash(Hash):: hash of string to string
387
+ def self.string_block_to_environment_hash(string_block)
388
+ result_hash = {}
389
+ last_offset = 0
390
+ string_block_length = string_block.length
391
+ while last_offset < string_block_length
392
+ offset = string_block.index(0.chr, last_offset)
393
+ if offset.nil?
394
+ offset = string_block.length
395
+ end
396
+ env_string = string_block[last_offset, offset - last_offset]
397
+ break if env_string.empty?
398
+ last_offset = offset + 1
399
+
400
+ # note that Windows uses "=C:=C:\" notation for working directory info, so
401
+ # ignore equals if it is the first character.
402
+ equals_offset = env_string.index('=', 1)
403
+ if equals_offset
404
+ env_key = env_string[0, equals_offset]
405
+ env_value = env_string[equals_offset + 1..-1]
406
+ result_hash[NoCaseKey.new(env_key)] = env_value
407
+ end
408
+ end
409
+
410
+ return result_hash
411
+ end
412
+
413
+ # Converts a hash of string to string to a string block by combining pairs
414
+ # into a single string delimited by the '=' character and then placing nul-
415
+ # terminators after each pair, followed by a final nul-terminator.
416
+ #
417
+ # === Parameters
418
+ # environment_hash(Hash):: hash of
419
+ def self.environment_hash_to_string_block(environment_hash)
420
+ result_block = ""
421
+ environment_hash.keys.sort.each do |key|
422
+ result_block += "#{key}=#{environment_hash[key]}\0"
423
+ end
424
+
425
+ return result_block + "\0"
426
+ end
427
+
428
+ end
234
429
  end
Binary file
data/right_popen.gemspec CHANGED
@@ -1,14 +1,16 @@
1
1
  require 'rubygems'
2
2
 
3
- spec = Gem::Specification.new do |spec|
4
- is_windows = RUBY_PLATFORM =~ /mswin/
3
+ def is_windows?
4
+ return RUBY_PLATFORM =~ /mswin/
5
+ end
5
6
 
7
+ spec = Gem::Specification.new do |spec|
6
8
  spec.name = 'right_popen'
7
- spec.version = '1.0.2'
9
+ spec.version = '1.0.5'
8
10
  spec.authors = ['Scott Messier', 'Raphael Simon']
9
11
  spec.email = 'scott@rightscale.com'
10
12
  spec.homepage = 'https://github.com/rightscale/right_popen'
11
- if is_windows
13
+ if is_windows?
12
14
  spec.platform = 'x86-mswin32-60'
13
15
  else
14
16
  spec.platform = Gem::Platform::RUBY
@@ -27,7 +29,7 @@ of its internal mechanisms. The Linux implementation is valid for any Linux
27
29
  platform but there is also a native implementation for Windows platforms.
28
30
  EOF
29
31
 
30
- if is_windows
32
+ if is_windows?
31
33
  extension_dir = "ext,"
32
34
  else
33
35
  extension_dir = ""
@@ -38,7 +40,7 @@ EOF
38
40
  item.include?("Makefile") || item.include?(".obj") || item.include?(".pdb") || item.include?(".def") || item.include?(".exp") || item.include?(".lib")
39
41
  end
40
42
  candidates = candidates.delete_if do |item|
41
- if is_windows
43
+ if is_windows?
42
44
  item.include?("/linux/")
43
45
  else
44
46
  item.include?("/win32/")
@@ -46,9 +48,13 @@ EOF
46
48
  end
47
49
  spec.files = candidates.sort!
48
50
 
49
- # Current implementation doesn't support > 0.12.8, but support any patched 0.12.8.x versions.
50
- spec.add_runtime_dependency(%q<eventmachine>, [">= 0.12.8", "< 0.12.9"])
51
- if is_windows
51
+ # Current implementation supports >= 0.12.8
52
+ spec.add_runtime_dependency(%q<eventmachine>, [">= 0.12.8"])
53
+ if is_windows?
54
+ # Windows implementation currently depends on deprecated behavior from
55
+ # 0.12.8, but we also need to support the 0.12.8.1 patch version. the Linux
56
+ # side is free to use 0.12.10+
57
+ spec.add_runtime_dependency(%q<eventmachine>, ["< 0.12.9"])
52
58
  spec.add_runtime_dependency(%q<win32-process>, [">= 0.6.1"])
53
59
  end
54
60
  end
data/spec/print_env.rb ADDED
@@ -0,0 +1,2 @@
1
+ puts "__test__=#{ENV['__test__']}" if ENV['__test__']
2
+ puts "PATH=#{ENV['PATH']}"
@@ -1,5 +1,4 @@
1
1
  require File.join(File.dirname(__FILE__), 'spec_helper')
2
- require 'right_popen'
3
2
 
4
3
  RUBY_CMD = 'ruby'
5
4
  STANDARD_MESSAGE = 'Standard message'
@@ -10,32 +9,82 @@ EXIT_STATUS = 146
10
9
  # for a quick smoke test
11
10
  LARGE_OUTPUT_COUNTER = 1000
12
11
 
12
+ # bump up count for most exhaustive leak detection.
13
+ REPEAT_TEST_COUNTER = 256
14
+
15
+ def is_windows?
16
+ return RUBY_PLATFORM =~ /mswin/
17
+ end
18
+
13
19
  describe 'RightScale::popen3' do
14
20
 
15
21
  module RightPopenSpec
16
22
 
17
23
  class Runner
18
24
  def initialize
19
- @done = false
25
+ @done = false
26
+ @output_text = nil
27
+ @error_text = nil
28
+ @status = nil
29
+ @last_exception = nil
30
+ @last_iteration = 0
31
+ end
32
+
33
+ attr_reader :output_text, :error_text, :status
34
+
35
+ def do_right_popen(command, env=nil)
20
36
  @output_text = ''
21
37
  @error_text = ''
22
38
  @status = nil
39
+ RightScale.popen3(:command => command,
40
+ :target => self,
41
+ :environment => env,
42
+ :stdout_handler => :on_read_stdout,
43
+ :stderr_handler => :on_read_stderr,
44
+ :exit_handler => :on_exit)
23
45
  end
24
46
 
25
- attr_reader :output_text, :error_text, :status
26
-
27
- def run_right_popen(command)
47
+ def run_right_popen(command, env=nil, count = 1)
48
+ puts "#{count}>" if count > 1
49
+ last_iteration = 0
28
50
  EM.next_tick do
29
- RightScale.popen3(command, self, :on_read_stdout, :on_read_stderr, :on_exit)
51
+ do_right_popen(command, env)
30
52
  end
31
53
  EM.run do
32
- timer = EM::PeriodicTimer.new(0.1) do
33
- if @done
54
+ timer = EM::PeriodicTimer.new(0.05) do
55
+ begin
56
+ if @done || @last_exception
57
+ last_iteration = last_iteration + 1
58
+ if @last_exception.nil? && last_iteration < count
59
+ @done = false
60
+ EM.next_tick do
61
+ if count > 1
62
+ print '+'
63
+ STDOUT.flush
64
+ end
65
+ do_right_popen(command, env)
66
+ end
67
+ else
68
+ puts "<" if count > 1
69
+ timer.cancel
70
+ EM.stop
71
+ end
72
+ end
73
+ rescue Exception => e
74
+ @last_exception = e
34
75
  timer.cancel
35
76
  EM.stop
36
77
  end
37
78
  end
38
79
  end
80
+ if @last_exception
81
+ if count > 1
82
+ message = "<#{last_iteration + 1}\n#{last_exception.message}"
83
+ else
84
+ message = last_exception.message
85
+ end
86
+ raise @last_exception.class, "#{message}\n#{@last_exception.backtrace.join("\n")}"
87
+ end
39
88
  end
40
89
 
41
90
  def on_read_stdout(data)
@@ -121,5 +170,50 @@ describe 'RightScale::popen3' do
121
170
  end
122
171
  runner.error_text.should == results
123
172
  end
173
+
174
+ it 'should setup environment variables' do
175
+ command = "\"#{RUBY_CMD}\" \"#{File.expand_path(File.join(File.dirname(__FILE__), 'print_env.rb'))}\""
176
+ runner = RightPopenSpec::Runner.new
177
+ runner.run_right_popen(command)
178
+ runner.status.exitstatus.should == 0
179
+ runner.output_text.should_not include('_test_')
180
+ runner.run_right_popen(command, :__test__ => '42')
181
+ runner.status.exitstatus.should == 0
182
+ runner.output_text.should match(/^__test__=42$/)
183
+ end
184
+
185
+ it 'should restore environment variables' do
186
+ ENV['__test__'] = '41'
187
+ old_envs = {}
188
+ ENV.each { |k, v| old_envs[k] = v }
189
+ command = "\"#{RUBY_CMD}\" \"#{File.expand_path(File.join(File.dirname(__FILE__), 'print_env.rb'))}\""
190
+ runner = RightPopenSpec::Runner.new
191
+ runner.run_right_popen(command, :__test__ => '42')
192
+ runner.status.exitstatus.should == 0
193
+ runner.output_text.should match(/^__test__=42$/)
194
+ ENV.each { |k, v| old_envs[k].should == v }
195
+ old_envs.each { |k, v| ENV[k].should == v }
196
+ end
197
+
198
+ if is_windows?
199
+ # FIX: this behavior is currently specific to Windows but should probably be
200
+ # implemented for Linux.
201
+ it 'should merge the PATH variable instead of overriding it' do
202
+ command = "\"#{RUBY_CMD}\" \"#{File.expand_path(File.join(File.dirname(__FILE__), 'print_env.rb'))}\""
203
+ runner = RightPopenSpec::Runner.new
204
+ runner.run_right_popen(command, 'PATH' => "c:/bogus\\bin")
205
+ runner.status.exitstatus.should == 0
206
+ runner.output_text.should include('PATH=c:\\bogus\\bin;')
207
+ end
208
+ end
209
+
210
+ it 'should run repeatedly without leaking resources' do
211
+ command = "\"#{RUBY_CMD}\" \"#{File.expand_path(File.join(File.dirname(__FILE__), 'produce_output.rb'))}\" \"#{STANDARD_MESSAGE}\" \"#{ERROR_MESSAGE}\""
212
+ runner = RightPopenSpec::Runner.new
213
+ runner.run_right_popen(command, nil, REPEAT_TEST_COUNTER)
214
+ runner.status.exitstatus.should == 0
215
+ runner.output_text.should == STANDARD_MESSAGE + "\n"
216
+ runner.error_text.should == ERROR_MESSAGE + "\n"
217
+ end
124
218
 
125
219
  end
data/spec/spec_helper.rb CHANGED
@@ -1,2 +1,4 @@
1
1
  require 'rubygems'
2
- $:.push File.join(File.dirname(__FILE__), '..', 'lib')
2
+ require 'spec'
3
+ require 'eventmachine'
4
+ require File.join(File.dirname(__FILE__), '..', 'lib', 'right_popen')
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: right_popen
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.2
4
+ version: 1.0.5
5
5
  platform: x86-mswin32-60
6
6
  authors:
7
7
  - Scott Messier
@@ -10,7 +10,7 @@ autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
12
 
13
- date: 2010-02-18 00:00:00 -08:00
13
+ date: 2010-02-23 00:00:00 -08:00
14
14
  default_executable:
15
15
  dependencies:
16
16
  - !ruby/object:Gem::Dependency
@@ -22,6 +22,13 @@ dependencies:
22
22
  - - ">="
23
23
  - !ruby/object:Gem::Version
24
24
  version: 0.12.8
25
+ version:
26
+ - !ruby/object:Gem::Dependency
27
+ name: eventmachine
28
+ type: :runtime
29
+ version_requirement:
30
+ version_requirements: !ruby/object:Gem::Requirement
31
+ requirements:
25
32
  - - <
26
33
  - !ruby/object:Gem::Version
27
34
  version: 0.12.9
@@ -58,6 +65,7 @@ files:
58
65
  - lib/win32/right_popen.rb
59
66
  - lib/win32/right_popen.so
60
67
  - right_popen.gemspec
68
+ - spec/print_env.rb
61
69
  - spec/produce_mixed_output.rb
62
70
  - spec/produce_output.rb
63
71
  - spec/produce_status.rb