Skip to content

Commit c7365b7

Browse files
authored
Use P/Invoke for os.strerror (#1500)
* nt.strerror_r * Eliminate switch on PythonErrorNumber * Isolate Mono.Unix * Add a few tests
1 parent 2e5bb60 commit c7365b7

3 files changed

Lines changed: 83 additions & 54 deletions

File tree

Src/IronPython.Modules/nt.cs

Lines changed: 30 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -1248,12 +1248,14 @@ public sealed class stat_result : PythonTuple {
12481248
internal stat_result(int mode) : this(new object[10] { mode, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, null) { }
12491249

12501250
internal stat_result(Mono.Unix.Native.Stat stat)
1251-
: this(new object[16] {(int)stat.st_mode, ToInt(stat.st_ino), ToInt(stat.st_dev), ToInt(stat.st_nlink), ToInt(stat.st_uid), ToInt(stat.st_gid), ToInt(stat.st_size), ToInt(stat.st_atime), ToInt(stat.st_mtime), ToInt(stat.st_ctime),
1251+
: this(new object[16] {Mono.Unix.Native.NativeConvert.FromFilePermissions(stat.st_mode), ToInt(stat.st_ino), ToInt(stat.st_dev), ToInt(stat.st_nlink), ToInt(stat.st_uid), ToInt(stat.st_gid), ToInt(stat.st_size),
1252+
ToInt(stat.st_atime), ToInt(stat.st_mtime), ToInt(stat.st_ctime),
12521253
stat.st_atime + stat.st_atime_nsec / (double)nanosecondsPerSeconds, stat.st_mtime + stat.st_mtime_nsec / (double)nanosecondsPerSeconds, stat.st_ctime + stat.st_ctime_nsec / (double)nanosecondsPerSeconds,
12531254
ToInt(stat.st_atime * nanosecondsPerSeconds + stat.st_atime_nsec), ToInt(stat.st_mtime * nanosecondsPerSeconds + stat.st_mtime_nsec), ToInt(stat.st_ctime * nanosecondsPerSeconds + stat.st_ctime_nsec) }, null) { }
12541255

12551256
internal stat_result(int mode, ulong fileidx, long size, long st_atime_ns, long st_mtime_ns, long st_ctime_ns)
1256-
: this(new object[16] { mode, ToInt(fileidx), 0, 0, 0, 0, ToInt(size), ToInt(st_atime_ns / nanosecondsPerSeconds), ToInt(st_mtime_ns / nanosecondsPerSeconds), ToInt(st_ctime_ns / nanosecondsPerSeconds),
1257+
: this(new object[16] { mode, ToInt(fileidx), 0, 0, 0, 0, ToInt(size),
1258+
ToInt(st_atime_ns / nanosecondsPerSeconds), ToInt(st_mtime_ns / nanosecondsPerSeconds), ToInt(st_ctime_ns / nanosecondsPerSeconds),
12571259
st_atime_ns / (double)nanosecondsPerSeconds, st_mtime_ns / (double)nanosecondsPerSeconds, st_ctime_ns / (double)nanosecondsPerSeconds,
12581260
ToInt(st_atime_ns), ToInt(st_mtime_ns), ToInt(st_ctime_ns) }, null) { }
12591261

@@ -1495,54 +1497,30 @@ public static object stat(CodeContext context, int fd)
14951497
=> fstat(context, fd);
14961498

14971499
public static string strerror(int code) {
1498-
switch (code) {
1499-
case 0: return "No error";
1500-
case PythonErrorNumber.E2BIG: return "Arg list too long";
1501-
case PythonErrorNumber.EACCES: return "Permission denied";
1502-
case PythonErrorNumber.EAGAIN: return "Resource temporarily unavailable";
1503-
case PythonErrorNumber.EBADF: return "Bad file descriptor";
1504-
case PythonErrorNumber.EBUSY: return "Resource device";
1505-
case PythonErrorNumber.ECHILD: return "No child processes";
1506-
case PythonErrorNumber.EDEADLK: return "Resource deadlock avoided";
1507-
case PythonErrorNumber.EDOM: return "Domain error";
1508-
case PythonErrorNumber.EDQUOT: return "Unknown error";
1509-
case PythonErrorNumber.EEXIST: return "File exists";
1510-
case PythonErrorNumber.EFAULT: return "Bad address";
1511-
case PythonErrorNumber.EFBIG: return "File too large";
1512-
case PythonErrorNumber.EILSEQ: return "Illegal byte sequence";
1513-
case PythonErrorNumber.EINTR: return "Interrupted function call";
1514-
case PythonErrorNumber.EINVAL: return "Invalid argument";
1515-
case PythonErrorNumber.EIO: return "Input/output error";
1516-
case PythonErrorNumber.EISCONN: return "Unknown error";
1517-
case PythonErrorNumber.EISDIR: return "Is a directory";
1518-
case PythonErrorNumber.EMFILE: return "Too many open files";
1519-
case PythonErrorNumber.EMLINK: return "Too many links";
1520-
case PythonErrorNumber.ENAMETOOLONG: return "Filename too long";
1521-
case PythonErrorNumber.ENFILE: return "Too many open files in system";
1522-
case PythonErrorNumber.ENODEV: return "No such device";
1523-
case PythonErrorNumber.ENOENT: return "No such file or directory";
1524-
case PythonErrorNumber.ENOEXEC: return "Exec format error";
1525-
case PythonErrorNumber.ENOLCK: return "No locks available";
1526-
case PythonErrorNumber.ENOMEM: return "Not enough space";
1527-
case PythonErrorNumber.ENOSPC: return "No space left on device";
1528-
case PythonErrorNumber.ENOSYS: return "Function not implemented";
1529-
case PythonErrorNumber.ENOTDIR: return "Not a directory";
1530-
case PythonErrorNumber.ENOTEMPTY: return "Directory not empty";
1531-
case PythonErrorNumber.ENOTSOCK: return "Unknown error";
1532-
case PythonErrorNumber.ENOTTY: return "Inappropriate I/O control operation";
1533-
case PythonErrorNumber.ENXIO: return "No such device or address";
1534-
case PythonErrorNumber.EPERM: return "Operation not permitted";
1535-
case PythonErrorNumber.EPIPE: return "Broken pipe";
1536-
case PythonErrorNumber.ERANGE: return "Result too large";
1537-
case PythonErrorNumber.EROFS: return "Read-only file system";
1538-
case PythonErrorNumber.ESPIPE: return "Invalid seek";
1539-
case PythonErrorNumber.ESRCH: return "No such process";
1540-
case PythonErrorNumber.EXDEV: return "Improper link";
1541-
default:
1542-
return "Unknown error " + code;
1500+
#if FEATURE_NATIVE
1501+
const int bufsize = 0x1FF;
1502+
var buffer = new StringBuilder(bufsize);
1503+
1504+
int result = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ?
1505+
Interop.Ucrtbase.strerror(code, buffer) :
1506+
strerror_r(code, buffer);
1507+
1508+
if (result == 0) {
1509+
var msg = buffer.ToString();
1510+
if (msg.Length > 0) {
1511+
return msg;
1512+
}
15431513
}
1514+
#endif
1515+
return "Unknown error " + code;
15441516
}
15451517

1518+
#if FEATURE_NATIVE
1519+
// Isolate Mono.Unix from the rest of the method so that we don't try to load the Mono.Unix assembly on Windows.
1520+
private static int strerror_r(int code, StringBuilder buffer)
1521+
=> Mono.Unix.Native.Syscall.strerror_r(Mono.Unix.Native.NativeConvert.ToErrno(code), buffer);
1522+
#endif
1523+
15461524
#if FEATURE_PROCESS
15471525
[Documentation("system(command) -> int\nExecute the command (a string) in a subshell.")]
15481526
public static int system([NotNone] string command) {
@@ -1823,7 +1801,7 @@ public static PythonTuple waitpid(int pid, int options) {
18231801
Process? process;
18241802
lock (_processToIdMapping) {
18251803
if (!_processToIdMapping.TryGetValue(pid, out process)) {
1826-
throw PythonOps.OSError(PythonErrorNumber.ECHILD, "No child processes");
1804+
throw GetOsError(PythonErrorNumber.ECHILD);
18271805
}
18281806
}
18291807

@@ -2207,15 +2185,13 @@ private static Exception DirectoryExists() {
22072185
#if FEATURE_NATIVE
22082186

22092187
private static Exception GetLastUnixError(string? filename = null, string? filename2 = null)
2210-
=> GetUnixError((int)Mono.Unix.Native.Syscall.GetLastError(), filename, filename2);
2211-
2212-
private static Exception GetUnixError(int error, string? filename = null, string? filename2 = null) {
2213-
var msg = Mono.Unix.Native.Stdlib.strerror((Mono.Unix.Native.Errno)error);
2214-
return PythonOps.OSError(error, msg, filename, null, filename2);
2215-
}
2188+
=> GetOsError(Mono.Unix.Native.NativeConvert.FromErrno(Mono.Unix.Native.Syscall.GetLastError()), filename, filename2);
22162189

22172190
#endif
22182191

2192+
private static Exception GetOsError(int error, string? filename = null, string? filename2 = null)
2193+
=> PythonOps.OSError(error, strerror(error), filename, null, filename2);
2194+
22192195
#if FEATURE_NATIVE || FEATURE_CTYPES
22202196

22212197
// Gets an error message for a Win32 error code.
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System;
5+
using System.Buffers;
6+
using System.Runtime.InteropServices;
7+
using System.Runtime.Versioning;
8+
using System.Text;
9+
10+
internal static partial class Interop {
11+
internal static partial class Ucrtbase {
12+
[DllImport(Libraries.Ucrtbase, SetLastError = false, CharSet = CharSet.Ansi, ExactSpelling = true)]
13+
[SupportedOSPlatform("windows")]
14+
private static extern int strerror_s([Out] StringBuilder buffer, nuint sizeInBytes, int errnum);
15+
16+
[SupportedOSPlatform("windows")]
17+
internal static int strerror(int errnum, StringBuilder buf) {
18+
return strerror_s(buf, (nuint)buf.Capacity + 1, errnum);
19+
}
20+
}
21+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
# Licensed to the .NET Foundation under one or more agreements.
2+
# The .NET Foundation licenses this file to you under the Apache 2.0 License.
3+
# See the LICENSE file in the project root for more information.
4+
5+
import os
6+
7+
from iptest import IronPythonTestCase, is_osx, is_linux, is_windows, run_test
8+
9+
class OsTest(IronPythonTestCase):
10+
def test_strerror(self):
11+
if is_windows:
12+
self.assertEqual(os.strerror(0), "No error")
13+
elif is_linux:
14+
self.assertEqual(os.strerror(0), "Success")
15+
elif is_osx:
16+
self.assertEqual(os.strerror(0), "Undefined error: 0")
17+
18+
if is_windows:
19+
self.assertEqual(os.strerror(39), "No locks available")
20+
elif is_linux:
21+
self.assertEqual(os.strerror(39), "Directory not empty")
22+
elif is_osx:
23+
self.assertEqual(os.strerror(39), "Destination address required")
24+
25+
if is_windows:
26+
self.assertEqual(os.strerror(40), "Function not implemented")
27+
elif is_linux:
28+
self.assertEqual(os.strerror(40), "Too many levels of symbolic links")
29+
elif is_osx:
30+
self.assertEqual(os.strerror(40), "Message too long")
31+
32+
run_test(__name__)

0 commit comments

Comments
 (0)