Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
195 changes: 194 additions & 1 deletion src/core/IronPython.Modules/fcntl.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,20 @@
#nullable enable

using System;
using System.Diagnostics;
using System.Numerics;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Runtime.Versioning;

using Mono.Unix;
using Mono.Unix.Native;

using Microsoft.Scripting.Runtime;

using IronPython.Runtime;
using IronPython.Runtime.Operations;


[assembly: PythonModule("fcntl", typeof(IronPython.Modules.PythonFcntl), PlatformsAttribute.PlatformFamily.Unix)]
namespace IronPython.Modules;
Expand All @@ -27,6 +35,92 @@ a file object.
""";


#region fcntl

[LightThrowing]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the light throwing path expected to occur in normal use? If not then I would consider making a regular exception.

Similar question for the other light throwing methods below.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It depends on the command but yes, sometimes. Those errors are very inexpensive to return so the usage pattern for some commands is simply try/error. For example handling errors EAGAIN, EWOULDBLOCK., EACCES. Having LightThrowing makes it relatively inexpensive here too. Nota bene the code still throws regular exceptions for programming errors like invalid value. That's intentional so that the errors do not go unnoticed when these calls are done from C#.

public static object fcntl(int fd, int cmd, [NotNone] Bytes arg) {
CheckFileDescriptor(fd);

const int maxArgSize = 1024; // 1 KiB
int argSize = arg.Count;
if (argSize > maxArgSize) {
throw PythonOps.ValueError("fcntl bytes arg too long");
}

if (!NativeConvert.TryToFcntlCommand(cmd, out FcntlCommand fcntlCommand)) {
throw PythonOps.OSError(PythonErrno.EINVAL, "unsupported fcntl command");
}

var buf = new byte[maxArgSize];
Array.Copy(arg.UnsafeByteArray, buf, argSize);

int result;
Errno errno;
unsafe {
fixed (byte* ptr = buf) {
do {
result = Syscall.fcntl(fd, fcntlCommand, (IntPtr)ptr);
} while (UnixMarshal.ShouldRetrySyscall(result, out errno));
}
}

if (result == -1) {
return LightExceptions.Throw(PythonNT.GetOsError(NativeConvert.FromErrno(errno)));
}
byte[] response = new byte[argSize];
Array.Copy(buf, response, argSize);
return Bytes.Make(response);
}


[LightThrowing]
public static object fcntl(int fd, int cmd, [Optional] object? arg) {
CheckFileDescriptor(fd);

long data = arg switch {
Missing => 0,
int i => i,
uint ui => ui,
long l => l,
ulong ul => (long)ul,
BigInteger bi => (long)bi,
Extensible<BigInteger> ebi => (long)ebi.Value,
_ => throw PythonOps.TypeErrorForBadInstance("integer argument expected, got {0}", arg)
};

if (!NativeConvert.TryToFcntlCommand(cmd, out FcntlCommand fcntlCommand)) {
throw PythonOps.OSError(PythonErrno.EINVAL, "unsupported fcntl command");
}

int result;
Errno errno;
do {
result = Syscall.fcntl(fd, fcntlCommand, data);
} while (UnixMarshal.ShouldRetrySyscall(result, out errno));

if (result == -1) {
return LightExceptions.Throw(PythonNT.GetOsError(NativeConvert.FromErrno(errno)));
}
return ScriptingRuntimeHelpers.Int32ToObject(result);
}


[LightThrowing]
public static object fcntl(CodeContext context, object? fd, int cmd, object? arg = null) {
int fileno = GetFileDescriptor(context, fd);

if (arg is Bytes bytes) {
return fcntl(fileno, cmd, bytes);
}

return fcntl(fileno, cmd, arg);
}

#endregion


#region ioctl

// supporting fcntl.ioctl(fileno, termios.TIOCGWINSZ, buf)
// where buf = array.array('h', [0, 0, 0, 0])
public static object ioctl(CodeContext context, int fd, int cmd, [NotNone] IBufferProtocol arg, int mutate_flag = 1) {
Expand All @@ -53,10 +147,109 @@ public static object ioctl(CodeContext context, int fd, int cmd, [NotNone] IBuff
throw new NotImplementedException($"ioctl: unsupported command {cmd}");
}

#endregion


#region flock

[DllImport("libc", SetLastError = true, EntryPoint = "flock")]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wonder what happens on Alpine. 😉

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will check 🤷

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It works! However, Mono.Unix doesn't load, missing dependency ld-linux-aarch64.so.1, which makes ipy pretty unusable. Even when I installed extra package gcompat (GNU C Library compatibility layer for musl, providing ld), Mono.Unix was still failing with __sprintf_chk: symbol not found.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess my comment was more of a joke, but still unfortunate that Mono.Unix doesn't work on it. 😢

private static extern int _flock(int fd, int op);

[LightThrowing]
public static object? flock(int fd, int operation) {
CheckFileDescriptor(fd);

int result;
int errno = 0;
do {
result = _flock(fd, operation);
} while (result == -1 && (errno = Marshal.GetLastWin32Error()) == PythonErrno.EINTR);

if (result == -1) {
return LightExceptions.Throw(PythonNT.GetOsError(errno));
}
return null;
}


[LightThrowing]
public static object? flock(CodeContext context, object? fd, int operation)
=> flock(GetFileDescriptor(context, fd), operation);

#endregion


#region lockf

[LightThrowing]
public static object? lockf(int fd, int cmd, long len = 0, long start = 0, int whence = 0) {
CheckFileDescriptor(fd);

Flock flock = new() {
l_whence = (SeekFlags)whence,
l_start = start,
l_len = len
};
if (cmd == LOCK_UN) {
flock.l_type = LockType.F_UNLCK;
} else if ((cmd & LOCK_SH) != 0) {
flock.l_type = LockType.F_RDLCK;
} else if ((cmd & LOCK_EX) != 0) {
flock.l_type = LockType.F_WRLCK;
} else {
throw PythonOps.ValueError("unrecognized lockf argument");
}

int result;
Errno errno;
do {
result = Syscall.fcntl(fd, (cmd & LOCK_NB) != 0 ? FcntlCommand.F_SETLK : FcntlCommand.F_SETLKW, ref flock);
} while (UnixMarshal.ShouldRetrySyscall(result, out errno));

if (result == -1) {
return LightExceptions.Throw(PythonNT.GetOsError(NativeConvert.FromErrno(errno)));
}
return null;
}


[LightThrowing]
public static object? lockf(CodeContext context, object? fd, int cmd, long len = 0, long start = 0, int whence = 0)
=> lockf(GetFileDescriptor(context, fd), cmd, len, start, whence);

#endregion


#region Helper Methods

private static int GetFileDescriptor(CodeContext context, object? obj) {
if (!PythonOps.TryGetBoundAttr(context, obj, "fileno", out object? filenoMeth)) {
throw PythonOps.TypeError("argument must be an int, or have a fileno() method.");
}
return PythonCalls.Call(context, filenoMeth) switch {
int i => i,
uint ui => (int)ui,
BigInteger bi => (int)bi,
Extensible<BigInteger> ebi => (int)ebi.Value,
_ => throw PythonOps.TypeError("fileno() returned a non-integer")
};
}


private static void CheckFileDescriptor(int fd) {
if (fd < 0) {
throw PythonOps.ValueError("file descriptor cannot be a negative integer ({0})", fd);
}
}

#endregion


// FD Flags
public static int FD_CLOEXEC = 1;
public static int FASYNC => RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? 0x0040 : 0x2000;

// O_* flags under F* name
public static int FASYNC => RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? 0x0040 : 0x2000; // O_ASYNC


#region Generated FD Commands
Expand Down
9 changes: 2 additions & 7 deletions tests/IronPython.Tests/Cases/CPythonCasesManifest.ini
Original file line number Diff line number Diff line change
Expand Up @@ -400,11 +400,6 @@ Ignore=true
[CPython.test_faulthandler]
Ignore=true

[CPython.test_fcntl]
RunCondition=$(IS_POSIX)
Ignore=true
Reason=ImportError: No module named fcntl

[CPython.test_file]
IsolationLevel=PROCESS # https://github.com/IronLanguages/ironpython3/issues/489

Expand Down Expand Up @@ -514,7 +509,7 @@ Ignore=true
[CPython.test_ioctl]
RunCondition=$(IS_POSIX)
Ignore=true
Reason=unittest.case.SkipTest: No module named 'fcntl'
Reason=unittest.case.SkipTest: module 'termios' has no attribute 'TIOCGPGRP', 'fcntl.ioctl' is a mock

[CPython.test_ipaddress]
Ignore=true
Expand Down Expand Up @@ -724,7 +719,7 @@ Ignore=true
[CPython.test_pty]
RunCondition=$(IS_POSIX)
Ignore=true
Reason=unittest.case.SkipTest: No module named 'fcntl'
Reason=Missing constants in 'termios', 'signal', 'termios' implementation is a stub

[CPython.test_pulldom]
Ignore=true
Expand Down