Skip to content

Commit 20b0669

Browse files
committed
Complete implementing genuine file descriptors
1 parent 827caee commit 20b0669

File tree

5 files changed

+127
-65
lines changed

5 files changed

+127
-65
lines changed

Src/IronPython.Modules/nt.cs

Lines changed: 60 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -351,19 +351,24 @@ public static int dup(CodeContext/*!*/ context, int fd) {
351351
PythonFileManager fileManager = context.LanguageContext.FileManager;
352352

353353
StreamBox streams = fileManager.GetStreams(fd); // OSError if fd not valid
354-
fileManager.EnsureRefStreams(streams);
355-
fileManager.AddRefStreams(streams);
356354
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) {
355+
fileManager.EnsureRefStreams(streams);
356+
fileManager.AddRefStreams(streams);
357357
return fileManager.Add(new(streams));
358358
} else {
359-
return fileManager.Add(UnixDup(fd), new(streams));
360-
}
361-
362-
// Isolate Mono.Unix from the rest of the method so that we don't try to load the Mono.Unix assembly on Windows.
363-
static int UnixDup(int fd) {
364-
int res = Mono.Unix.Native.Syscall.dup(fd);
365-
if (res < 0) throw GetLastUnixError();
366-
return res;
359+
if (!streams.IsSingleStream && fd is 1 or 2) {
360+
// If there is a separate write stream, dupping over stout or sderr uses write stream's file descriptor
361+
fd = streams.WriteStream is FileStream fs ? fs.SafeFileHandle.DangerousGetHandle().ToInt32() : fd;
362+
}
363+
int fd2 = UnixDup(fd, -1, out Stream? dupstream);
364+
if (dupstream is not null) {
365+
return fileManager.Add(fd2, new(dupstream));
366+
} else {
367+
// Share the same set of streams between the original and the dupped descriptor
368+
fileManager.EnsureRefStreams(streams);
369+
fileManager.AddRefStreams(streams);
370+
return fileManager.Add(fd2, new(streams));
371+
}
367372
}
368373
}
369374

@@ -384,22 +389,45 @@ public static int dup2(CodeContext/*!*/ context, int fd, int fd2) {
384389
close(context, fd2);
385390
}
386391

387-
// TODO: race condition: `open` or `dup` on another thread may occupy fd2
392+
// TODO: race condition: `open` or `dup` on another thread may occupy fd2 (simulated desctiptors only)
388393

389-
fileManager.EnsureRefStreams(streams);
390-
fileManager.AddRefStreams(streams);
391394
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) {
395+
fileManager.EnsureRefStreams(streams);
396+
fileManager.AddRefStreams(streams);
392397
return fileManager.Add(fd2, new(streams));
393398
} else {
394-
return fileManager.Add(UnixDup2(fd, fd2), new(streams));
399+
if (!streams.IsSingleStream && fd is 1 or 2) {
400+
// If there is a separate write stream, dupping over stout or sderr uses write stream's file descriptor
401+
fd = streams.WriteStream is FileStream fs ? fs.SafeFileHandle.DangerousGetHandle().ToInt32() : fd;
402+
}
403+
fd2 = UnixDup(fd, fd2, out Stream? dupstream); // closes fd2 atomically if reopened in the meantime
404+
fileManager.Remove(fd2);
405+
if (dupstream is not null) {
406+
return fileManager.Add(fd2, new(dupstream));
407+
} else {
408+
// Share the same set of streams between the original and the dupped descriptor
409+
fileManager.EnsureRefStreams(streams);
410+
fileManager.AddRefStreams(streams);
411+
return fileManager.Add(fd2, new(streams));
412+
}
395413
}
414+
}
396415

397-
// Isolate Mono.Unix from the rest of the method so that we don't try to load the Mono.Unix assembly on Windows.
398-
static int UnixDup2(int fd, int fd2) {
399-
int res = Mono.Unix.Native.Syscall.dup2(fd, fd2);
400-
if (res < 0) throw GetLastUnixError();
401-
return res;
416+
417+
private static int UnixDup(int fd, int fd2, out Stream? stream) {
418+
int res = fd2 < 0 ? Mono.Unix.Native.Syscall.dup(fd) : Mono.Unix.Native.Syscall.dup2(fd, fd2);
419+
if (res < 0) throw GetLastUnixError();
420+
if (ClrModule.IsMono) {
421+
// This does not work on .NET, probably because .NET FileStream is not aware of Mono.Unix.UnxiStream
422+
stream = new Mono.Unix.UnixStream(res, ownsHandle: true);
423+
} else {
424+
// This does not work 100% correctly on .NET, probably because each FileStream has its own read/write cursor
425+
// (it should be shared between dupped descriptors)
426+
//stream = new FileStream(new SafeFileHandle((IntPtr)res, ownsHandle: true), FileAccess.ReadWrite);
427+
// Accidentaly, this would also not work on Mono: https://github.com/mono/mono/issues/12783
428+
stream = null; // Handle stream sharing in PythonFileManager
402429
}
430+
return res;
403431
}
404432

405433
#if FEATURE_PROCESS
@@ -849,25 +877,27 @@ public static object open(CodeContext/*!*/ context, [NotNone] string path, int f
849877
FileMode fileMode = FileModeFromFlags(flags);
850878
FileAccess access = FileAccessFromFlags(flags);
851879
FileOptions options = FileOptionsFromFlags(flags);
852-
Stream fs;
880+
Stream s;
881+
FileStream? fs;
853882
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows) && IsNulFile(path)) {
854-
fs = Stream.Null;
883+
fs = null;
884+
s = Stream.Null;
855885
} else if (access == FileAccess.Read && (fileMode == FileMode.CreateNew || fileMode == FileMode.Create || fileMode == FileMode.Append)) {
856886
// .NET doesn't allow Create/CreateNew w/ access == Read, so create the file, then close it, then
857887
// open it again w/ just read access.
858888
fs = new FileStream(path, fileMode, FileAccess.Write, FileShare.None);
859889
fs.Close();
860-
fs = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite, DefaultBufferSize, options);
890+
s = fs = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite, DefaultBufferSize, options);
861891
} else if (access == FileAccess.ReadWrite && fileMode == FileMode.Append) {
862-
fs = new FileStream(path, FileMode.Append, FileAccess.Write, FileShare.ReadWrite, DefaultBufferSize, options);
892+
s = fs = new FileStream(path, FileMode.Append, FileAccess.Write, FileShare.ReadWrite, DefaultBufferSize, options);
863893
} else {
864-
fs = new FileStream(path, fileMode, access, FileShare.ReadWrite, DefaultBufferSize, options);
894+
s = fs = new FileStream(path, fileMode, access, FileShare.ReadWrite, DefaultBufferSize, options);
865895
}
866896

867897
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) {
868-
return context.LanguageContext.FileManager.Add(new(fs));
898+
return context.LanguageContext.FileManager.Add(new(s));
869899
} else {
870-
return context.LanguageContext.FileManager.Add((int)fs.SafeFileHandle.DangerousGetHandle(), new(fs));
900+
return context.LanguageContext.FileManager.Add((int)fs!.SafeFileHandle.DangerousGetHandle(), new(s));
871901
}
872902
} catch (Exception e) {
873903
throw ToPythonException(e, path);
@@ -916,14 +946,14 @@ public static PythonTuple pipe(CodeContext context) {
916946
} else {
917947
var pipeStreams = CreatePipeStreamsUnix();
918948
return PythonTuple.MakeTuple(
919-
manager.Add((int)pipeStreams.Item1.SafeFileHandle.DangerousGetHandle(), new(pipeStreams.Item1)),
920-
manager.Add((int)pipeStreams.Item2.SafeFileHandle.DangerousGetHandle(), new(pipeStreams.Item2))
949+
manager.Add(pipeStreams.Item1, new(pipeStreams.Item2)),
950+
manager.Add(pipeStreams.Item3, new(pipeStreams.Item4))
921951
);
922952
}
923953

924-
static Tuple<Stream, Stream> CreatePipeStreamsUnix() {
954+
static Tuple<int, Stream, int, Stream> CreatePipeStreamsUnix() {
925955
Mono.Unix.UnixPipes pipes = Mono.Unix.UnixPipes.CreatePipes();
926-
return Tuple.Create<Stream, Stream>(pipes.Reading, pipes.Writing);
956+
return Tuple.Create<int, Stream, int, Stream>(pipes.Reading.Handle, pipes.Reading, pipes.Writing.Handle, pipes.Writing);
927957
}
928958
}
929959
#endif

Src/IronPython/Modules/_fileio.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -239,7 +239,7 @@ static Exception BadMode(string mode) {
239239

240240
[Documentation("close() -> None. Close the file.\n\n"
241241
+ "A closed file cannot be used for further I/O operations. close() may be"
242-
+ "called more than once without error. Changes the fileno to -1."
242+
+ "called more than once without error."
243243
)]
244244
public override void close(CodeContext/*!*/ context) {
245245
if (_closed) {

Src/IronPython/Runtime/PythonFileManager.cs

Lines changed: 34 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
using System.Collections.Generic;
1111
using System.Diagnostics.CodeAnalysis;
1212
using System.IO;
13+
using System.Runtime.InteropServices;
1314
using System.Runtime.Versioning;
1415

1516
using Microsoft.Scripting.Runtime;
@@ -87,13 +88,16 @@ public bool IsConsoleStream() {
8788

8889
// Isolate Mono.Unix from the rest of the method so that we don't try to load the Mono.Unix assembly on Windows.
8990
bool isattyUnix() {
90-
// TODO: console streams may be dupped to differend FD numbers, do not use hard-coded 0, 1, 2
91-
return StreamType switch {
92-
ConsoleStreamType.Input => Mono.Unix.Native.Syscall.isatty(0),
93-
ConsoleStreamType.Output => Mono.Unix.Native.Syscall.isatty(1),
94-
ConsoleStreamType.ErrorOutput => Mono.Unix.Native.Syscall.isatty(2),
95-
_ => false
96-
};
91+
if (Id >= 0) {
92+
return Mono.Unix.Native.Syscall.isatty(Id);
93+
} else {
94+
return StreamType switch {
95+
ConsoleStreamType.Input => Mono.Unix.Native.Syscall.isatty(0),
96+
ConsoleStreamType.Output => Mono.Unix.Native.Syscall.isatty(1),
97+
ConsoleStreamType.ErrorOutput => Mono.Unix.Native.Syscall.isatty(2),
98+
_ => false
99+
};
100+
}
97101
}
98102
}
99103

@@ -213,7 +217,8 @@ internal class PythonFileManager {
213217
private int _current = _offset; // lowest potentially unused key in _objects at or above _offset
214218
private readonly ConcurrentDictionary<Stream, int> _refs = new();
215219

216-
// Mandatory Add for Unix, on Windows only for dup2 case
220+
// This version of Add is used with genuine file descriptors (Unix).
221+
// Exception: support dup2 on all frameworks/platfroms.
217222
public int Add(int id, StreamBox streams) {
218223
ContractUtils.RequiresNotNull(streams, nameof(streams));
219224
ContractUtils.Requires(streams.Id < 0, nameof(streams));
@@ -228,6 +233,8 @@ public int Add(int id, StreamBox streams) {
228233
}
229234
}
230235

236+
// This version of Add is used for emulated file descriptors.
237+
// Must not be used on Unix.
231238
[SupportedOSPlatform("windows")]
232239
public int Add(StreamBox streams) {
233240
ContractUtils.RequiresNotNull(streams, nameof(streams));
@@ -280,7 +287,13 @@ public int GetOrAssignId(StreamBox streams) {
280287
lock (_synchObject) {
281288
int res = streams.Id;
282289
if (res == -1) {
283-
res = Add(streams);
290+
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) {
291+
res = Add(streams);
292+
} else {
293+
res = GetGenuineFileDescriptor(streams.ReadStream);
294+
if (res < 0) throw new InvalidOperationException("stream not associated with a file descriptor");
295+
Add(res, streams);
296+
}
284297
}
285298
return res;
286299
}
@@ -313,5 +326,17 @@ public bool DerefStreamsAndCloseIfLast(StreamBox streams) {
313326
public bool ValidateFdRange(int fd) {
314327
return fd >= 0 && fd < LIMIT_OFILE;
315328
}
329+
330+
[UnsupportedOSPlatform("windows")]
331+
private static int GetGenuineFileDescriptor(Stream stream) {
332+
return stream switch {
333+
FileStream fs => checked((int)fs.SafeFileHandle.DangerousGetHandle()),
334+
#if FEATURE_PIPES
335+
System.IO.Pipes.PipeStream ps => checked((int)ps.SafePipeHandle.DangerousGetHandle()),
336+
#endif
337+
Mono.Unix.UnixStream us => us.Handle,
338+
_ => -1
339+
};
340+
}
316341
}
317342
}

Tests/modules/io_related/test_fd.py

Lines changed: 27 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
import sys
1010
import unittest
1111

12-
from iptest import IronPythonTestCase, is_cli, run_test
12+
from iptest import IronPythonTestCase, is_cli, is_posix, run_test
1313
from threading import Timer
1414

1515
flags = os.O_CREAT | os.O_TRUNC | os.O_RDWR
@@ -45,12 +45,11 @@ def test_dup2(self):
4545

4646
# Assigning to self must be a no-op.
4747
self.assertEqual(os.dup2(fd, fd), fd if is_cli or sys.version_info >= (3,7) else None)
48-
self.assertTrue(is_open(fd))
4948

5049
# The source must be valid.
51-
self.assertRaisesMessage(OSError, "[Errno 9] Bad file descriptor", os.dup2, -1, fd)
50+
self.assertRaisesMessage(OSError, "[Errno 9] Bad file descriptor" if is_cli or sys.version_info >= (3,5) else "[Errno 0] Error", os.dup2, -1, fd)
5251
self.assertTrue(is_open(fd))
53-
self.assertRaisesMessage(OSError, "[Errno 9] Bad file descriptor", os.dup2, 99, fd)
52+
self.assertRaisesMessage(OSError, "[Errno 9] Bad file descriptor", os.dup2, fd + 10000, fd)
5453
self.assertTrue(is_open(fd))
5554

5655
# If the source is not open, then the destination is unaffected.
@@ -67,16 +66,16 @@ def test_dup2(self):
6766
# Using dup2 can skip fds.
6867
self.assertEqual(os.dup2(fd, fd + 2), fd + 2 if is_cli or sys.version_info >= (3,7) else None)
6968
self.assertTrue(is_open(fd))
70-
self.assertTrue(not is_open(fd + 1))
69+
self.assertFalse(is_open(fd + 1))
7170
self.assertTrue(is_open(fd + 2))
7271

7372
# Verify that dup2 closes the previous occupant of a fd.
74-
self.assertEqual(os.open(os.devnull, os.O_RDWR, 0o600), fd + 1)
75-
self.assertEqual(os.dup2(fd + 1, fd), fd if is_cli or sys.version_info >= (3,7) else None)
73+
fdn = os.open(os.devnull, os.O_RDWR, 0o600)
74+
self.assertEqual(os.dup2(fdn, fd), fd if is_cli or sys.version_info >= (3,7) else None)
7675

7776
self.assertTrue(is_open_nul(fd))
78-
self.assertTrue(is_open_nul(fd + 1))
79-
os.close(fd + 1)
77+
self.assertTrue(is_open_nul(fdn))
78+
os.close(fdn)
8079
self.assertTrue(is_open_nul(fd))
8180
self.assertEqual(os.write(fd, b"1"), 1)
8281

@@ -87,7 +86,7 @@ def test_dup2(self):
8786
self.assertEqual(os.read(fd, 1), b"2")
8887

8988
os.close(fd)
90-
# fd+1 is already closed
89+
# fdn is already closed
9190
os.close(fd + 2)
9291
os.unlink(test_filename)
9392

@@ -101,29 +100,33 @@ def test_dup(self):
101100

102101
# The source must be valid.
103102
self.assertRaisesMessage(OSError, "[Errno 9] Bad file descriptor", os.dup, -1)
104-
self.assertRaisesMessage(OSError, "[Errno 9] Bad file descriptor", os.dup, 99)
103+
self.assertRaisesMessage(OSError, "[Errno 9] Bad file descriptor", os.dup, fd1 + 10000)
105104

106105
# Test basic functionality.
107106
fd2 = os.dup(fd1)
108-
self.assertTrue(fd2 == fd1 + 1)
107+
if not (is_cli and is_posix):
108+
# On IronPython/Posix, the first call to dup or dup2 may load Mono.Unix.dll and the corresponding `.so`
109+
# This makes the fd numbers less predictable
110+
self.assertEqual(fd2, fd1 + 1)
109111
self.assertTrue(is_open(fd2))
110112
self.assertTrue(is_open(fd1))
111113
os.close(fd1)
112-
self.assertTrue(not is_open(fd1))
114+
self.assertFalse(is_open(fd1))
113115
self.assertTrue(is_open(fd2))
114116

115-
# dup uses the lowest-numbered unused descriptor for the new descriptor.
116117
fd3 = os.dup(fd2)
117-
self.assertEqual(fd3, fd1)
118+
# dup uses the lowest-numbered unused descriptor for the new descriptor.
119+
if not (is_cli and is_posix):
120+
self.assertEqual(fd3, fd1)
118121

119-
# writing though the duplicated fd writes to the same file
120-
self.assertEqual(os.write(fd2, b"fd2"), 3)
121-
self.assertEqual(os.write(fd3, b"fd3"), 3)
122-
self.assertEqual(os.write(fd2, b"fd2-again"), 9)
122+
# writing through the duplicated fd writes to the same file
123+
self.assertEqual(os.write(fd2, b"(fd2)"), 5)
124+
self.assertEqual(os.write(fd3, b"(=====fd3=====)"), 15)
125+
self.assertEqual(os.write(fd2, b"(fd2-again)"), 11)
123126
os.close(fd3)
124127

125128
self.assertEqual(os.lseek(fd2, 0, os.SEEK_SET), 0)
126-
self.assertEqual(os.read(fd2, 15), b"fd2fd3fd2-again")
129+
self.assertEqual(os.read(fd2, 5 + 15 + 11), b"(fd2)(=====fd3=====)(fd2-again)")
127130

128131
# cleanup
129132
os.close(fd2)
@@ -133,20 +136,20 @@ def test_dup_file(self):
133136
test_filename = "tmp_%d.dup-file.test" % os.getpid()
134137

135138
file1 = open(test_filename, 'w+')
136-
file1.write("file1")
139+
file1.write("(file1)")
137140
file1.flush()
138141

139142
fd2 = os.dup(file1.fileno())
140143
file2 = open(fd2, 'w+')
141144
self.assertNotEqual(file1.fileno(), file2.fileno())
142145

143-
file2.write("file2")
146+
file2.write("(======file2======)")
144147
file2.flush()
145-
file1.write("file1-again")
148+
file1.write("(file1-again)")
146149
file1.close()
147150

148151
file2.seek(0)
149-
self.assertEqual(file2.read(), "file1file2file1-again")
152+
self.assertEqual(file2.read(), "(file1)(======file2======)(file1-again)")
150153

151154
file2.close()
152155
os.unlink(test_filename)

Tests/test_file.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -787,6 +787,10 @@ def test_open_abplus(self):
787787
with open(self.temp_file, "ab+") as f:
788788
f.write(b"abc")
789789
f.seek(0)
790-
self.assertEqual(f.read(), b"abc")
790+
self.assertEqual(f.read(2), b"ab")
791+
f.write(b"def")
792+
self.assertEqual(f.read(2), b"")
793+
f.seek(0)
794+
self.assertEqual(f.read(6), b"abcdef")
791795

792796
run_test(__name__)

0 commit comments

Comments
 (0)