Skip to content

Commit ea20a1a

Browse files
authored
Enhance logging output for safeSetTimes() when the client runtime user does not the file requiring timestamp change (#3654)
Improve diagnostics and reduce alarm for filesystem timestamp permission failures. Timestamp update failures due to permission issues (e.g., EPERM) are now reported as warnings with clearer guidance, including detection of file owner vs. client runtime user UID mismatches. This helps users quickly identify common causes such as multi-user or Samba-created files without implying a fatal or critical client error.
1 parent adcbb36 commit ea20a1a

2 files changed

Lines changed: 170 additions & 53 deletions

File tree

.github/actions/spelling/allow.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@ concated
9090
confdir
9191
constness
9292
controllen
93+
cpath
9394
crt
9495
Cstrip
9596
ctl
@@ -141,6 +142,7 @@ envp
141142
eopkg
142143
epel
143144
epfd
145+
errnos
144146
eselect
145147
estr
146148
eventfd

src/util.d

Lines changed: 168 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ module util;
33

44
// What does this module require to function?
55
import core.memory;
6-
import core.stdc.errno : ENOENT, EINTR, EBUSY, EXDEV, EAGAIN;
6+
import core.stdc.errno : ENOENT, EINTR, EBUSY, EXDEV, EAGAIN, EPERM, EACCES, EROFS;
77
import core.stdc.stdlib;
88
import core.stdc.string;
99
import core.sys.posix.pwd;
@@ -66,9 +66,10 @@ shared static this() {
6666

6767
// To assist with filesystem severity issues, configure an enum that can be used
6868
enum FsErrorSeverity {
69-
warning,
70-
error,
71-
fatal
69+
warning,
70+
error,
71+
fatal,
72+
permission
7273
}
7374

7475
// Creates a safe backup of the given item, and only performs the function if not in a --dry-run scenario.
@@ -1066,8 +1067,9 @@ void displayFileSystemErrorMessage(string message, string callingFunction, strin
10661067

10671068
// Header prefix for logging accuracy
10681069
string headerPrefix = severity == FsErrorSeverity.warning ? "WARNING"
1069-
: severity == FsErrorSeverity.fatal ? "FATAL"
1070-
: "ERROR";
1070+
: severity == FsErrorSeverity.permission ? "WARNING"
1071+
: severity == FsErrorSeverity.fatal ? "FATAL"
1072+
: "ERROR";
10711073

10721074
// Filesystem logging header
10731075
addLogEntry(headerPrefix ~ ": The local file system returned an error with the following details:");
@@ -1114,72 +1116,83 @@ void displayFileSystemErrorMessage(string message, string callingFunction, strin
11141116
addLogEntry(" Error Message: No error message available");
11151117
}
11161118

1117-
// Disk space diagnostics (best-effort)
1118-
// We intentionally probe both the current directory and the target path directory when possible.
1119-
try {
1120-
// Always check the current working directory as a baseline
1121-
ulong freeCwd = to!ulong(getAvailableDiskSpace("."));
1122-
addLogEntry(" Disk Space (CWD): " ~ to!string(freeCwd) ~ " bytes available");
1123-
1124-
// If we have a context path, also check its parent directory when possible.
1125-
// We keep this conservative: if anything throws, just log the exception.
1126-
if (!contextPath.empty) {
1127-
string targetProbePath = contextPath;
1128-
1129-
// If it's a file path, probe the parent directory (where writes/renames happen).
1130-
// Avoid throwing if parentDir isn't available or contextPath is weird.
1131-
try {
1132-
// std.path.dirName handles both file/dir paths; if it returns ".", keep as-is.
1133-
import std.path : dirName;
1134-
auto parent = dirName(contextPath);
1135-
if (!parent.empty) targetProbePath = parent;
1136-
} catch (Exception e) {
1137-
addLogEntry(" NOTE: Failed to derive parent directory from path: " ~ e.msg);
1138-
}
1119+
// Disk space diagnostics (best-effort) - if this is not a permission issue
1120+
if (severity != FsErrorSeverity.permission) {
1121+
// We intentionally probe both the current directory and the target path directory when possible.
1122+
try {
1123+
// Always check the current working directory as a baseline
1124+
ulong freeCwd = to!ulong(getAvailableDiskSpace("."));
1125+
addLogEntry(" Disk Space (CWD): " ~ to!string(freeCwd) ~ " bytes available");
1126+
1127+
// If we have a context path, also check its parent directory when possible.
1128+
// We keep this conservative: if anything throws, just log the exception.
1129+
if (!contextPath.empty) {
1130+
string targetProbePath = contextPath;
1131+
1132+
// If it's a file path, probe the parent directory (where writes/renames happen).
1133+
// Avoid throwing if parentDir isn't available or contextPath is weird.
1134+
try {
1135+
// std.path.dirName handles both file/dir paths; if it returns ".", keep as-is.
1136+
import std.path : dirName;
1137+
auto parent = dirName(contextPath);
1138+
if (!parent.empty) targetProbePath = parent;
1139+
} catch (Exception e) {
1140+
addLogEntry(" NOTE: Failed to derive parent directory from path: " ~ e.msg);
1141+
}
11391142

1140-
ulong freeTarget = to!ulong(getAvailableDiskSpace(targetProbePath));
1141-
addLogEntry(" Disk Space (Path): " ~ to!string(freeTarget) ~ " bytes available (parent path: " ~ targetProbePath ~ ")");
1143+
ulong freeTarget = to!ulong(getAvailableDiskSpace(targetProbePath));
1144+
addLogEntry(" Disk Space (Path): " ~ to!string(freeTarget) ~ " bytes available (parent path: " ~ targetProbePath ~ ")");
11421145

1143-
// Preserve existing behaviour: if disk space check returns 0, force exit.
1144-
// (Assumes getAvailableDiskSpace returns 0 on a hard failure in your implementation.)
1145-
if (freeTarget == 0 || freeCwd == 0) {
1146-
// Must force exit here, allow logging to be done
1147-
forceExit();
1148-
}
1149-
} else {
1150-
// Preserve existing behaviour: if disk space check returns 0, force exit.
1151-
if (freeCwd == 0) {
1152-
forceExit();
1146+
// Preserve existing behaviour: if disk space check returns 0, force exit.
1147+
// (Assumes getAvailableDiskSpace returns 0 on a hard failure in your implementation.)
1148+
if (freeTarget == 0 || freeCwd == 0) {
1149+
// Must force exit here, allow logging to be done
1150+
forceExit();
1151+
}
1152+
} else {
1153+
// Preserve existing behaviour: if disk space check returns 0, force exit.
1154+
if (freeCwd == 0) {
1155+
forceExit();
1156+
}
11531157
}
1158+
} catch (Exception e) {
1159+
// Handle exceptions from disk space check or type conversion
1160+
addLogEntry(" NOTE: Exception during disk space check: " ~ e.msg);
11541161
}
1155-
} catch (Exception e) {
1156-
// Handle exceptions from disk space check or type conversion
1157-
addLogEntry(" NOTE: Exception during disk space check: " ~ e.msg);
11581162
}
11591163

11601164
// Add note for WARNING messages
1161-
if (headerPrefix == "WARNING") {
1165+
if (severity == FsErrorSeverity.warning) {
11621166
addLogEntry();
1163-
addLogEntry("NOTE: This error is non-fatal; the client will continue to operate, but this may affect future operations if not resolved");
1167+
addLogEntry("NOTE: This warning is non-fatal; the client will continue to operate, but this may affect future operations if not resolved");
1168+
addLogEntry();
1169+
}
1170+
1171+
// Add note for filesystem permission messages
1172+
if (severity == FsErrorSeverity.permission) {
1173+
addLogEntry();
1174+
addLogEntry("NOTE: Sync will continue. This file’s timestamps could not be updated because the effective user does not own the file.");
1175+
addLogEntry(" Potential Fix:");
1176+
addLogEntry(" Run the client as the file owner, or change ownership of the sync tree so it is owned by the user running the client.");
1177+
addLogEntry(" Learn more about File Ownership:");
1178+
addLogEntry(" https://www.redhat.com/en/blog/linux-file-permissions-explained");
1179+
addLogEntry(" https://unix.stackexchange.com/questions/191940/difference-between-owner-root-and-ruid-euid");
11641180
addLogEntry();
11651181
}
11661182

11671183
// Add note for ERROR messages
1168-
if (headerPrefix == "ERROR") {
1184+
if (severity == FsErrorSeverity.error) {
11691185
addLogEntry();
11701186
addLogEntry("NOTE: This error requires attention; the client may continue running, but functionality is impaired and the issue should be resolved.");
11711187
addLogEntry();
11721188
}
11731189

11741190
// Add note for FATAL messages
1175-
if (headerPrefix == "FATAL") {
1191+
if (severity == FsErrorSeverity.fatal) {
11761192
addLogEntry();
11771193
addLogEntry("NOTE: This error is fatal; the client cannot continue and this issue must be corrected before retrying. The client will now attempt to exit in a safe and orderly manner.");
11781194
addLogEntry();
11791195
}
1180-
1181-
// Separate this block from surrounding log output
1182-
addLogEntry();
11831196
}
11841197

11851198
// Display the POSIX Error Message
@@ -2103,9 +2116,57 @@ private bool safeGetTimes(string path, out SysTime accessTime, out SysTime modTi
21032116
return false;
21042117
}
21052118

2119+
// Some errnos are 'expected' in the wild (permissions, RO mounts, immutable files)
2120+
// What is this errno
2121+
private bool isExpectedPermissionStyleErrno(int err) {
2122+
// Return true of this is an expected error due to permission issues
2123+
return err == EPERM || err == EACCES || err == EROFS;
2124+
}
2125+
2126+
// Helper function to determine path mismatch against UID|GID and process effective UID
2127+
private bool getPathOwnerMismatch(string path, out uint fileUid, out uint effectiveUid) {
2128+
version (Posix) {
2129+
stat_t st;
2130+
2131+
// Default outputs
2132+
fileUid = 0;
2133+
effectiveUid = cast(uint) geteuid();
2134+
2135+
try {
2136+
// absolutePath can throw; keep this helper non-throwing
2137+
auto fullPath = absolutePath(path);
2138+
2139+
// Ensure we pass a NUL-terminated string to the C API
2140+
auto cpath = toStringz(fullPath);
2141+
2142+
if (lstat(cpath, &st) != 0) {
2143+
if (debugLogging) {
2144+
addLogEntry("getPathOwnerMismatch(): lstat() failed for '" ~ path ~ "'", ["debug"]);
2145+
}
2146+
return false;
2147+
}
2148+
2149+
fileUid = cast(uint) st.st_uid;
2150+
// effectiveUid already set above
2151+
return fileUid != effectiveUid;
2152+
2153+
} catch (Exception e) {
2154+
if (debugLogging) {
2155+
addLogEntry("getPathOwnerMismatch(): exception for '" ~ path ~ "': " ~ e.msg, ["debug"]);
2156+
}
2157+
return false;
2158+
}
2159+
} else {
2160+
fileUid = 0;
2161+
effectiveUid = 0;
2162+
return false;
2163+
}
2164+
}
2165+
21062166
// Retry wrapper for setTimes()
21072167
private bool safeSetTimes(string path, SysTime accessTime, SysTime modTime, string thisFunctionName) {
2108-
int maxAttempts = 5;
2168+
2169+
enum int maxAttempts = 5;
21092170

21102171
foreach (attempt; 0 .. maxAttempts) {
21112172
try {
@@ -2117,18 +2178,72 @@ private bool safeSetTimes(string path, SysTime accessTime, SysTime modTime, stri
21172178
return false;
21182179
}
21192180

2181+
// Transient filesystem error: retry with backoff
21202182
if (isTransientErrno(e.errno)) {
2121-
// slightly longer backoff here is fine too, but keep it simple/consistent
2183+
if (debugLogging) {
2184+
// Log that we hit a transient error when doing debugging, otherwise nothing
2185+
addLogEntry("safeSetTimes() transient filesystem error response: " ~ e.msg ~ "\n - Attempting retry for setTimes()", ["debug"]);
2186+
}
2187+
// Backoff and retry
21222188
Thread.sleep(dur!"msecs"(15 * (attempt + 1)));
21232189
continue;
21242190
}
21252191

2192+
// Non-transient: special-case common permission errors
2193+
// The user running the client needs to be the owner of the files if the client needs to set explicit timestamps
2194+
// See https://github.com/abraunegg/onedrive/issues/3651 for details
2195+
if (isExpectedPermissionStyleErrno(e.errno)) {
2196+
// Configure application message to display
2197+
string permissionErrorMessage = "Unable to set local file timestamps (mtime/atime): Operation not permitted";
2198+
if (e.errno == EPERM) {
2199+
permissionErrorMessage = permissionErrorMessage ~ " (EPERM)";
2200+
}
2201+
2202+
if (e.errno == EACCES) {
2203+
permissionErrorMessage = permissionErrorMessage ~ " (EACCES)";
2204+
}
2205+
2206+
if (e.errno == EROFS) {
2207+
permissionErrorMessage = permissionErrorMessage ~ " (EROFS)";
2208+
}
2209+
2210+
// Get extra details if required
2211+
string extraHint;
2212+
uint fileUid;
2213+
uint effectiveUid;
2214+
2215+
if (e.errno == EPERM && getPathOwnerMismatch(path, fileUid, effectiveUid)) {
2216+
extraHint =
2217+
"\nThe onedrive client user does not own this file. onedrive user effective UID=" ~ to!string(effectiveUid) ~ ", file owner UID=" ~ to!string(fileUid) ~ "." ~
2218+
"\nOn Unix-like systems, setting explicit file timestamps typically requires the process to be the file owner or run with sufficient privileges.";
2219+
2220+
// Update permissionErrorMessage to add extraHint
2221+
permissionErrorMessage = permissionErrorMessage ~ extraHint;
2222+
}
2223+
2224+
// If we are doing --verbose or --debug display this file system error
2225+
if (verboseLogging) {
2226+
// Display applicable message for the user regarding permission error on path
2227+
displayFileSystemErrorMessage(
2228+
permissionErrorMessage,
2229+
thisFunctionName,
2230+
path,
2231+
FsErrorSeverity.permission
2232+
);
2233+
}
2234+
2235+
// It is pointless attempting a re-try in this scenario as those conditions will not change by retrying 15ms later.
2236+
return false;
2237+
}
2238+
2239+
// Everything else: preserve existing behaviour
21262240
displayFileSystemErrorMessage(e.msg, thisFunctionName, path);
21272241
return false;
21282242
}
21292243
}
21302244

2131-
displayFileSystemErrorMessage("Failed to set file timestamps after retries", thisFunctionName, path);
2245+
// Only reached if transient errors never resolved
2246+
displayFileSystemErrorMessage("Failed to set path timestamps after retries", thisFunctionName, path);
21322247
return false;
21332248
}
21342249

0 commit comments

Comments
 (0)