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

Sign up to get free protection for your applications and to get access to all the features.
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