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 +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
|