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 +8 -3
- data/Rakefile +5 -0
- data/ext/win32/right_popen.c +238 -30
- data/ext/win32/right_popen.h +21 -21
- data/lib/right_popen.rb +35 -2
- data/lib/win32/right_popen.rb +231 -36
- data/lib/win32/right_popen.so +0 -0
- data/right_popen.gemspec +15 -9
- data/spec/print_env.rb +2 -0
- data/spec/right_popen_spec.rb +102 -8
- data/spec/spec_helper.rb +3 -1
- metadata +10 -2
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
|
-
|
45
|
-
RightScale.popen3(
|
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
|
-
|
98
|
+
rake spec
|
94
99
|
|
95
100
|
|
96
101
|
== LICENSE
|
data/Rakefile
CHANGED
data/ext/win32/right_popen.c
CHANGED
@@ -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
|
-
|
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
|
260
|
-
|
266
|
+
*pPid = (rb_pid_t)pi.dwProcessId;
|
267
|
+
bResult = TRUE;
|
261
268
|
}
|
262
269
|
|
263
|
-
return
|
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, "
|
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
|
}
|
data/ext/win32/right_popen.h
CHANGED
@@ -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
|
data/lib/win32/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,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
|
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
|
-
#
|
199
|
-
|
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
|
-
#
|
212
|
-
|
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(
|
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
|
data/lib/win32/right_popen.so
CHANGED
Binary file
|
data/right_popen.gemspec
CHANGED
@@ -1,14 +1,16 @@
|
|
1
1
|
require 'rubygems'
|
2
2
|
|
3
|
-
|
4
|
-
|
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.
|
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
|
50
|
-
spec.add_runtime_dependency(%q<eventmachine>, [">= 0.12.8"
|
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
data/spec/right_popen_spec.rb
CHANGED
@@ -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
|
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
|
-
|
26
|
-
|
27
|
-
|
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
|
-
|
51
|
+
do_right_popen(command, env)
|
30
52
|
end
|
31
53
|
EM.run do
|
32
|
-
timer = EM::PeriodicTimer.new(0.
|
33
|
-
|
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
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.
|
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-
|
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
|