Skip to content

Commit e7a937d

Browse files
authored
Implement fcntl syscalls (#1893)
* Implement syscalls fcntl and flock * Implement fcntl.lockf * Reorder fcntl functions to match documentation * Enable test_fcntl * Avoid using `Marshal.AllocHGlobal`
1 parent 578972c commit e7a937d

File tree

2 files changed

+196
-8
lines changed

2 files changed

+196
-8
lines changed

src/core/IronPython.Modules/fcntl.cs

Lines changed: 194 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,20 @@
55
#nullable enable
66

77
using System;
8+
using System.Diagnostics;
9+
using System.Numerics;
10+
using System.Reflection;
811
using System.Runtime.InteropServices;
912
using System.Runtime.Versioning;
1013

14+
using Mono.Unix;
1115
using Mono.Unix.Native;
1216

17+
using Microsoft.Scripting.Runtime;
18+
1319
using IronPython.Runtime;
20+
using IronPython.Runtime.Operations;
21+
1422

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

2937

38+
#region fcntl
39+
40+
[LightThrowing]
41+
public static object fcntl(int fd, int cmd, [NotNone] Bytes arg) {
42+
CheckFileDescriptor(fd);
43+
44+
const int maxArgSize = 1024; // 1 KiB
45+
int argSize = arg.Count;
46+
if (argSize > maxArgSize) {
47+
throw PythonOps.ValueError("fcntl bytes arg too long");
48+
}
49+
50+
if (!NativeConvert.TryToFcntlCommand(cmd, out FcntlCommand fcntlCommand)) {
51+
throw PythonOps.OSError(PythonErrno.EINVAL, "unsupported fcntl command");
52+
}
53+
54+
var buf = new byte[maxArgSize];
55+
Array.Copy(arg.UnsafeByteArray, buf, argSize);
56+
57+
int result;
58+
Errno errno;
59+
unsafe {
60+
fixed (byte* ptr = buf) {
61+
do {
62+
result = Syscall.fcntl(fd, fcntlCommand, (IntPtr)ptr);
63+
} while (UnixMarshal.ShouldRetrySyscall(result, out errno));
64+
}
65+
}
66+
67+
if (result == -1) {
68+
return LightExceptions.Throw(PythonNT.GetOsError(NativeConvert.FromErrno(errno)));
69+
}
70+
byte[] response = new byte[argSize];
71+
Array.Copy(buf, response, argSize);
72+
return Bytes.Make(response);
73+
}
74+
75+
76+
[LightThrowing]
77+
public static object fcntl(int fd, int cmd, [Optional] object? arg) {
78+
CheckFileDescriptor(fd);
79+
80+
long data = arg switch {
81+
Missing => 0,
82+
int i => i,
83+
uint ui => ui,
84+
long l => l,
85+
ulong ul => (long)ul,
86+
BigInteger bi => (long)bi,
87+
Extensible<BigInteger> ebi => (long)ebi.Value,
88+
_ => throw PythonOps.TypeErrorForBadInstance("integer argument expected, got {0}", arg)
89+
};
90+
91+
if (!NativeConvert.TryToFcntlCommand(cmd, out FcntlCommand fcntlCommand)) {
92+
throw PythonOps.OSError(PythonErrno.EINVAL, "unsupported fcntl command");
93+
}
94+
95+
int result;
96+
Errno errno;
97+
do {
98+
result = Syscall.fcntl(fd, fcntlCommand, data);
99+
} while (UnixMarshal.ShouldRetrySyscall(result, out errno));
100+
101+
if (result == -1) {
102+
return LightExceptions.Throw(PythonNT.GetOsError(NativeConvert.FromErrno(errno)));
103+
}
104+
return ScriptingRuntimeHelpers.Int32ToObject(result);
105+
}
106+
107+
108+
[LightThrowing]
109+
public static object fcntl(CodeContext context, object? fd, int cmd, object? arg = null) {
110+
int fileno = GetFileDescriptor(context, fd);
111+
112+
if (arg is Bytes bytes) {
113+
return fcntl(fileno, cmd, bytes);
114+
}
115+
116+
return fcntl(fileno, cmd, arg);
117+
}
118+
119+
#endregion
120+
121+
122+
#region ioctl
123+
30124
// supporting fcntl.ioctl(fileno, termios.TIOCGWINSZ, buf)
31125
// where buf = array.array('h', [0, 0, 0, 0])
32126
public static object ioctl(CodeContext context, int fd, int cmd, [NotNone] IBufferProtocol arg, int mutate_flag = 1) {
@@ -53,10 +147,109 @@ public static object ioctl(CodeContext context, int fd, int cmd, [NotNone] IBuff
53147
throw new NotImplementedException($"ioctl: unsupported command {cmd}");
54148
}
55149

150+
#endregion
151+
152+
153+
#region flock
154+
155+
[DllImport("libc", SetLastError = true, EntryPoint = "flock")]
156+
private static extern int _flock(int fd, int op);
157+
158+
[LightThrowing]
159+
public static object? flock(int fd, int operation) {
160+
CheckFileDescriptor(fd);
161+
162+
int result;
163+
int errno = 0;
164+
do {
165+
result = _flock(fd, operation);
166+
} while (result == -1 && (errno = Marshal.GetLastWin32Error()) == PythonErrno.EINTR);
167+
168+
if (result == -1) {
169+
return LightExceptions.Throw(PythonNT.GetOsError(errno));
170+
}
171+
return null;
172+
}
173+
174+
175+
[LightThrowing]
176+
public static object? flock(CodeContext context, object? fd, int operation)
177+
=> flock(GetFileDescriptor(context, fd), operation);
178+
179+
#endregion
180+
181+
182+
#region lockf
183+
184+
[LightThrowing]
185+
public static object? lockf(int fd, int cmd, long len = 0, long start = 0, int whence = 0) {
186+
CheckFileDescriptor(fd);
187+
188+
Flock flock = new() {
189+
l_whence = (SeekFlags)whence,
190+
l_start = start,
191+
l_len = len
192+
};
193+
if (cmd == LOCK_UN) {
194+
flock.l_type = LockType.F_UNLCK;
195+
} else if ((cmd & LOCK_SH) != 0) {
196+
flock.l_type = LockType.F_RDLCK;
197+
} else if ((cmd & LOCK_EX) != 0) {
198+
flock.l_type = LockType.F_WRLCK;
199+
} else {
200+
throw PythonOps.ValueError("unrecognized lockf argument");
201+
}
202+
203+
int result;
204+
Errno errno;
205+
do {
206+
result = Syscall.fcntl(fd, (cmd & LOCK_NB) != 0 ? FcntlCommand.F_SETLK : FcntlCommand.F_SETLKW, ref flock);
207+
} while (UnixMarshal.ShouldRetrySyscall(result, out errno));
208+
209+
if (result == -1) {
210+
return LightExceptions.Throw(PythonNT.GetOsError(NativeConvert.FromErrno(errno)));
211+
}
212+
return null;
213+
}
214+
215+
216+
[LightThrowing]
217+
public static object? lockf(CodeContext context, object? fd, int cmd, long len = 0, long start = 0, int whence = 0)
218+
=> lockf(GetFileDescriptor(context, fd), cmd, len, start, whence);
219+
220+
#endregion
221+
222+
223+
#region Helper Methods
224+
225+
private static int GetFileDescriptor(CodeContext context, object? obj) {
226+
if (!PythonOps.TryGetBoundAttr(context, obj, "fileno", out object? filenoMeth)) {
227+
throw PythonOps.TypeError("argument must be an int, or have a fileno() method.");
228+
}
229+
return PythonCalls.Call(context, filenoMeth) switch {
230+
int i => i,
231+
uint ui => (int)ui,
232+
BigInteger bi => (int)bi,
233+
Extensible<BigInteger> ebi => (int)ebi.Value,
234+
_ => throw PythonOps.TypeError("fileno() returned a non-integer")
235+
};
236+
}
237+
238+
239+
private static void CheckFileDescriptor(int fd) {
240+
if (fd < 0) {
241+
throw PythonOps.ValueError("file descriptor cannot be a negative integer ({0})", fd);
242+
}
243+
}
244+
245+
#endregion
246+
56247

57248
// FD Flags
58249
public static int FD_CLOEXEC = 1;
59-
public static int FASYNC => RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? 0x0040 : 0x2000;
250+
251+
// O_* flags under F* name
252+
public static int FASYNC => RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? 0x0040 : 0x2000; // O_ASYNC
60253

61254

62255
#region Generated FD Commands

tests/IronPython.Tests/Cases/CPythonCasesManifest.ini

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -400,11 +400,6 @@ Ignore=true
400400
[CPython.test_faulthandler]
401401
Ignore=true
402402

403-
[CPython.test_fcntl]
404-
RunCondition=$(IS_POSIX)
405-
Ignore=true
406-
Reason=ImportError: No module named fcntl
407-
408403
[CPython.test_file]
409404
IsolationLevel=PROCESS # https://github.com/IronLanguages/ironpython3/issues/489
410405

@@ -510,7 +505,7 @@ Ignore=true
510505
[CPython.test_ioctl]
511506
RunCondition=$(IS_POSIX)
512507
Ignore=true
513-
Reason=unittest.case.SkipTest: No module named 'fcntl'
508+
Reason=unittest.case.SkipTest: module 'termios' has no attribute 'TIOCGPGRP', 'fcntl.ioctl' is a mock
514509

515510
[CPython.test_ipaddress]
516511
Ignore=true
@@ -720,7 +715,7 @@ Ignore=true
720715
[CPython.test_pty]
721716
RunCondition=$(IS_POSIX)
722717
Ignore=true
723-
Reason=unittest.case.SkipTest: No module named 'fcntl'
718+
Reason=Missing constants in 'termios', 'signal', 'termios' implementation is a stub
724719

725720
[CPython.test_pulldom]
726721
Ignore=true

0 commit comments

Comments
 (0)