Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[SR-13300] Attempt to use FileManager.setAttributes() to set dates, fails #3974

Closed
swift-ci opened this issue Jul 27, 2020 · 14 comments
Closed
Assignees

Comments

@swift-ci
Copy link
Contributor

Previous ID SR-13300
Radar None
Original Reporter NPAssoc (JIRA User)
Type Bug
Status Resolved
Resolution Done
Environment

Raspian Linux Swift version 5.1.5-v0.1

Additional Detail from JIRA
Votes 1
Component/s Foundation
Labels Bug
Assignee @benrimmington
Priority Medium

md5: e2d8e2a7325bb243150880bad357f02f

Issue Description:

When I use the code below to try to set the Modify [or creation] date of a file, it fails with the message:

Failure to set modify date of: /home/pi/projects/4thRestore/OSC.log The operation could not be completed. (NSCocoaErrorDomain error 512.)

This works under MacOS but not with Raspian Linux. Note: running under Sudo does not help.

do {

{{ try mgr.copyItem(at: file.fileID!, to: destUrl)}}

} catch {

{{ print("File copy error during restore! " + error.localizedDescription)}}

{{ return false}}

{{}}}

// Apply the saved modify date to the file since it may have come from an incompatible file system.

if !self.setModifyDate(url: destUrl,

{{ modifyDate: file.lastModifyDate)}}

{

{{ result = false;}}

{{}}}

where...

{{ public func setModifyDate(url:URL,}}

{{ modifyDate newDate:Date) -> Bool}}

{

...

{{ let attributes = [FileAttributeKey.modificationDate: newDate]}}

{{ do {}}

{{ try mgr.setAttributes(attributes, ofItemAtPath: url.path)}}

{{ } catch {}}

{{ print("Failure to set modify date of: " + url.path + " " + error.localizedDescription)}}
{{ }}}

...

}

@benrimmington
Copy link
Contributor

(NSCocoaErrorDomain error 512.)

This is CocoaError.fileWriteUnknown or "Write error (reason unknown)".

Can you see if there's an underlying error?

if let cocoaError = error as? CocoaError,
   let underlyingError = cocoaError.underlying {
  print(underlyingError)
}

@swift-ci
Copy link
Contributor Author

Comment by Nicholas Pisarro Jr (JIRA)

My code now reads:

let attributes = [FileAttributeKey.modificationDate: newDate]

do {
   try mgr.setAttributes(attributes, ofItemAtPath: url.path)        }
catch
{
   print("Failure to set modify date of: " + url.path + " " + error.localizedDescription)

   // Print the underlying error.
   if let cocoaError = error as? CocoaError,
      let underlyingError = cocoaError.underlying {
     print("Underlying error:", underlyingError)
   }
   else {
       print ("No underlying error found.")
   }
}

I got:

...$ command/1stSwift -r -v -include OSC* ../4th ../4thRestore
0 Additions, 2 Changes, 0 Deletions
Restore files (y/n)? y
File Changed: /OSC.log
Failure to set modify date of: /home/pi/projects/4thRestore/OSC.log The operation could not be completed. (NSCocoaErrorDomain error 512.)
No underlying error found.
File Changed: /OSCMultiMonitor.log
Failure to set modify date of: /home/pi/projects/4thRestore/OSCMultiMonitor.log The operation could not be completed. (NSCocoaErrorDomain error 512.)
No underlying error found. 

Nick

@benrimmington
Copy link
Contributor

OK, I thought there might be an underlying POSIX error from the utimes call, via the _NSErrorWithErrno function. But the failure could be from earlier than that.

Setting the modificationDate on Windows is unsupported, it throws a CocoaError.fileWriteUnknown error here. It's possible that swift-corelibs-foundation for Raspberry Pi OS is doing the same thing. You'd need to check the source code for the fork you're using.

@benrimmington
Copy link
Contributor

The error as? CocoaError bridging works on macOS, but does it work on non-Apple platforms?

Another way to check for the underlying error is with the NSError APIs:

if let nsError = error as? NSError,
   let underlyingError = nsError.userInfo[NSUnderlyingErrorKey] {
  print(underlyingError)
}

If there's still no underlying POSIX error, then it's likely to be an issue with the Raspberry Pi fork of swift-corelibs-foundation.

@swift-ci
Copy link
Contributor Author

Comment by Nicholas Pisarro Jr (JIRA)

That code shows an underlying error:

command/1stSwift -r -v -include OSC* ../4th ../4thRestore
0 Additions, 2 Changes, 0 Deletions
Restore files (y/n)? y
File Changed: /OSC.log
Failure to set modify date of: /home/pi/projects/4thRestore/OSC.log The operation could not be completed. (NSCocoaErrorDomain error 512.)
Error Domain=NSPOSIXErrorDomain Code=22 "Invalid argument"
File Changed: /OSCMultiMonitor.log
Failure to set modify date of: /home/pi/projects/4thRestore/OSCMultiMonitor.log The operation could not be completed. (NSCocoaErrorDomain error 512.)
Error Domain=NSPOSIXErrorDomain Code=22 "Invalid argument" 

How might I set the modify date directly using a posix call?

@benrimmington
Copy link
Contributor

So the issue appears to be that utimes is failing with POSIX errno 22 (EINVAL).

The documentation for POSIX is here, and the page for futimens/utimensat/utimes is here.

  • You might need to import Glibc, if it isn't re-exported by Foundation.
  • For the const char *path parameters, you can use fileSystemRepresentation(withPath:), but I think you'd then need to defer a call to .deallocate() because the result isn't autoreleased (on non-Apple platforms).
  • For the const struct timespec times[2] or const struct timeval times[2] parameters, you should be able to pass a Swift array directly, and the compiler will implicitly create temporary C arrays. See the Pointers as Array Parameters section in Interacting with C Pointers.

If you need more advice, try the Using Swift category of the forums.

@swift-ci
Copy link
Contributor Author

Comment by Bridger Maxwell (JIRA)

Did the posix call work for setting the modification date? Is there a simpler way to set the modification date to the current time?

@swift-ci
Copy link
Contributor Author

Comment by Nicholas Pisarro Jr (JIRA)

Haven't tried it. I missed the notification for 14 Sep 2020 9:04 PM comment.

I'll try it in the next few days.

@swift-ci
Copy link
Contributor Author

Comment by Nicholas Pisarro Jr (JIRA)

Using Posix seems to work! This code works on both Mac & Raspian…

//
//  SnapPosix.swift
//  1stSwift
//
//  Created by Nicholas Pisarro on 10/15/20.
//


import Foundation
#if MACISH
import Darwin
import Darwin.POSIX
#else
import Glibc
#endif
// import Darwin.POSIX.fcntl


public class MyPosixFuncs
{
    public static func newModifyDate(_ url:NSURL, newModifyDate:NSDate)
    {
        let filepath = UnsafeMutablePointer<Int8>.allocate(capacity: 512)
        let result = url.getFileSystemRepresentation(filepath, maxLength: 512)
        if result
        {
            let fd = open(filepath, O_RDONLY)
            
            if fd > 0
            {
                #if !MACISH
                let NSEC_PER_SEC:UInt64 = 1000000000
                #endif
                // Come up with a timespec.
                let now = newModifyDate.timeIntervalSince1970
                let nowWholeSecsFloor = floor(now)
                let nowNanosOnly = now - nowWholeSecsFloor
                let nowNanosFloor = floor(nowNanosOnly * Double(NSEC_PER_SEC))
                                
                let timish = UnsafeMutablePointer<timespec>.allocate(capacity: 2)
                timish[0] = timespec(tv_sec: Int(nowWholeSecsFloor),
                                     tv_nsec: Int(nowNanosFloor))
                timish[1] = timespec(tv_sec: Int(nowWholeSecsFloor),
                                     tv_nsec: Int(nowNanosFloor))
                var result:Int32 = -2
                #if MACISH
                if #available(OSX 10.13, *) {
                    result = futimens(fd, timish)
                }
                #else
                result = futimens(fd, timish)
                #endif
                if result != 0
                {
                    let path = url.path
                    print("snap: Failed \(result): Trying to set dates of \(path!).")
                }
                timish.deallocate()
                close(fd)
            }
        }
        filepath.deallocate()
    }
}
 

More testing is required.

(This took about 8 hours to develop, what with figuring out UnsafeMutablePointer, how to navigate the posix documentation and getting everything in the right format.)

@benrimmington
Copy link
Contributor

If you want to set the file timestamps to the current time, the simplest method is one of:

import Foundation

extension URL {

  func touch_1() throws {
    try withUnsafeFileSystemRepresentation { path in
      guard 0 == utimes(path, nil) else {
        throw NSError(domain: NSPOSIXErrorDomain, code: Int(errno))
      }
    }
  }

  @available(macOS 10.13, iOS 11.0, tvOS 11.0, watchOS 4.0, *)
  func touch_2() throws {
    try withUnsafeFileSystemRepresentation { path in
      guard 0 == utimensat(AT_FDCWD, path, nil, 0) else {
        throw NSError(domain: NSPOSIXErrorDomain, code: Int(errno))
      }
    }
  }
}
  • Foundation should re-export the POSIX APIs, so there's no need to import Darwin, etc.

    • However, you can use #if canImport(Darwin) instead of #if MACISH as needed.
  • The path argument is allocated / deallocated by withUnsafeFileSystemRepresentation.

  • The nil argument specifies that the file timestamps shall be set to the current time.

@swift-ci
Copy link
Contributor Author

Comment by Nicholas Pisarro Jr (JIRA)

This is the Restore code of an incremental backup program I'm writing. It doesn't use hard links like the Unix version or Time Machine. The label "now" is actually mislabeled, since I want to restore the original date of the backed-up file. I'll relabel it.

I had trouble with {{ withUnsafeFileSystemRepresentation. #if canImport(Darwin)}} seems more robust than what I'm doing. But the real proper solution is for mgr.setAttributes to get fixed, so I don't need this platform specific code.

@benrimmington
Copy link
Contributor

@benrimmington
Copy link
Contributor

Until apple/swift-corelibs-foundation#2904 is merged, and somehow available on Raspberry Pi OS, you can avoid this bug by rounding dates to the nearest second.

extension Date {

  func rounded_SR_13300(
    _ rule: FloatingPointRoundingRule = .toNearestOrAwayFromZero
  ) -> Self {
    Self(timeIntervalSince1970: timeIntervalSince1970.rounded(rule))
  }

  mutating func round_SR_13300(
    _ rule: FloatingPointRoundingRule = .toNearestOrAwayFromZero
  ) {
    self = rounded_SR_13300(rule)
  }
}

URLResourceValues is an alternative API, with extra properties such as the contentAccessDate. It uses FileManager internally, so you'd still need to round the dates.

extension URLResourceValues {

  func rounded_SR_13300(
    _ rule: FloatingPointRoundingRule = .toNearestOrAwayFromZero
  ) -> Self {
    var values = self
    values.contentAccessDate?.round_SR_13300(rule)
    values.contentModificationDate?.round_SR_13300(rule)
    return values
  }

  mutating func round_SR_13300(
    _ rule: FloatingPointRoundingRule = .toNearestOrAwayFromZero
  ) {
    self = rounded_SR_13300(rule)
  }
}

Unlike the FileManager, which I think gets all the values at once, you have to give URL a set of resource keys. For example:

extension URL {

  func resourceValues_SR_13300() throws -> URLResourceValues {
    try resourceValues(forKeys: [
      .contentAccessDateKey,
      .contentModificationDateKey,
    ])
  }

  mutating func setResourceValues_SR_13300(
    _ values: URLResourceValues
  ) throws {
    try setResourceValues(values.rounded_SR_13300())
  }
}

@benrimmington
Copy link
Contributor

I'll mark this issue as resolved: apple/swift-corelibs-foundation#2904 has been merged.

Swift 5.4 will likely be released in March 2021, if it follows the pattern of previous releases.

@swift-ci swift-ci transferred this issue from apple/swift-issues Apr 25, 2022
@shahmishal shahmishal transferred this issue from apple/swift May 5, 2022
This issue was closed.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

2 participants