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

Cannot change LastWriteTime or LastAccessTime of a symlink #38824

Closed
Tracked by #57205 ...
Liturgist opened this issue Jul 6, 2020 · 10 comments · Fixed by #52639
Closed
Tracked by #57205 ...

Cannot change LastWriteTime or LastAccessTime of a symlink #38824

Liturgist opened this issue Jul 6, 2020 · 10 comments · Fixed by #52639
Assignees
Milestone

Comments

@Liturgist
Copy link

Liturgist commented Jul 6, 2020

Description

It does not appear to be possible to change the LastWriteTime or LastAccessTime of a symlink. It works on hardlinks.

PS C:\src\t> $SleepTime = 61
PS C:\src\t> $BaseDir = 'C:\src\t'
PS C:\src\t> $BaseFile = Join-Path -Path $BaseDir -ChildPath 'yy.txt'
PS C:\src\t> $SymlinkFile = Join-Path -Path $BaseDir -ChildPath 'yy-slink.txt'
PS C:\src\t> $HardlinkFile = Join-Path -Path $BaseDir -ChildPath 'yy-hlink.txt'
PS C:\src\t> if (Test-Path -Path $BaseFile) { Remove-Item -Path $BaseFile }
PS C:\src\t> if (Test-Path -Path $SymlinkFile) { Remove-Item -Path $SymlinkFile }
PS C:\src\t> if (Test-Path -Path $HardlinkFile) { Remove-Item -Path $HardlinkFile }
PS C:\src\t> New-Item -Path $BaseFile -ItemType File -Value "this is a new file`r`n"


    Directory: C:\src\t

Mode                 LastWriteTime         Length Name
----                 -------------         ------ ----
-a---          2020-07-06    14:23             20 yy.txt

PS C:\src\t> New-Item -ItemType SymbolicLink -Path $SymlinkFile -Target $BaseFile


    Directory: C:\src\t

Mode                 LastWriteTime         Length Name
----                 -------------         ------ ----
la---          2020-07-06    14:23              0 yy-slink.txt -> C:\src\t\yy.txt

PS C:\src\t> Get-ChildItem -Path 'C:\src\t' -Filter 'yy*.txt' | Format-List 'Name','*Time'

Name           : yy.txt
CreationTime   : 2020-07-06 14:23:41
LastAccessTime : 2020-07-06 14:23:41
LastWriteTime  : 2020-07-06 14:23:41

Name           : yy-slink.txt
CreationTime   : 2020-07-06 14:23:41
LastAccessTime : 2020-07-06 14:23:41
LastWriteTime  : 2020-07-06 14:23:41


PS C:\src\t> Start-Sleep -Seconds $SleepTime
PS C:\src\t> $NewTimestamp = Get-Date
PS C:\src\t> Set-ItemProperty -Path $SymlinkFile -Name LastAccessTime -Value $NewTimestamp
PS C:\src\t> Set-ItemProperty -Path $SymlinkFile -Name LastWriteTime -Value $NewTimestamp
PS C:\src\t> Start-Sleep -Seconds 5
PS C:\src\t> Get-ChildItem -Path $BaseDir -Filter 'yy*.txt' | Format-List 'Name','*Time'

Name           : yy.txt
CreationTime   : 2020-07-06 14:23:41
LastAccessTime : 2020-07-06 14:24:42
LastWriteTime  : 2020-07-06 14:24:42

Name           : yy-slink.txt
CreationTime   : 2020-07-06 14:23:41
LastAccessTime : 2020-07-06 14:23:41
LastWriteTime  : 2020-07-06 14:23:41


PS C:\src\t> & cmd.exe /C "DIR /TA yy*"
 Volume in drive C has no label.
 Volume Serial Number is 0E33-300C

 Directory of C:\src\t

2020-07-06  14:23    <SYMLINK>      yy-slink.txt [C:\src\t\yy.txt]
2020-07-06  14:24                20 yy.txt
               2 File(s)             20 bytes
               0 Dir(s)  739,939,135,488 bytes free
PS C:\src\t> & cmd.exe /C "DIR /TW yy*"
 Volume in drive C has no label.
 Volume Serial Number is 0E33-300C

 Directory of C:\src\t

2020-07-06  14:23    <SYMLINK>      yy-slink.txt [C:\src\t\yy.txt]
2020-07-06  14:24                20 yy.txt
               2 File(s)             20 bytes
               0 Dir(s)  739,939,135,488 bytes free
PS C:\src\t> New-Item -ItemType HardLink -Path $HardlinkFile -Target $BaseFile


    Directory: C:\src\t

Mode                 LastWriteTime         Length Name
----                 -------------         ------ ----
-a---          2020-07-06    14:24             20 yy-hlink.txt

PS C:\src\t> Get-ChildItem -Path $BaseDir -Filter 'yy*.txt' | Format-List 'Name','*Time'

Name           : yy.txt
CreationTime   : 2020-07-06 14:23:41
LastAccessTime : 2020-07-06 14:24:42
LastWriteTime  : 2020-07-06 14:24:42

Name           : yy-hlink.txt
CreationTime   : 2020-07-06 14:23:41
LastAccessTime : 2020-07-06 14:24:42
LastWriteTime  : 2020-07-06 14:24:42

Name           : yy-slink.txt
CreationTime   : 2020-07-06 14:23:41
LastAccessTime : 2020-07-06 14:23:41
LastWriteTime  : 2020-07-06 14:23:41


PS C:\src\t> Start-Sleep -Seconds $SleepTime
PS C:\src\t> $NewTimestamp = Get-Date
PS C:\src\t> Set-ItemProperty -Path $HardlinkFile -Name LastAccessTime -Value $NewTimestamp
PS C:\src\t> Set-ItemProperty -Path $HardlinkFile -Name LastWriteTime -Value $NewTimestamp
PS C:\src\t> Start-Sleep -Seconds 5
PS C:\src\t> Get-ChildItem -Path $BaseDir -Filter 'yy*.txt' | Format-List 'Name','*Time'

Name           : yy.txt
CreationTime   : 2020-07-06 14:23:41
LastAccessTime : 2020-07-06 14:24:42
LastWriteTime  : 2020-07-06 14:24:42

Name           : yy-hlink.txt
CreationTime   : 2020-07-06 14:23:41
LastAccessTime : 2020-07-06 14:25:50
LastWriteTime  : 2020-07-06 14:25:50

Name           : yy-slink.txt
CreationTime   : 2020-07-06 14:23:41
LastAccessTime : 2020-07-06 14:23:41
LastWriteTime  : 2020-07-06 14:23:41


PS C:\src\t> & cmd.exe /C "DIR /TA yy*"
 Volume in drive C has no label.
 Volume Serial Number is 0E33-300C

 Directory of C:\src\t

2020-07-06  14:25                20 yy-hlink.txt
2020-07-06  14:23    <SYMLINK>      yy-slink.txt [C:\src\t\yy.txt]
2020-07-06  14:24                20 yy.txt
               3 File(s)             40 bytes
               0 Dir(s)  739,939,672,064 bytes free
PS C:\src\t> & cmd.exe /C "DIR /TW yy*"
 Volume in drive C has no label.
 Volume Serial Number is 0E33-300C

 Directory of C:\src\t

2020-07-06  14:25                20 yy-hlink.txt
2020-07-06  14:23    <SYMLINK>      yy-slink.txt [C:\src\t\yy.txt]
2020-07-06  14:24                20 yy.txt
               3 File(s)             40 bytes
               0 Dir(s)  739,939,672,064 bytes free

Configuration

C:>dotnet --info
.NET SDK (reflecting any global.json):
 Version:   5.0.100-preview.5.20279.10
 Commit:    8139f1b74e

Runtime Environment:
 OS Name:     Windows
 OS Version:  10.0.18363
 OS Platform: Windows
 RID:         win10-x64
 Base Path:   C:\Users\pwatson\AppData\Local\Microsoft\dotnet\sdk\5.0.100-preview.5.20279.10\

Host (useful for support):
  Version: 5.0.0-preview.5.20278.1
  Commit:  4ae4e2fe08

.NET SDKs installed:
  2.2.402 [C:\Program Files\dotnet\sdk]
  3.0.100-rc1-014190 [C:\Program Files\dotnet\sdk]
  3.1.301 [C:\Program Files\dotnet\sdk]
  5.0.100-preview.5.20279.10 [C:\Users\pwatson\AppData\Local\Microsoft\dotnet\sdk]

.NET runtimes installed:
  Microsoft.AspNetCore.All 2.1.19 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.All]
  Microsoft.AspNetCore.All 2.2.7 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.All]
  Microsoft.AspNetCore.App 2.1.19 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App]
  Microsoft.AspNetCore.App 2.2.7 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App]
  Microsoft.AspNetCore.App 3.0.0-rc1.19457.4 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App]
  Microsoft.AspNetCore.App 3.1.5 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App]
  Microsoft.AspNetCore.App 5.0.0-preview.5.20279.2 [C:\Users\pwatson\AppData\Local\Microsoft\dotnet\shared\Microsoft.AspNetCore.App]
  Microsoft.NETCore.App 2.1.19 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
  Microsoft.NETCore.App 2.2.7 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
  Microsoft.NETCore.App 3.0.0-rc1-19456-20 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
  Microsoft.NETCore.App 3.1.4 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
  Microsoft.NETCore.App 3.1.5 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
  Microsoft.NETCore.App 5.0.0-preview.5.20278.1 [C:\Users\pwatson\AppData\Local\Microsoft\dotnet\shared\Microsoft.NETCore.App]
  Microsoft.WindowsDesktop.App 3.0.0-rc1-19456-20 [C:\Program Files\dotnet\shared\Microsoft.WindowsDesktop.App]
  Microsoft.WindowsDesktop.App 3.1.4 [C:\Program Files\dotnet\shared\Microsoft.WindowsDesktop.App]
  Microsoft.WindowsDesktop.App 3.1.5 [C:\Program Files\dotnet\shared\Microsoft.WindowsDesktop.App]
  Microsoft.WindowsDesktop.App 5.0.0-preview.5.20278.3 [C:\Users\pwatson\AppData\Local\Microsoft\dotnet\shared\Microsoft.WindowsDesktop.App]

Regression?

Unknown

Other information

PS C:\src\t> $PSVersionTable

Name                           Value
----                           -----
PSVersion                      7.0.1
PSEdition                      Core
GitCommitId                    7.0.1
OS                             Microsoft Windows 10.0.18363
Platform                       Win32NT
PSCompatibleVersions           {1.0, 2.0, 3.0, 4.0…}
PSRemotingProtocolVersion      2.3
SerializationVersion           1.1.0.1
WSManStackVersion              3.0
@Dotnet-GitSync-Bot Dotnet-GitSync-Bot added the untriaged New issue has not been triaged by the area owner label Jul 6, 2020
@Dotnet-GitSync-Bot
Copy link
Collaborator

I couldn't figure out the best area label to add to this issue. If you have write-permissions please help me learn by adding exactly one area label.

@NikolaMilosavljevic
Copy link
Member

@jeffschwMSFT this doesn't seem related to Setup, can you reassign to FileSystem owners?

@carlossanlop carlossanlop added this to the Future milestone Aug 6, 2020
@mklement0
Copy link

@Liturgist, allow me to provide a more succinct repro:

Run either in PowerShell [Core] v6+ or in Windows PowerShell, in which case the session must be elevated:

# Create a test target file.
$null > tmp.txt

# Create a symlink to it.
$link = New-Item -Force -ItemType SymbolicLink tmpL.txt -Target tmp.txt

# Try to modify the *symlink*'s LastWriteTime.
$link.LastWriteTime = '2020-01-01'

# List both the link and the target, which shows that the *target*'s
# LastWriteTime was modified.
Get-Item tmp.txt, tmpL.txt -OutVariable result

# Clean up.
$result | Remove-Item

In PowerShell Core, you'll get output such as the following, showing that the target was modified, not the link itself:

    Directory: /Users/jdoe

UnixMode   User             Group                 LastWriteTime           Size Name
--------   ----             -----                 -------------           ---- ----
-rw-r--r-- mklement         staff                1/1/2020 00:00              0 tmp.txt
lrwxr-xr-x mklement         staff                8/6/2020 13:42              7 tmpL.txt -> /Users/jdoe/tmp.txt

See also: The proposed symlink API in #24271

@carlossanlop
Copy link
Member

Thanks for the details.

I know in Linux, there are ways to manipulate the attributes of the symbolic link itself, For example you can read the file attributes using stat and you can read the symbolic link attributes using lstat. You can also use touch to alter the timestamp of a file, and you can use touch -h to alter the timestamp of the symbolic link itself.

What I would like to investigate is if Windows has similar options to modify symbolic link attributes. I know the Windows methods to create symbolic links, but I can't find a method to edit an existing symbolic link.

@Liturgist can you share more information about why you want to edit the symbolic link attributes, and specifically, the timestamps?

@carlossanlop carlossanlop removed the untriaged New issue has not been triaged by the area owner label Aug 14, 2020
@hamarb123
Copy link
Contributor

hamarb123 commented Mar 24, 2021

@carlossanlop I've implemented fixes for this in #49555.
I'm happy to make another PR for Windows as per #49555 (comment) with the changes that I removed in a617a01. These changes make it set on the symlink and they pass my additional test (SettingUpdatesPropertiesOnSymlink in BaseGetSetTimes.cs currently in PR #49555).
I'm happy to be assigned to this issue.

@carlossanlop
Copy link
Member

Thank you so much for offering your help, @hamarb123 ! I assigned this issue to you per your request. I'm looking forward to reviewing your PR.

@jozkee
Copy link
Member

jozkee commented Aug 30, 2021

From #58012 by @fitdev:

Existing C:\mysymlink.txt symlink file that points to existing physical file C:\realfile.txt:

FileInfo symlink_to_existing_file = new FileInfo(@"C:\mysymlink.txt");
This will set the timestamp on target, i.e. on C:\realfile.txt (not sure if it sets it on immediate or on final target - I only tested it with 1 level of indirection, but suspect it always sets it on the final target):

var new_CreationTimeUtc = DateTime.UtcNow;
symlink_to_existing_file.CreationTimeUtc = new_CreationTimeUtc;
However when we compare the timestamps, the getter still returns "old" value! Why? Because, presumably it reads the value from the symlink itself, and not the target:

symlink_to_existing_file.Refresh();
var are_timestamps_the_same = symlink_to_existing_file.CreationTimeUtc == new_CreationTimeUtc; //should be true, but is actually false!
The issue seems to affect timestamps only (i.e. Creation, LastWrite, and LastAccess times). Attributes seem to always be set on the actual instance, and not on target, which is the way it should be IMHO.

The example above concerns symlinks, FileInfo, and CreationTimeUtc, however the discrepancy extends to DirectoryInfo as well, plus the static helper methods on File and Directory, so both files and directories seem to be affected.

At the very least the getters/setters should be consistent.

@hamarb123
Copy link
Contributor

Just to clarify my reasoning as to why I think we should proceed with the changes in #49555 and #52639 (setting it on the reparse point itself) is because (in no particular order):

  1. Not every reparse point is a 'link' and thus how should the .NET runtime react to setting the dates on the target if the target is something that is known to be a link to another file in the filesystem, but doesn't know how to do so; obviously it would have to not do it in these cases and this would be somewhat inconsistent behaviour in my opinion
  2. When we get an API for getting the target of a reparse point (when it is a link that is known, including symlinks), it will be trivial to set the date on the target if desired, but if setting the date on the 'file' that is a link sets it on the target, how would one set it on the symlink itself (or any other supported link type), it appears that it wouldn't be possible, perhaps there is a workaround we could implement for this if we went this direction (eg. new method)
  3. Also, how often do we need to actually change those dates on files (and folders), because writing to a file updates its modification date (even when you write to a symlink it updates the target's modification date, which is obviously desired behaviour), and creating a file updates its creation date. So if you actually want to change these dates arbitrarily, do you actually want to be limited to not ever setting the dates on symlinks, or even having to go out of your way to set it on a symlink itself if we made it get and set the target's info; I think most of the scenarios (including my own) either want to set it on the symlink, or don't care. I can see an argument for getting the target's modification date when opening a symlink file as if it were a real file (eg. as a command-line argument) because the date of the link itself would be not what you want.
  4. If we did make it so the APIs worked on the target instead of the link itself, because it is likely that people will be setting dates on things that are reparse points and would prefer the behaviour of getting and setting the dates on the targets, if the target doesn't exist (eg. on an unmounted drive), most code would not check if the target exists today and would just check if their path exists (ie. the reparse point) and would be satisfied to call something like File.SetCreationTime but this would always throw, thus forcing them to check if it is a reparse point and then if it is check if the target exists, thus defeating the purpose of making it set on the target (in my opinion) because they've ended up with the same amount of code as before, just instead they still get/set it on their original path instead of their target path because of this edge case.

This is why I think we should set and get from the link itself in my opinion.

@ghost ghost removed the in-pr There is an active PR which will close this issue when it is merged label Oct 9, 2021
@ghost ghost added the in-pr There is an active PR which will close this issue when it is merged label Oct 20, 2021
jozkee pushed a commit that referenced this issue Nov 15, 2021
…#52639)

* Implement most of the fix for #38824

Reverts the changes in the seperate PR a617a01 to fix #38824.
Does not re-enable the test because that relies on #49555, will add a seperate commit to whichever is merged last to enable the SettingUpdatesPropertiesOnSymlink test.

* Most of the code for PR #52639 to fix #38824

• Deal with merge conflicts
• Add FSOPT_NOFOLLOW for macOS and use it in setattrlist
• Use AT_SYMLINK_NOFOLLOW with utimensat (note: there doesn't seem to be an equivalent for utimes)
• Add SettingUpdatesPropertiesOnSymlink test to test if it actually sets it on the symlink itself (to the best that we can anyway ie. write + read the times)
• Specify FILE_FLAG_OPEN_REPARSE_POINT for CreateFile in FileSystem.Windows.cs (is there any other CreateFile calls that need this?)

* Remove additional FILE_FLAG_OPEN_REPARSE_POINT

I added FILE_FLAG_OPEN_REPARSE_POINT before and it was then added in another PR, this removes the duplicate entry.

* Add missing override keywords

Add missing override keywords for abstract members in the tests.

* Fix access modifiers

Fix access modifiers for tests - were meant to be protected rather than public

* Add more symlink tests, rearrange files

• Move symlink creation api to better spot in non-base files
• Add IsDirectory property for some of the new tests
• Change abstract symlink api to CreateSymlink that accepts path and target
• Create a CreateSymlinkToItem method that creates a symlink to an item that may be relative that uses the new/modified abstract CreateSymlink method
• Add SettingUpdatesPropertiesCore to avoid code duplication
• Add tests for the following variants: normal/symlink, target exists/doesn't exist, absolute/relative target
• Exclude nonexistent symlink target tests on Unix for Directories since they are counted as files

* Fix return type of CreateSymlink in File/GetSetTimes.cs

* Remove browser from new symlink tests as it doesn't support creation of symlinks

Remove browser from new symlink tests as it doesn't support creation of symlinks

* Use lutimes, improve code readability, simplify tests

• Use lutimes when it's available
• Extract dwFlagsAndAttributes to a variable
• Use same year for all tests
• Checking to delete old symlink is unnecessary, so don't
• Replace var with explicit type

* Change year in test to 2014 to reduce diff

* Rename symlink tests, use 1 core symlink times function, and check that target times don't change

Rename symlink tests, use 1 core symlink times function, and check that target times don't change

* Inline RunSymlinkTestPart 'function'

Inline RunSymlinkTestPart 'function' so that the code can be reordered so the access time test can be valid.

* Share CreateSymlinkToItem call in tests and update comment for clarity

* Update symlink time tests

• Make SettingUpdatesPropertiesOnSymlink a theory
• Remove special case for Unix due to #52639 (comment) (will revert if fails)
• Rename isRelative to targetIsRelative for clarity

* Remove unnecessary Assert.All

* Changes to SettingUpdatesPropertiesOnSymlink test

• Rename item field to link field
• Don't use if one-liner
• Use all time functions since only using UTC isn't necessary
• Remove the now-defunct IsDirectory property since we aren't checking it anymore

* Remove unnecessary fsi.Refresh()

• Remove unnecessary fsi.Refresh() since atime is only updated when reading a file

* Updates to test and pal_time.c

• Remove targetIsRelative cases
• Multi-line if statement
• Combine HAVE_LUTIMES and #else conditions to allow more code charing

* Remove trailing space
@ghost ghost removed the in-pr There is an active PR which will close this issue when it is merged label Nov 15, 2021
@wegylexy
Copy link
Contributor

wegylexy commented Dec 5, 2021

Is there a quick workaround for 6.0.0-rtm to set LastWriteTimeUtc of a symlink?

@jozkee
Copy link
Member

jozkee commented Dec 6, 2021

You can use the pinvokes yourself to write a function that updates the symlink rather than the target, for Windows that would be:

using Microsoft.Win32.SafeHandles;
using System.ComponentModel;
using System.Runtime.InteropServices;

const int FileBasicInfo = 0;

SafeFileHandle handle = 
    CreateFilePrivate(
        "C:/your-link", 
        0x40000000 /* Write */, 
        FileShare.ReadWrite | FileShare.Delete, 
        lpSecurityAttributes: 0,
        FileMode.Open, 
        0x00200000 /* Open reparse point */,
        IntPtr.Zero);

var basicInfo = new FILE_BASIC_INFO()
{
    CreationTime = -1,
    LastAccessTime = -1,
    LastWriteTime = DateTime.UtcNow.AddDays(1).ToFileTime(),
    ChangeTime = -1,
    FileAttributes = 0
};

unsafe
{
    if (!SetFileInformationByHandle(handle, FileBasicInfo, &basicInfo, (uint)sizeof(FILE_BASIC_INFO)))
    {
        throw new Win32Exception(Marshal.GetLastWin32Error());
    }
}

Console.WriteLine("Success!");

// interop code:

[DllImport("kernel32.dll", EntryPoint = "CreateFileW", CharSet = CharSet.Unicode, ExactSpelling = true, SetLastError = true)]
static unsafe extern SafeFileHandle CreateFilePrivate(
            string lpFileName,
            int dwDesiredAccess,
            FileShare dwShareMode,
            uint lpSecurityAttributes,
            FileMode dwCreationDisposition,
            int dwFlagsAndAttributes,   
            IntPtr hTemplateFile);


[DllImport("kernel32.dll", ExactSpelling = true, SetLastError = true)]
static unsafe extern bool SetFileInformationByHandle(
            SafeFileHandle hFile,
            int FileInformationClass,
            void* lpFileInformation,
            uint dwBufferSize);

struct FILE_BASIC_INFO
{
    internal long CreationTime;
    internal long LastAccessTime;
    internal long LastWriteTime;
    internal long ChangeTime;
    internal uint FileAttributes;
}

You can expand above code to also work with other OSes by looking at what .NET uses internally.

@ghost ghost locked as resolved and limited conversation to collaborators Jan 6, 2022
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

Successfully merging a pull request may close this issue.

10 participants