Skip to content

Commit c02f027

Browse files
authored
Use PosixFileStream for files on POSIX (#1855)
* Use PosixFileStream for files on POSIX * Improve non-buffering strategy for FileStream * Ignore OSException along with IOException * Fix file size checks in constructor * Implement proper lifetime management of handles * Describe Rules of Engagement * Implement mmap.resize on POSIX * Update after review
1 parent 9f4ebc1 commit c02f027

File tree

8 files changed

+746
-114
lines changed

8 files changed

+746
-114
lines changed

Src/IronPython.Modules/_warnings.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -260,7 +260,7 @@ internal static void showwarning(CodeContext context, object message, PythonType
260260
((TextWriter)file).Write(text);
261261
} // unrecognized file type - warning is lost
262262
}
263-
} catch (IOException) {
263+
} catch (Exception ex) when (ex is IOException or OSException) {
264264
// invalid file - warning is lost
265265
}
266266
}

Src/IronPython.Modules/mmap.cs

Lines changed: 317 additions & 35 deletions
Large diffs are not rendered by default.

Src/IronPython.Modules/nt.cs

Lines changed: 88 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -431,22 +431,41 @@ public static int dup2(CodeContext/*!*/ context, int fd, int fd2) {
431431
}
432432

433433

434+
[SupportedOSPlatform("linux"), SupportedOSPlatform("osx")]
434435
private static int UnixDup(int fd, int fd2, out Stream? stream) {
435436
int res = fd2 < 0 ? Mono.Unix.Native.Syscall.dup(fd) : Mono.Unix.Native.Syscall.dup2(fd, fd2);
436437
if (res < 0) throw GetLastUnixError();
437438
if (ClrModule.IsMono) {
438-
// This does not work on .NET, probably because .NET FileStream is not aware of Mono.Unix.UnixStream
439-
stream = new Mono.Unix.UnixStream(res, ownsHandle: true);
439+
// Elaborate workaround on Mono to avoid UnixStream as out
440+
stream = new Mono.Unix.UnixStream(res, ownsHandle: false);
441+
FileAccess fileAccess = stream.CanWrite ? stream.CanRead ? FileAccess.ReadWrite : FileAccess.Write : FileAccess.Read;
442+
stream.Dispose();
443+
try {
444+
// FileStream on Mono created with a file descriptor might not work: https://github.com/mono/mono/issues/12783
445+
// Test if it does, without closing the handle if it doesn't
446+
var sfh = new SafeFileHandle((IntPtr)res, ownsHandle: false);
447+
stream = new FileStream(sfh, fileAccess);
448+
// No exception? Great! We can use FileStream.
449+
stream.Dispose();
450+
sfh.Dispose();
451+
stream = null; // Create outside of try block
452+
} catch (IOException) {
453+
// Fall back to UnixStream
454+
stream = new Mono.Unix.UnixStream(res, ownsHandle: true);
455+
}
456+
if (stream is null) {
457+
// FileStream is safe
458+
var sfh = new SafeFileHandle((IntPtr)res, ownsHandle: true);
459+
stream = new FileStream(sfh, fileAccess);
460+
}
440461
} else {
441-
// This does not work 100% correctly on .NET, probably because each FileStream has its own read/write cursor
442-
// (it should be shared between dupped descriptors)
443-
//stream = new FileStream(new SafeFileHandle((IntPtr)res, ownsHandle: true), FileAccess.ReadWrite);
444-
// Accidentaly, this would also not work on Mono: https://github.com/mono/mono/issues/12783
445-
stream = null; // Handle stream sharing in PythonFileManager
462+
// normal case
463+
stream = new PosixFileStream(res);
446464
}
447465
return res;
448466
}
449467

468+
450469
#if FEATURE_PROCESS
451470
/// <summary>
452471
/// single instance of environment dictionary is shared between multiple runtimes because the environment
@@ -470,6 +489,9 @@ public static object fstat(CodeContext/*!*/ context, int fd) {
470489
PythonFileManager fileManager = context.LanguageContext.FileManager;
471490

472491
if (fileManager.TryGetStreams(fd, out StreamBox? streams)) {
492+
if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux) || RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) {
493+
return fstatUnix(fd);
494+
}
473495
if (streams.IsConsoleStream()) return new stat_result(0x2000);
474496
if (streams.IsStandardIOStream()) return new stat_result(0x1000);
475497
if (StatStream(streams.ReadStream) is not null and var res) return res;
@@ -483,15 +505,9 @@ public static object fstat(CodeContext/*!*/ context, int fd) {
483505
#endif
484506
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) {
485507
if (ReferenceEquals(stream, Stream.Null)) return new stat_result(0x2000);
486-
} else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux) || RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) {
487-
if (IsUnixStream(stream)) return new stat_result(0x1000);
488508
}
489509
return null;
490510
}
491-
492-
static bool IsUnixStream(Stream stream) {
493-
return stream is Mono.Unix.UnixStream;
494-
}
495511
}
496512

497513
public static void fsync(CodeContext context, int fd) {
@@ -867,9 +883,16 @@ public static void mkdir(CodeContext context, [NotNone] Bytes path, [ParamDictio
867883
public static void mkdir(CodeContext context, object? path, [ParamDictionary, NotNone] IDictionary<string, object> kwargs, [NotNone] params object[] args)
868884
=> mkdir(ConvertToFsString(context, path, nameof(path)), kwargs, args);
869885

870-
private const int DefaultBufferSize = 4096;
886+
[Documentation("""
887+
open(path, flags, mode=511, *, dir_fd=None)
888+
889+
Open a file for low level IO. Returns a file descriptor (integer).
871890
872-
[Documentation("open(path, flags, mode=511, *, dir_fd=None)")]
891+
If dir_fd is not None, it should be a file descriptor open to a directory,
892+
and path should be relative; path will then be relative to that directory.
893+
dir_fd may not be implemented on your platform.
894+
If it is unavailable, using it will raise a NotImplementedError.
895+
""")]
873896
public static object open(CodeContext/*!*/ context, [NotNone] string path, int flags, [ParamDictionary, NotNone] IDictionary<string, object> kwargs, [NotNone] params object[] args) {
874897
var numArgs = args.Length;
875898
CheckOptionalArgsCount(numRegParms: 2, numOptPosParms: 1, numKwParms: 1, numArgs, kwargs.Count);
@@ -889,12 +912,28 @@ public static object open(CodeContext/*!*/ context, [NotNone] string path, int f
889912
}
890913
}
891914

915+
if ((RuntimeInformation.IsOSPlatform(OSPlatform.Linux) || RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) && !ClrModule.IsMono) {
916+
// Use PosixFileStream to operate on fd directly
917+
// On Mono, we must use FileStream due to limitations in MemoryMappedFile
918+
Stream s = PosixFileStream.Open(path, flags, unchecked((uint)mode), out int fd);
919+
if ((flags & O_APPEND) != 0) {
920+
s.Seek(0L, SeekOrigin.End);
921+
}
922+
return context.LanguageContext.FileManager.Add(fd, new(s));
923+
}
924+
892925
try {
926+
// FileStream buffer size must be >= 0 on .NET, and >= 1 on .NET Framework and Mono.
927+
// On .NET, buffer size 0 or 1 disables buffering.
928+
// On .NET Framework, buffer size 1 disables buffering.
929+
// On Mono, buffer size 1 makes writes of length >= 2 bypass the buffer.
930+
const int NoBuffering = 1;
931+
893932
FileMode fileMode = FileModeFromFlags(flags);
894933
FileAccess access = FileAccessFromFlags(flags);
895934
FileOptions options = FileOptionsFromFlags(flags);
896935
Stream s; // the stream opened to acces the file
897-
FileStream? fs; // downcast of s if s is FileStream (this is always the case on POSIX)
936+
FileStream? fs; // downcast of s if s is FileStream
898937
Stream? rs = null; // secondary read stream if needed, otherwise same as s
899938
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows) && IsNulFile(path)) {
900939
fs = null;
@@ -904,15 +943,15 @@ public static object open(CodeContext/*!*/ context, [NotNone] string path, int f
904943
// open it again w/ just read access.
905944
fs = new FileStream(path, fileMode, FileAccess.Write, FileShare.None);
906945
fs.Close();
907-
s = fs = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite, DefaultBufferSize, options);
946+
s = fs = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite, NoBuffering, options);
908947
} else if (access == FileAccess.ReadWrite && fileMode == FileMode.Append) {
909948
// .NET doesn't allow Append w/ access != Write, so open the file w/ Write
910949
// and a secondary stream w/ Read, then seek to the end.
911-
s = fs = new FileStream(path, FileMode.Append, FileAccess.Write, FileShare.ReadWrite, DefaultBufferSize, options);
912-
rs = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite, DefaultBufferSize, options);
950+
s = fs = new FileStream(path, FileMode.Append, FileAccess.Write, FileShare.ReadWrite, NoBuffering, options);
951+
rs = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite, NoBuffering, options);
913952
rs.Seek(0L, SeekOrigin.End);
914953
} else {
915-
s = fs = new FileStream(path, fileMode, access, FileShare.ReadWrite, DefaultBufferSize, options);
954+
s = fs = new FileStream(path, fileMode, access, FileShare.ReadWrite, NoBuffering, options);
916955
}
917956
rs ??= s;
918957

@@ -1436,6 +1475,13 @@ private static object statUnix(string path) {
14361475
return LightExceptions.Throw(GetLastUnixError(path));
14371476
}
14381477

1478+
private static object fstatUnix(int fd) {
1479+
if (Mono.Unix.Native.Syscall.fstat(fd, out Mono.Unix.Native.Stat buf) == 0) {
1480+
return new stat_result(buf);
1481+
}
1482+
return LightExceptions.Throw(GetLastUnixError());
1483+
}
1484+
14391485
private const int OPEN_EXISTING = 3;
14401486
private const int FILE_ATTRIBUTE_NORMAL = 0x00000080;
14411487
private const int FILE_READ_ATTRIBUTES = 0x0080;
@@ -1669,8 +1715,27 @@ public static void truncate(CodeContext context, object? path, BigInteger length
16691715
public static void truncate(CodeContext context, int fd, BigInteger length)
16701716
=> ftruncate(context, fd, length);
16711717

1672-
public static void ftruncate(CodeContext context, int fd, BigInteger length)
1673-
=> context.LanguageContext.FileManager.GetStreams(fd).Truncate((long)length);
1718+
public static void ftruncate(CodeContext context, int fd, BigInteger length) {
1719+
if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux) || RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) {
1720+
ftruncateUnix(fd, (long)length);
1721+
} else {
1722+
context.LanguageContext.FileManager.GetStreams(fd).Truncate((long)length);
1723+
}
1724+
}
1725+
1726+
1727+
[SupportedOSPlatform("linux"), SupportedOSPlatform("osx")]
1728+
internal static void ftruncateUnix(int fd, long length) {
1729+
int result;
1730+
Mono.Unix.Native.Errno errno;
1731+
do {
1732+
result = Mono.Unix.Native.Syscall.ftruncate(fd, length);
1733+
} while (Mono.Unix.UnixMarshal.ShouldRetrySyscall(result, out errno));
1734+
1735+
if (errno != 0)
1736+
throw GetOsError(Mono.Unix.Native.NativeConvert.FromErrno(errno));
1737+
}
1738+
16741739

16751740
#if FEATURE_FILESYSTEM
16761741
public static object times() {
@@ -2351,7 +2416,7 @@ private static Exception DirectoryExistsError(string? filename) {
23512416

23522417
#if FEATURE_NATIVE
23532418

2354-
private static Exception GetLastUnixError(string? filename = null, string? filename2 = null)
2419+
internal static Exception GetLastUnixError(string? filename = null, string? filename2 = null)
23552420
=> GetOsError(Mono.Unix.Native.NativeConvert.FromErrno(Mono.Unix.Native.Syscall.GetLastError()), filename, filename2);
23562421

23572422
#endif

0 commit comments

Comments
 (0)