48
48
import java .nio .channels .Channels ;
49
49
import java .nio .channels .WritableByteChannel ;
50
50
import java .util .ArrayList ;
51
+ import java .util .HashMap ;
51
52
import java .util .List ;
52
53
import java .util .Map ;
53
54
57
58
import com .oracle .graal .python .builtins .PythonBuiltinClassType ;
58
59
import com .oracle .graal .python .builtins .PythonBuiltins ;
59
60
import com .oracle .graal .python .builtins .objects .PNone ;
60
- import com .oracle .graal .python .builtins .objects .bytes .BytesNodes ;
61
61
import com .oracle .graal .python .builtins .objects .bytes .PBytes ;
62
- import com .oracle .graal .python .builtins .objects .common .SequenceStorageNodes ;
63
62
import com .oracle .graal .python .builtins .objects .exception .OSErrorEnum ;
64
63
import com .oracle .graal .python .builtins .objects .exception .OSErrorEnum .ErrorAndMessagePair ;
65
64
import com .oracle .graal .python .builtins .objects .function .PArguments ;
66
65
import com .oracle .graal .python .builtins .objects .list .PList ;
67
66
import com .oracle .graal .python .builtins .objects .object .PythonObjectLibrary ;
68
- import com .oracle .graal .python .builtins .objects .str .PString ;
69
67
import com .oracle .graal .python .nodes .ErrorMessages ;
70
68
import com .oracle .graal .python .nodes .PGuards ;
71
69
import com .oracle .graal .python .nodes .expression .CastToListExpressionNode .CastToListNode ;
77
75
import com .oracle .graal .python .runtime .PosixResources ;
78
76
import com .oracle .graal .python .runtime .PythonContext ;
79
77
import com .oracle .graal .python .runtime .PythonOptions ;
78
+ import com .oracle .graal .python .runtime .sequence .storage .SequenceStorage ;
80
79
import com .oracle .truffle .api .CompilerDirectives .TruffleBoundary ;
80
+ import com .oracle .truffle .api .TruffleFile ;
81
+ import com .oracle .truffle .api .TruffleLanguage .Env ;
81
82
import com .oracle .truffle .api .TruffleLogger ;
82
83
import com .oracle .truffle .api .dsl .Cached ;
83
84
import com .oracle .truffle .api .dsl .GenerateNodeFactory ;
84
85
import com .oracle .truffle .api .dsl .NodeFactory ;
85
86
import com .oracle .truffle .api .dsl .Specialization ;
86
87
import com .oracle .truffle .api .frame .VirtualFrame ;
88
+ import com .oracle .truffle .api .interop .UnsupportedMessageException ;
87
89
import com .oracle .truffle .api .library .CachedLibrary ;
88
90
89
91
@ CoreFunctions (defineModule = "_posixsubprocess" )
@@ -100,62 +102,140 @@ protected List<? extends NodeFactory<? extends PythonBuiltinBaseNode>> getNodeFa
100
102
abstract static class ForkExecNode extends PythonBuiltinNode {
101
103
private static final TruffleLogger LOGGER = PythonLanguage .getLogger (ForkExecNode .class );
102
104
103
- @ Child private BytesNodes .ToBytesNode toBytes = BytesNodes .ToBytesNode .create ();
104
-
105
105
@ Specialization
106
106
int forkExec (VirtualFrame frame , PList args , @ SuppressWarnings ("unused" ) PList execList , @ SuppressWarnings ("unused" ) boolean closeFds ,
107
107
@ SuppressWarnings ("unused" ) PList fdsToKeep , String cwd , PList env ,
108
108
int p2cread , int p2cwrite , int c2pread , int c2pwrite ,
109
109
int errread , int errwrite , @ SuppressWarnings ("unused" ) int errpipe_read , int errpipe_write ,
110
- @ SuppressWarnings ("unused" ) boolean restore_signals , @ SuppressWarnings ("unused" ) boolean call_setsid , @ SuppressWarnings ("unused" ) PNone preexec_fn ,
111
- @ Cached SequenceStorageNodes .CopyInternalArrayNode copy ) {
110
+ @ SuppressWarnings ("unused" ) boolean restore_signals , @ SuppressWarnings ("unused" ) boolean call_setsid , @ SuppressWarnings ("unused" ) PNone preexec_fn ) {
112
111
113
112
PythonContext context = getContext ();
114
113
Object state = IndirectCallContext .enter (frame , context , this );
115
114
try {
116
115
return forkExec (args , execList , closeFds , fdsToKeep , cwd , env , p2cread , p2cwrite , c2pread , c2pwrite , errread , errwrite , errpipe_read , errpipe_write , restore_signals , call_setsid ,
117
- preexec_fn , copy );
116
+ preexec_fn );
118
117
} finally {
119
118
IndirectCallContext .exit (frame , context , state );
120
119
}
121
120
}
122
121
123
122
@ TruffleBoundary
124
- private synchronized int forkExec (PList args , @ SuppressWarnings ( "unused" ) PList execList , @ SuppressWarnings ("unused" ) boolean closeFds ,
123
+ private synchronized int forkExec (PList args , PList execList , @ SuppressWarnings ("unused" ) boolean closeFds ,
125
124
@ SuppressWarnings ("unused" ) PList fdsToKeep , String cwd , PList env ,
126
125
int p2cread , int p2cwrite , int c2pread , int c2pwrite ,
127
126
int errread , int errwrite , @ SuppressWarnings ("unused" ) int errpipe_read , int errpipe_write ,
128
- @ SuppressWarnings ("unused" ) boolean restore_signals , @ SuppressWarnings ("unused" ) boolean call_setsid , @ SuppressWarnings ("unused" ) PNone preexec_fn ,
129
- @ Cached SequenceStorageNodes .CopyInternalArrayNode copy ) {
127
+ @ SuppressWarnings ("unused" ) boolean restore_signals , @ SuppressWarnings ("unused" ) boolean call_setsid , @ SuppressWarnings ("unused" ) PNone preexec_fn ) {
130
128
PythonContext context = getContext ();
131
129
PosixResources resources = context .getResources ();
132
130
if (!context .isExecutableAccessAllowed ()) {
133
131
return -1 ;
134
132
}
135
133
136
- ArrayList <String > argStrings = new ArrayList <>();
137
- Object [] copyOfInternalArray = copy .execute (args .getSequenceStorage ());
138
- for (Object o : copyOfInternalArray ) {
139
- if (o instanceof String ) {
140
- argStrings .add ((String ) o );
141
- } else if (o instanceof PString ) {
142
- argStrings .add (((PString ) o ).getValue ());
143
- } else {
134
+ SequenceStorage argsStorage = args .getSequenceStorage ();
135
+ ArrayList <String > argStrings = new ArrayList <>(argsStorage .length ());
136
+ CastToJavaStringNode castToStringNode = CastToJavaStringNode .getUncached ();
137
+ for (int i = 0 ; i < argsStorage .length (); i ++) {
138
+ try {
139
+ argStrings .add (castToStringNode .execute (argsStorage .getItemNormalized (i )));
140
+ } catch (CannotCastException ex ) {
144
141
throw raise (PythonBuiltinClassType .OSError , ErrorMessages .ILLEGAL_ARG );
145
142
}
146
143
}
147
144
148
- if (!argStrings .isEmpty ()) {
149
- if (argStrings .get (0 ).equals (context .getOption (PythonOptions .Executable ))) {
145
+ File cwdFile ;
146
+ Env truffleEnv = context .getEnv ();
147
+ if (getSafeTruffleFile (truffleEnv , cwd ).exists ()) {
148
+ cwdFile = new File (cwd );
149
+ } else {
150
+ throw raise (PythonBuiltinClassType .OSError , ErrorMessages .WORK_DIR_NOT_ACCESSIBLE , cwd );
151
+ }
152
+
153
+ SequenceStorage envStorage = env .getSequenceStorage ();
154
+ HashMap <String , String > envMap = new HashMap <>(envStorage .length ());
155
+ PythonObjectLibrary pyLib = PythonObjectLibrary .getUncached ();
156
+ for (int i = 0 ; i < envStorage .length (); i ++) {
157
+ Object keyValue = envStorage .getItemNormalized (i );
158
+ if (!(keyValue instanceof PBytes )) {
159
+ continue ;
160
+ }
161
+ // NOTE: passing 'null' frame means we took care of the global state in the callers
162
+ String str = checkNullBytesAndEncode (pyLib , (PBytes ) keyValue );
163
+ String [] strings = str .split ("=" , 2 );
164
+ if (strings .length == 2 ) {
165
+ envMap .put (strings [0 ], strings [1 ]);
166
+ }
167
+ }
168
+
169
+ // The execList argument contains a list of paths to executables. They should be tried
170
+ // one-by-one until we find one that can be executed. Unless passed explicitly as an
171
+ // argument by the user, the list is constructed by the Python wrapper code, which
172
+ // creates an entry for each directory in $PATH joined with the executable name
173
+
174
+ // CPython iterates the executable list trying to call execve for each item until it
175
+ // finds one whose execution succeeds. We do the same to be as compatible as possible.
176
+
177
+ // Moreover, execve allows to set program arguments (argv) including argv[0] to anything
178
+ // and independently of that choose the executable. There is nothing like that in the
179
+ // ProcessBuilder API, so we have to replace the first argument with the right
180
+ // executable path taken from execList
181
+
182
+ if (argStrings .isEmpty ()) {
183
+ // CPython fails on OS level and does not raise any python level error, just the
184
+ // message is print to stderr by the subprocess
185
+ throw raise (PythonBuiltinClassType .OSError , "A NULL argv[0] was passed through an exec system call" );
186
+ }
187
+
188
+ LOGGER .fine (() -> "_posixsubprocess.fork_exec: " + String .join (" " , argStrings ));
189
+ IOException firstError = null ;
190
+ SequenceStorage execListStorage = execList .getSequenceStorage ();
191
+ for (int i = 0 ; i < execListStorage .length (); i ++) {
192
+ Object item = execListStorage .getItemNormalized (i );
193
+ if (!(item instanceof PBytes )) {
194
+ throw raise (PythonBuiltinClassType .TypeError , ErrorMessages .EXPECTED_BYTES_P_FOUND , item );
195
+ }
196
+ String path = checkNullBytesAndEncode (pyLib , (PBytes ) item );
197
+ int executableListLen = 0 ;
198
+ if (path .equals (context .getOption (PythonOptions .Executable ))) {
199
+ // In case someone passed to us sys.executable that happens to be java command
200
+ // invocation with additional options like classpath, we split it to the
201
+ // individual arguments
150
202
String [] executableList = PythonOptions .getExecutableList (context );
151
203
argStrings .remove (0 );
152
- for (int i = executableList .length - 1 ; i >= 0 ; i --) {
153
- argStrings .add (0 , executableList [i ]);
204
+ executableListLen = executableList .length ;
205
+ for (int j = executableListLen - 1 ; j >= 0 ; j --) {
206
+ argStrings .add (0 , executableList [j ]);
154
207
}
208
+ } else {
209
+ argStrings .set (0 , path );
155
210
}
211
+ TruffleFile executableFile = getSafeTruffleFile (truffleEnv , argStrings .get (0 ));
212
+ if (executableFile .isExecutable ()) {
213
+ try {
214
+ return exec (argStrings , cwdFile , envMap , p2cwrite , p2cread , c2pwrite , c2pread , errwrite , errpipe_write , resources , errread );
215
+ } catch (IOException ex ) {
216
+ if (firstError == null ) {
217
+ firstError = ex ;
218
+ }
219
+ }
220
+ } else {
221
+ LOGGER .finest (() -> "_posixsubprocess.fork_exec not executable: " + executableFile );
222
+ }
223
+ for (int j = 1 ; j < executableListLen ; j ++) {
224
+ argStrings .remove (1 );
225
+ }
226
+ }
227
+ if (errpipe_write != -1 ) {
228
+ handleIOError (errpipe_write , resources , firstError );
156
229
}
230
+ return -1 ;
231
+ }
157
232
158
- LOGGER .fine (() -> "_posixsubprocess.fork_exec: " + String .join (" " , argStrings ));
233
+ // Tries executing given arguments, throws IOException if the executable cannot be executed,
234
+ // any other error is handled here
235
+ private int exec (ArrayList <String > argStrings , File cwd , Map <String , String > env ,
236
+ int p2cwrite , int p2cread , int c2pwrite , int c2pread ,
237
+ int errwrite , int errpipe_write , PosixResources resources , int errread ) throws IOException {
238
+ LOGGER .finest (() -> "_posixsubprocess.fork_exec trying to exec: " + String .join (" " , argStrings ));
159
239
ProcessBuilder pb = new ProcessBuilder (argStrings );
160
240
if (p2cread != -1 && p2cwrite != -1 ) {
161
241
pb .redirectInput (Redirect .PIPE );
@@ -179,30 +259,11 @@ private synchronized int forkExec(PList args, @SuppressWarnings("unused") PList
179
259
pb .redirectErrorStream (true );
180
260
}
181
261
182
- try {
183
- if (getContext ().getEnv ().getPublicTruffleFile (cwd ).exists ()) {
184
- pb .directory (new File (cwd ));
185
- } else {
186
- throw raise (PythonBuiltinClassType .OSError , ErrorMessages .WORK_DIR_NOT_ACCESSIBLE , cwd );
187
- }
188
- } catch (SecurityException e ) {
189
- throw raise (PythonBuiltinClassType .OSError , e );
190
- }
191
-
192
- Map <String , String > environment = pb .environment ();
193
- for (Object keyValue : env .getSequenceStorage ().getInternalArray ()) {
194
- if (keyValue instanceof PBytes ) {
195
- // NOTE: passing 'null' frame means we took care of the global state in the
196
- // callers
197
- String [] string = new String (toBytes .execute (null , keyValue )).split ("=" , 2 );
198
- if (string .length == 2 ) {
199
- environment .put (string [0 ], string [1 ]);
200
- }
201
- }
202
- }
262
+ pb .directory (cwd );
263
+ pb .environment ().putAll (env );
203
264
265
+ Process process = pb .start ();
204
266
try {
205
- Process process = pb .start ();
206
267
if (p2cwrite != -1 ) {
207
268
// user code is expected to close the unused ends of the pipes
208
269
resources .getFileChannel (p2cwrite ).close ();
@@ -216,25 +277,59 @@ private synchronized int forkExec(PList args, @SuppressWarnings("unused") PList
216
277
resources .getFileChannel (errread ).close ();
217
278
resources .fdopen (errread , Channels .newChannel (process .getErrorStream ()));
218
279
}
219
-
220
- return resources .registerChild (process );
221
- } catch (IOException e ) {
280
+ } catch (IOException ex ) {
281
+ // We only want to rethrow the IOException that may come out of pb.start()
222
282
if (errpipe_write != -1 ) {
223
- // write exec error information here. Data format: "exception name:hex
224
- // errno:description"
225
- handleIOError (errpipe_write , resources , e );
283
+ handleIOError (errpipe_write , resources , ex );
226
284
}
227
285
return -1 ;
228
286
}
287
+
288
+ return resources .registerChild (process );
289
+ }
290
+
291
+ private TruffleFile getSafeTruffleFile (Env env , String path ) {
292
+ try {
293
+ return env .getPublicTruffleFile (path );
294
+ } catch (SecurityException e ) {
295
+ throw raise (PythonBuiltinClassType .OSError , e );
296
+ }
297
+ }
298
+
299
+ private String checkNullBytesAndEncode (PythonObjectLibrary pyLib , PBytes bytesObj ) {
300
+ byte [] bytes ;
301
+ try {
302
+ bytes = pyLib .getBufferBytes (bytesObj );
303
+ } catch (UnsupportedMessageException e ) {
304
+ throw new AssertionError (); // should not happen
305
+ }
306
+ for (byte b : bytes ) {
307
+ if (b == 0 ) {
308
+ throw raise (PythonBuiltinClassType .ValueError , ErrorMessages .EMBEDDED_NULL_BYTE );
309
+ }
310
+ }
311
+ // Note: we use intentionally the default encoding for the bytes. We're most likely
312
+ // getting bytes that the Python wrapper encoded from strings passed to it by the user
313
+ // and we should support non-ascii characters supported by the current FS. See
314
+ // test_warnings.test_nonascii
315
+ return new String (bytes );
229
316
}
230
317
231
318
@ TruffleBoundary (allowInlining = true )
232
319
private void handleIOError (int errpipe_write , PosixResources resources , IOException e ) {
320
+ // write exec error information here. Data format: "exception name:hex
321
+ // errno:description". The exception can be null if we did not find any file in the
322
+ // execList that could be executed
233
323
Channel err = resources .getFileChannel (errpipe_write );
234
324
if (!(err instanceof WritableByteChannel )) {
235
325
throw raise (PythonBuiltinClassType .OSError , ErrorMessages .ERROR_WRITING_FORKEXEC );
236
326
} else {
237
- ErrorAndMessagePair pair = OSErrorEnum .fromException (e );
327
+ ErrorAndMessagePair pair ;
328
+ if (e == null ) {
329
+ pair = new ErrorAndMessagePair (OSErrorEnum .ENOENT , OSErrorEnum .ENOENT .getMessage ());
330
+ } else {
331
+ pair = OSErrorEnum .fromException (e );
332
+ }
238
333
try {
239
334
((WritableByteChannel ) err ).write (ByteBuffer .wrap (("OSError:" + Long .toHexString (pair .oserror .getNumber ()) + ":" + pair .message ).getBytes ()));
240
335
} catch (IOException e1 ) {
@@ -253,7 +348,6 @@ int forkExecDefault(VirtualFrame frame, Object args, Object executable_list, Obj
253
348
@ Cached CastToListNode castFdsToKeep ,
254
349
@ Cached CastToJavaStringNode castCwd ,
255
350
@ Cached CastToListNode castEnv ,
256
- @ Cached SequenceStorageNodes .CopyInternalArrayNode copy ,
257
351
@ CachedLibrary (limit = "3" ) PythonObjectLibrary lib ) {
258
352
String actualCwd ;
259
353
if (cwd instanceof PNone ) {
@@ -291,7 +385,7 @@ int forkExecDefault(VirtualFrame frame, Object args, Object executable_list, Obj
291
385
lib .asSizeWithState (errpipe_read , PArguments .getThreadState (frame )),
292
386
lib .asSizeWithState (errpipe_write , PArguments .getThreadState (frame )),
293
387
lib .isTrueWithState (restore_signals , PArguments .getThreadState (frame )),
294
- lib .isTrueWithState (call_setsid , PArguments .getThreadState (frame )), PNone .NO_VALUE , copy );
388
+ lib .isTrueWithState (call_setsid , PArguments .getThreadState (frame )), PNone .NO_VALUE );
295
389
}
296
390
297
391
}
0 commit comments