Use xattrs to mark shared opaque outputs on Mac (#1007)

The implementation for Windows remains the same and continues to use magic timestamps.

On Mac, instead of timestamps extended attributes are used. Concretely, a file is a shared opaque output IFF it has a com.microsoft.buildxl:shared_opaque_output attribute and it's value is equal to 42 (at some point in the future we could insert a more meaningful value here, e.g., PipId)

AB#1607996
This commit is contained in:
Aleksandar Milicevic 2019-10-07 11:49:29 -07:00 коммит произвёл GitHub
Родитель 46091c1b57
Коммит 7e32af8aa6
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
3 изменённых файлов: 165 добавлений и 76 удалений

Просмотреть файл

@ -21,7 +21,8 @@ readonly FULLY_CACHED=0
readonly NOT_FULLY_CACHED=1
# this is the magic timestamp ("2003-03-03 3:03:03") translated to UTC Epoch seconds
readonly magicTimestamp=$(date -j -u -f "%Y-%m-%d %H:%M:%S" "2003-03-03 3:03:03" +%s)
# readonly magicTimestamp=$(date -j -u -f "%Y-%m-%d %H:%M:%S" "2003-03-03 3:03:03" +%s)
readonly magicXattrName="com.microsoft.buildxl:shared_opaque_output"
function run_build_and_check_stuff {
local expectGraphReloadedStatus=$1
@ -58,16 +59,15 @@ function run_build_and_check_stuff {
print_info $(if [[ $expectFullyCachedStatus == $FULLY_CACHED ]]; then echo "Verified build fully cached"; else echo "Verified build NOT fully cached"; fi)
fi
# check that all files in all shared opaque directories (whose name is 'sod*') have the magic timestamp for 'Btime'
# check that all files in all shared opaque directories (whose name is 'sod*') have the magic xattr
local sodFilesFile="$MY_DIR/sod-files.txt"
find "$MY_DIR/out/objects" -type d -name 'sod*' -exec find {} -type f -o -type l \; > "$sodFilesFile"
local sodFilesTimestamps=$(cat "$sodFilesFile" | xargs stat -t "%s" -f "%SB" | sort | uniq)
if [[ $sodFilesTimestamps != $magicTimestamp ]]; then
print_error "Some files in the some shared output directories don't have the magic timestamp ('2003-03-03 3:03:03', i.e., $magicTimestamp) for Btime"
cat "$sodFilesFile" | xargs stat -t "%s" -f "Btime: %SB, path: %N"
rm -f "$sodFilesFile"
return 4
fi
for f in `cat $sodFilesFile`; do
xattr -s "$f" | grep -q $magicXattrName || {
print_error "File '$f' does not have the magic xattr '$magicXattrName'"
return 4
}
done
rm -f "$sodFilesFile"
}

Просмотреть файл

@ -2,6 +2,7 @@
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
using System;
using System.Runtime.InteropServices;
using System.Security.AccessControl;
using BuildXL.Native.IO;
using BuildXL.Utilities;
@ -10,31 +11,152 @@ using static BuildXL.Utilities.FormattableStringEx;
namespace BuildXL.Processes
{
/// <summary>
/// Utility class for identifying a file as being an output of a shared opaque. This helps the scrubber to be more precautious when deleting files under shared opaques.
/// Utility class for identifying a file as being an output of a shared opaque.
/// This helps the scrubber to be more precautious when deleting files under shared opaques.
/// </summary>
/// <remarks>
/// Currently, we use two different strategies when running on Windows and non-Windows platforms
/// - on Windows, we set a magic timestamp as file's creation (birth) date
/// - on Mac, we set an extended attribute with a special name.
/// On Mac, some tools tend to change the timestamps (even the birth date) which is the reason for this.
///
/// TODO: eventually we should unify this and use the same mechanism for all platforms, if possible.
/// </remarks>
public static class SharedOpaqueOutputHelper
{
/// <summary>
/// Flags the given path as being an output under a shared opaque by setting the creation time to <see cref="WellKnownTimestamps.OutputInSharedOpaqueTimestamp"/>
/// </summary>
/// <exception cref="BuildXLException">When the timestamp cannot be set</exception>
public static void SetPathAsSharedOpaqueOutput(string expandedPath)
private static class Win
{
try
/// <summary>
/// Flags the given path as being an output under a shared opaque by setting the creation time to
/// <see cref="WellKnownTimestamps.OutputInSharedOpaqueTimestamp"/>.
/// </summary>
/// <exception cref="BuildXLException">When the timestamp cannot be set</exception>
public static void SetPathAsSharedOpaqueOutput(string expandedPath)
{
// Only the creation time is used to identify a file as the output of a shared opaque
FileUtilities.SetFileTimestamps(expandedPath, new FileTimestamps(WellKnownTimestamps.OutputInSharedOpaqueTimestamp));
}
catch (BuildXLException ex)
{
// On (unsafe) double writes, a race can occur, we give the other contender a second chance
if (IsSharedOpaqueOutput(expandedPath))
// In the case of a no replay, this case can happen if the file got into the cache as a static output,
// but later was made a shared opaque output without a content change.
// Make sure we allow for attribute writing first
var writeAttributesDenied = !FileUtilities.HasWritableAttributeAccessControl(expandedPath);
if (writeAttributesDenied)
{
return;
FileUtilities.SetFileAccessControl(expandedPath, FileSystemRights.WriteAttributes | FileSystemRights.WriteExtendedAttributes, allow: true);
}
// Since these files should be just created outputs, this shouldn't happen and we bail out hard.
throw new BuildXLException(I($"Failed to open output file '{expandedPath}' for writing."), ex);
try
{
// Only the creation time is used to identify a file as the output of a shared opaque
FileUtilities.SetFileTimestamps(expandedPath, new FileTimestamps(WellKnownTimestamps.OutputInSharedOpaqueTimestamp));
}
catch (BuildXLException ex)
{
// On (unsafe) double writes, a race can occur, we give the other contender a second chance
if (IsSharedOpaqueOutput(expandedPath))
{
return;
}
// Since these files should be just created outputs, this shouldn't happen and we bail out hard.
throw new BuildXLException(I($"Failed to open output file '{expandedPath}' for writing."), ex);
}
finally
{
// Restore the attributes as they were originally set
if (writeAttributesDenied)
{
FileUtilities.SetFileAccessControl(expandedPath, FileSystemRights.WriteAttributes | FileSystemRights.WriteExtendedAttributes, allow: false);
}
}
}
/// <summary>
/// Checks if the given path is an output under a shared opaque by verifying whether <see cref="WellKnownTimestamps.OutputInSharedOpaqueTimestamp"/> is the creation time of the file
/// </summary>
/// <remarks>
/// If the given path is a directory, it is always considered part of a shared opaque
/// </remarks>
public static bool IsSharedOpaqueOutput(string expandedPath)
{
try
{
var creationTime = FileUtilities.GetFileTimestamps(expandedPath).CreationTime;
return creationTime == WellKnownTimestamps.OutputInSharedOpaqueTimestamp;
}
catch (BuildXLException ex)
{
throw new BuildXLException(I($"Failed to open output file '{expandedPath}' for reading."), ex);
}
}
}
private static unsafe class Unix
{
private const string MY_XATTR_NAME = "com.microsoft.buildxl:shared_opaque_output";
// arbitrary value; in the future, we could store something more useful here (e.g., the producer PipId or something)
private const long MY_XATTR_VALUE = 42;
// from xattr.h:
// #define XATTR_NOFOLLOW 0x0001 /* Don't follow symbolic links */
private const int XATTR_NOFOLLOW = 1;
[DllImport("libc", EntryPoint = "setxattr")]
private static extern int SetXattr(
[MarshalAs(UnmanagedType.LPStr)] string path,
[MarshalAs(UnmanagedType.LPStr)] string name,
void *value,
ulong size,
uint position,
int options);
[DllImport("libc", EntryPoint = "getxattr")]
private static extern long GetXattr(
[MarshalAs(UnmanagedType.LPStr)] string path,
[MarshalAs(UnmanagedType.LPStr)] string name,
void *value,
ulong size,
uint position,
int options);
/// <summary>
/// Flags the given path as being an output under a shared opaque by setting
/// <see cref="MY_XATTR_NAME"/> xattr to a <see cref="MY_XATTR_VALUE"/>.
/// </summary>
public static void SetPathAsSharedOpaqueOutput(string expandedPath)
{
long value = MY_XATTR_VALUE;
var err = SetXattr(expandedPath, MY_XATTR_NAME, &value, sizeof(long), 0, XATTR_NOFOLLOW);
if (err != 0)
{
throw new BuildXLException(I($"Failed to set '{MY_XATTR_NAME}' extended attribute. Error: {err}"));
}
}
/// <summary>
/// Checks if the given path is an output under a shared opaque by checking if
/// it contains extended attribute by <see cref="MY_XATTR_NAME"/> name.
/// </summary>
public static bool IsSharedOpaqueOutput(string expandedPath)
{
long value = 0;
uint valueSize = sizeof(long);
var resultSize = GetXattr(expandedPath, MY_XATTR_NAME, &value, valueSize, 0, XATTR_NOFOLLOW);
return resultSize == valueSize && value == MY_XATTR_VALUE;
}
}
/// <summary>
/// Marks a given path as "shared opaque output"
/// </summary>
/// <exception cref="BuildXLException">When unsuccessful</exception>
public static void SetPathAsSharedOpaqueOutput(string expandedPath)
{
if (OperatingSystemHelper.IsUnixOS)
{
Unix.SetPathAsSharedOpaqueOutput(expandedPath);
}
else
{
Win.SetPathAsSharedOpaqueOutput(expandedPath);
}
}
@ -74,18 +196,9 @@ namespace BuildXL.Processes
return true;
}
DateTime creationTime;
try
{
creationTime = FileUtilities.GetFileTimestamps(expandedPath).CreationTime;
}
catch (BuildXLException ex)
{
throw new BuildXLException(I($"Failed to open output file '{expandedPath}' for reading."), ex);
}
return creationTime == WellKnownTimestamps.OutputInSharedOpaqueTimestamp;
return OperatingSystemHelper.IsUnixOS
? Unix.IsSharedOpaqueOutput(expandedPath)
: Win.IsSharedOpaqueOutput(expandedPath);
}
/// <summary>
@ -93,37 +206,13 @@ namespace BuildXL.Processes
/// </summary>
public static void EnforceFileIsSharedOpaqueOutput(string expandedPath)
{
// If the file has the right timestamps already, then there is nothing to do.
// If the file is already marked, then there is nothing to do.
if (IsSharedOpaqueOutput(expandedPath))
{
return;
}
// In the case of a no replay, this case can happen if the file got into the cache as a static output, but later was made a shared opaque
// output without a content change.
#if PLATFORM_WIN
// Make sure we allow for attribute writing first
var writeAttributesDenied = !FileUtilities.HasWritableAttributeAccessControl(expandedPath);
if (writeAttributesDenied)
{
FileUtilities.SetFileAccessControl(expandedPath, FileSystemRights.WriteAttributes | FileSystemRights.WriteExtendedAttributes, allow: true);
}
#endif
try
{
SetPathAsSharedOpaqueOutput(expandedPath);
}
finally
{
#if PLATFORM_WIN
// Restore the attributes as they were originally set
if (writeAttributesDenied)
{
FileUtilities.SetFileAccessControl(expandedPath, FileSystemRights.WriteAttributes | FileSystemRights.WriteExtendedAttributes, allow: false);
}
#endif
}
SetPathAsSharedOpaqueOutput(expandedPath);
}
}
}

Просмотреть файл

@ -171,7 +171,7 @@ namespace Test.BuildXL.Engine
}
[Fact]
public void OutputsUnderSharedOpaqueHaveAWellKnownCreationTimeEvenOnCacheReplay()
public void OutputsUnderSharedOpaqueAreProperlyMarkedEvenOnCacheReplay()
{
var file = X("out/SharedOpaqueOutput.txt");
var spec0 = ProduceFileUnderSharedOpaque(file);
@ -185,8 +185,8 @@ namespace Test.BuildXL.Engine
// Make sure the file was produced
Assert.True(File.Exists(producedFile));
// And that it has a well-known creation time
XAssert.AreEqual(WellKnownTimestamps.OutputInSharedOpaqueTimestamp, FileUtilities.GetFileTimestamps(producedFile).CreationTime);
// And that it has been marked as shared opaque output
XAssert.IsTrue(SharedOpaqueOutputHelper.IsSharedOpaqueOutput(producedFile));
File.Delete(producedFile);
@ -196,12 +196,12 @@ namespace Test.BuildXL.Engine
IgnoreWarnings();
// Make sure this is a cache replay
AssertVerboseEventLogged(EventId.ProcessPipCacheHit);
// And check the timestamp again
XAssert.AreEqual(WellKnownTimestamps.OutputInSharedOpaqueTimestamp, FileUtilities.GetFileTimestamps(producedFile).CreationTime);
// And check again that the file is still properly marked
XAssert.IsTrue(SharedOpaqueOutputHelper.IsSharedOpaqueOutput(producedFile));
}
[Fact]
public void StaticOutputBecomingASharedOpaqueOutputHasWellKnownCreationTime()
public void StaticOutputBecomingASharedOpaqueOutputIsProperlyMarkedAsSharedOpaqueOutput()
{
var file = X("out/MyFile.txt");
@ -216,8 +216,8 @@ namespace Test.BuildXL.Engine
// Make sure the file was produced
Assert.True(File.Exists(producedFile));
// Since this is a statically declared file, the creation timestamp should not be the one used for shared opaques
XAssert.AreNotEqual(WellKnownTimestamps.OutputInSharedOpaqueTimestamp, FileUtilities.GetFileTimestamps(producedFile).CreationTime);
// Since this is a statically declared file, it shouldn't be marked as a shared opaque output
XAssert.IsFalse(SharedOpaqueOutputHelper.IsSharedOpaqueOutput(producedFile), "Statically declared file marked as shared opaque output");
// Delete the created file (since scrubbing is not on for this test, we have to simulate it)
File.Delete(producedFile);
@ -230,7 +230,7 @@ namespace Test.BuildXL.Engine
RunEngine(rememberAllChangedTrackedInputs: true);
// Check the timestamp is the right one
XAssert.AreEqual(WellKnownTimestamps.OutputInSharedOpaqueTimestamp, FileUtilities.GetFileTimestamps(producedFile).CreationTime);
XAssert.IsTrue(SharedOpaqueOutputHelper.IsSharedOpaqueOutput(producedFile), "SOD file not marked on cache miss");
// Delete the file
File.Delete(producedFile);
@ -242,11 +242,11 @@ namespace Test.BuildXL.Engine
// Make sure this is a cache replay
AssertVerboseEventLogged(EventId.ProcessPipCacheHit);
// Check the timestamp is the right one now
XAssert.AreEqual(WellKnownTimestamps.OutputInSharedOpaqueTimestamp, FileUtilities.GetFileTimestamps(producedFile).CreationTime);
XAssert.IsTrue(SharedOpaqueOutputHelper.IsSharedOpaqueOutput(producedFile), "SOD file not marked on cache replay");
}
[Fact]
public void SharedOpaqueOutputsOnFailingPipHaveWellKnownCreationTime()
public void SharedOpaqueOutputsOnFailingPipMustBeProperlyMarked()
{
var file = X("out/MyFile.txt");
var objDir = Configuration.Layout.ObjectDirectory.ToString(Context.PathTable);
@ -261,7 +261,7 @@ namespace Test.BuildXL.Engine
AssertErrorEventLogged(EventId.PipProcessError);
// Check the timestamp is the right one
XAssert.AreEqual(WellKnownTimestamps.OutputInSharedOpaqueTimestamp, FileUtilities.GetFileTimestamps(producedFile).CreationTime);
XAssert.IsTrue(SharedOpaqueOutputHelper.IsSharedOpaqueOutput(producedFile), "SOD file not marked on pip failure");
}
private string ProduceFileUnderSharedOpaque(string file, bool failOnExit = false, string dependencies = "") => ProduceFileUnderDirectory(file, isDynamic: true, failOnExit, dependencies);