Skip to content

Commit f32bf73

Browse files
jameslanslozier
andauthored
Use System.Diagnostics.Process to implement subprocess.Popen, which could be used on posix and windows (#933)
* Use System.Diagnostics.Process to implement subprocess.Popen, which could be used on posix and windows * Apply suggestions from code review Co-authored-by: slozier <slozier@users.noreply.github.com> Co-authored-by: slozier <slozier@users.noreply.github.com>
1 parent 260204c commit f32bf73

5 files changed

Lines changed: 137 additions & 34 deletions

File tree

Src/IronPythonTest/Cases/CPythonCasesManifest.ini

Lines changed: 0 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -35,10 +35,6 @@ Ignore=true
3535
Ignore=true
3636
Reason=Current implementation of get_last_error needs to be debugged
3737

38-
[CPython.ctypes.test_find]
39-
RunCondition=NOT $(IS_POSIX)
40-
Reason=Blocked by https://github.com/IronLanguages/ironpython3/issues/541
41-
4238
[CPython.ctypes.test_frombuffer]
4339
Ignore=true
4440

@@ -60,10 +56,6 @@ Ignore=true
6056
[CPython.ctypes.test_libc]
6157
Ignore=true
6258

63-
[CPython.ctypes.test_loading]
64-
RunCondition=NOT $(IS_POSIX)
65-
Reason=Blocked by https://github.com/IronLanguages/ironpython3/issues/541
66-
6759
[CPython.ctypes.test_objects]
6860
Ignore=true
6961
Reason=CPython implementation detail
@@ -193,10 +185,6 @@ Ignore=true
193185
Ignore=true
194186
Reason=ImportError: No module named audioop
195187

196-
[CPython.test_base64]
197-
RunCondition=NOT $(IS_POSIX)
198-
Reason=Blocked by https://github.com/IronLanguages/ironpython3/issues/541
199-
200188
[CPython.test_bigmem]
201189
Ignore=true
202190

@@ -586,8 +574,6 @@ Ignore=true
586574

587575
[CPython.test_keyword]
588576
NotParallelSafe=true
589-
RunCondition=NOT $(IS_POSIX)
590-
Reason=https://github.com/IronLanguages/ironpython3/issues/541
591577

592578
[CPython.test_keywordonlyarg]
593579
Ignore=true
@@ -921,8 +907,6 @@ Ignore=true
921907
Ignore=true
922908

923909
[CPython.test_source_encoding]
924-
RunCondition=NOT $(IS_POSIX)
925-
Reason=https://github.com/IronLanguages/ironpython3/issues/541
926910
IsolationLevel=ENGINE # source file in a non-UTF-8 encoding
927911

928912
[CPython.test_spwd]
@@ -1125,10 +1109,6 @@ Reason=https://github.com/IronLanguages/ironpython3/issues/581
11251109
IsolationLevel=ENGINE
11261110
MaxRecursion=100
11271111

1128-
[CPython.test_uuid]
1129-
RunCondition=NOT $(IS_POSIX)
1130-
Reason=https://github.com/IronLanguages/ironpython3/issues/541
1131-
11321112
[CPython.test_venv]
11331113
Ignore=true
11341114

Src/IronPythonTest/Cases/IronPythonCasesManifest.ini

Lines changed: 4 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,8 @@ Redirect=false
55
Timeout=120000 # 2 minute timeout
66

77
[IronPython.test_builtin_stdlib]
8-
RunCondition=NOT $(IS_POSIX)
9-
Reason=Blocked by https://github.com/IronLanguages/ironpython3/issues/541
10-
11-
[IronPython.test_bz2_stdlib]
12-
RunCondition=NOT $(IS_POSIX)
13-
Reason=Blocked by https://github.com/IronLanguages/ironpython3/issues/541
8+
RunCondition=NOT $(IS_MONO)
9+
Reason=Exception on adding DocTestSuite
1410

1511
[IronPython.test_class]
1612
Ignore=true
@@ -198,17 +194,13 @@ IsolationLevel=PROCESS # changes the locale which can cause other tests to fail
198194
[IronPython.modules.system_related.test_nt]
199195
RunCondition=NOT $(IS_POSIX)
200196

201-
[IronPython.modules.system_related.test_sys]
202-
RunCondition=NOT $(IS_POSIX)
203-
Reason=Blocked by https://github.com/IronLanguages/ironpython3/issues/541
204-
205197
[IronPython.modules.system_related.test_sys_getframe]
206198
IsolationLevel=PROCESS # https://github.com/IronLanguages/ironpython3/issues/489
207199
FullFrames=true
208200

209201
[IronPython.modules.system_related.test_thread]
210-
RunCondition=NOT $(IS_POSIX)
211-
Reason=Blocked by https://github.com/IronLanguages/ironpython3/issues/541
202+
RunCondition=NOT $(IS_MONO)
203+
Reason=Blocked by https://github.com/IronLanguages/ironpython3/issues/937
212204

213205
[IronPython.scripts.test_builder]
214206
Ignore=true

Src/StdLib/Lib/subprocess.py

Lines changed: 126 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -345,6 +345,7 @@ class Popen(args, bufsize=-1, executable=None,
345345
"""
346346

347347
import sys
348+
ironpython = (sys.implementation.name == 'ironpython')
348349
mswindows = (sys.platform == "win32")
349350

350351
import io
@@ -401,6 +402,11 @@ class STARTUPINFO:
401402
hStdOutput = None
402403
hStdError = None
403404
wShowWindow = 0
405+
elif ironpython:
406+
import threading
407+
import clr
408+
clr.AddReference("System")
409+
from System.Diagnostics import Process
404410
else:
405411
import _posixsubprocess
406412
import select
@@ -457,6 +463,8 @@ def __repr__(self):
457463

458464
__del__ = Close
459465
__str__ = __repr__
466+
elif ironpython:
467+
__all__.extend(["Process"])
460468

461469
try:
462470
MAXFD = os.sysconf("SC_OPEN_MAX")
@@ -778,6 +786,13 @@ def __init__(self, args, bufsize=-1, executable=None,
778786
raise ValueError(
779787
"close_fds is not supported on Windows platforms"
780788
" if you redirect stdin/stdout/stderr")
789+
790+
elif ironpython:
791+
if preexec_fn is not None:
792+
raise ValueError("preexec_fn is not supported by IronPython")
793+
# if close_fds:
794+
# raise ValueError("close_fds is not supported by IronPython")
795+
781796
else:
782797
# POSIX
783798
if close_fds is _PLATFORM_DEFAULT_CLOSE_FDS:
@@ -984,7 +999,6 @@ def _check_timeout(self, endtime, orig_timeout):
984999
if _time() > endtime:
9851000
raise TimeoutExpired(self.args, orig_timeout)
9861001

987-
9881002
if mswindows:
9891003
#
9901004
# Windows methods
@@ -1272,6 +1286,117 @@ def terminate(self):
12721286

12731287
kill = terminate
12741288

1289+
1290+
elif ironpython:
1291+
#
1292+
# Dotnet methods
1293+
#
1294+
def _get_handles(self, stdin, stdout, stderr):
1295+
# Can't get redirect file before Process.Start() is called
1296+
# postpone it to _execute_child
1297+
return (stdin, -1, -1, stdout, -1, stderr)
1298+
1299+
def _execute_child(self, args, executable, preexec_fn, close_fds,
1300+
pass_fds, cwd, env,
1301+
startupinfo, creationflags, shell,
1302+
stdin, unused_p2cwrite,
1303+
unused_c2pread, stdout,
1304+
unused_errread, stderr,
1305+
unused_restore_signals, unused_start_new_session):
1306+
"""Execute program (Dotnet version)"""
1307+
p = Process()
1308+
s = p.StartInfo
1309+
1310+
if env:
1311+
for k, v in env.items():
1312+
s.Environment[k] = v
1313+
1314+
if shell:
1315+
if not isinstance(args, str):
1316+
args = list2cmdline(args)
1317+
# escape backslash and double quote
1318+
args = ''.join('\\' + c if c in {'\\', '"'} else c for c in args)
1319+
s.Arguments = '-c "{}"'.format(args)
1320+
s.FileName = executable or '/bin/sh'
1321+
else:
1322+
if not isinstance(args, str):
1323+
s.FileName = args[0]
1324+
s.Arguments = list2cmdline(args[1:])
1325+
else:
1326+
s.FileName = args
1327+
1328+
s.RedirectStandardInput = stdin is not None
1329+
s.RedirectStandardOutput = stdout is not None
1330+
s.RedirectStandardError = stderr is not None
1331+
s.WorkingDirectory = cwd
1332+
s.UseShellExecute = False
1333+
1334+
p.Start()
1335+
1336+
self.pid = p.Id
1337+
self._child_created = True
1338+
self._handle = p
1339+
1340+
if stdin == PIPE:
1341+
self.stdin = open(p.StandardInput.BaseStream)
1342+
if stdout == PIPE:
1343+
self.stdout = io.BufferedReader(open(p.StandardOutput.BaseStream))
1344+
if stderr == PIPE:
1345+
self.stderr = io.BufferedReader(open(p.StandardError.BaseStream))
1346+
1347+
# dotnet can't redirect stdio to file/stream, thus has to feed from parent
1348+
if stdin not in (None, DEVNULL, PIPE):
1349+
# assume file-like object
1350+
input = stdin.read()
1351+
with open(self._handle.StandardInput.BaseStream) as f:
1352+
f.write(input)
1353+
1354+
def _internal_poll(self):
1355+
"""Check if child process has terminated. Returns returncode
1356+
attribute.
1357+
1358+
This method is called by __del__, so it can only refer to objects
1359+
in its local scope.
1360+
1361+
"""
1362+
if self.returncode is None and self._handle.HasExited:
1363+
self.returncode = self._handle.ExitCode
1364+
return self.returncode
1365+
1366+
def wait(self, timeout=None, endtime=None):
1367+
"""Wait for child process to terminate. Returns returncode
1368+
attribute."""
1369+
if endtime is not None:
1370+
timeout = self._remaining_time(endtime)
1371+
if timeout is None:
1372+
self._handle.WaitForExit()
1373+
else:
1374+
self._handle.WaitForExit(int(timeout * 1000))
1375+
self.returncode = self._handle.ExitCode
1376+
return self.returncode
1377+
1378+
def _communicate(self, input, endtime, orig_timeout):
1379+
# .NET framework caches stdout and stderr
1380+
# so we can simply wait then read
1381+
if self.stdin:
1382+
if input:
1383+
self.stdin.write(input)
1384+
self.stdin.close()
1385+
1386+
if orig_timeout is not None:
1387+
self.wait(endtime=endtime)
1388+
1389+
return (
1390+
self.stdout.read() if self.stdout else None,
1391+
self.stderr.read() if self.stderr else None,
1392+
)
1393+
1394+
def terminate(self):
1395+
"""Terminates the process."""
1396+
self._handle.Kill()
1397+
1398+
kill = terminate
1399+
12751400
else:
12761401
#
12771402
# POSIX methods

Src/StdLib/Lib/test/regrtest.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,8 @@
165165
# Therefore it is necessary to absolutize manually the __file__ and __path__ of
166166
# the packages to prevent later imports to fail when the CWD is different.
167167
for module in sys.modules.values():
168+
if type(module).__name__ == 'namespace#': # namespace in IronPython
169+
continue
168170
if hasattr(module, '__path__'):
169171
module.__path__ = [os.path.abspath(path) for path in module.__path__]
170172
if hasattr(module, '__file__'):

Src/StdLib/Lib/test/test_uuid.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import io
55
import os
66
import shutil
7+
import sys
78
import uuid
89

910
def importable(name):
@@ -386,7 +387,10 @@ def test_uuid5(self):
386387
equal(u, uuid.UUID(v))
387388
equal(str(u), v)
388389

389-
@unittest.skipUnless(os.name == 'posix', 'requires Posix')
390+
@unittest.skipUnless(
391+
os.name == 'posix' and sys.implementation.name != 'ironpython',
392+
'requires os.fork'
393+
)
390394
def testIssue8621(self):
391395
# On at least some versions of OSX uuid.uuid4 generates
392396
# the same sequence of UUIDs in the parent and any

0 commit comments

Comments
 (0)