blob: e3b7464dd59b602d721e6e0ab3415c0a0874b5a0 [file] [log] [blame]
//===- VirtualOutputBackends.cpp - Virtual output backends ----------------===//
//
// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
// See https://llvm.org/LICENSE.txt for license information.
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
//
//===----------------------------------------------------------------------===//
//
// This file implements vfs::OutputBackend.
//
//===----------------------------------------------------------------------===//
#include "llvm/Support/VirtualOutputBackends.h"
#include "llvm/ADT/ScopeExit.h"
#include "llvm/Support/FileSystem.h"
#include "llvm/Support/LockFileManager.h"
#include "llvm/Support/MemoryBuffer.h"
#include "llvm/Support/Path.h"
#include "llvm/Support/Process.h"
#include "llvm/Support/Signals.h"
using namespace llvm;
using namespace llvm::vfs;
void ProxyOutputBackend::anchor() {}
void OnDiskOutputBackend::anchor() {}
IntrusiveRefCntPtr<OutputBackend> vfs::makeNullOutputBackend() {
struct NullOutputBackend : public OutputBackend {
IntrusiveRefCntPtr<OutputBackend> cloneImpl() const override {
return const_cast<NullOutputBackend *>(this);
}
Expected<std::unique_ptr<OutputFileImpl>>
createFileImpl(StringRef Path, std::optional<OutputConfig>) override {
return std::make_unique<NullOutputFileImpl>();
}
};
return makeIntrusiveRefCnt<NullOutputBackend>();
}
IntrusiveRefCntPtr<OutputBackend> vfs::makeFilteringOutputBackend(
IntrusiveRefCntPtr<OutputBackend> UnderlyingBackend,
std::function<bool(StringRef, std::optional<OutputConfig>)> Filter) {
struct FilteringOutputBackend : public ProxyOutputBackend {
Expected<std::unique_ptr<OutputFileImpl>>
createFileImpl(StringRef Path,
std::optional<OutputConfig> Config) override {
if (Filter(Path, Config))
return ProxyOutputBackend::createFileImpl(Path, Config);
return std::make_unique<NullOutputFileImpl>();
}
IntrusiveRefCntPtr<OutputBackend> cloneImpl() const override {
return makeIntrusiveRefCnt<FilteringOutputBackend>(
getUnderlyingBackend().clone(), Filter);
}
FilteringOutputBackend(
IntrusiveRefCntPtr<OutputBackend> UnderlyingBackend,
std::function<bool(StringRef, std::optional<OutputConfig>)> Filter)
: ProxyOutputBackend(std::move(UnderlyingBackend)),
Filter(std::move(Filter)) {
assert(this->Filter && "Expected a non-null function");
}
std::function<bool(StringRef, std::optional<OutputConfig>)> Filter;
};
return makeIntrusiveRefCnt<FilteringOutputBackend>(
std::move(UnderlyingBackend), std::move(Filter));
}
IntrusiveRefCntPtr<OutputBackend>
vfs::makeMirroringOutputBackend(IntrusiveRefCntPtr<OutputBackend> Backend1,
IntrusiveRefCntPtr<OutputBackend> Backend2) {
struct ProxyOutputBackend1 : public ProxyOutputBackend {
using ProxyOutputBackend::ProxyOutputBackend;
};
struct ProxyOutputBackend2 : public ProxyOutputBackend {
using ProxyOutputBackend::ProxyOutputBackend;
};
struct MirroringOutput final : public OutputFileImpl, raw_pwrite_stream {
Error keep() final {
flush();
return joinErrors(F1->keep(), F2->keep());
}
Error discard() final {
flush();
return joinErrors(F1->discard(), F2->discard());
}
raw_pwrite_stream &getOS() final { return *this; }
void write_impl(const char *Ptr, size_t Size) override {
F1->getOS().write(Ptr, Size);
F2->getOS().write(Ptr, Size);
}
void pwrite_impl(const char *Ptr, size_t Size, uint64_t Offset) override {
this->flush();
F1->getOS().pwrite(Ptr, Size, Offset);
F2->getOS().pwrite(Ptr, Size, Offset);
}
uint64_t current_pos() const override { return F1->getOS().tell(); }
size_t preferred_buffer_size() const override {
return PreferredBufferSize;
}
void reserveExtraSpace(uint64_t ExtraSize) override {
F1->getOS().reserveExtraSpace(ExtraSize);
F2->getOS().reserveExtraSpace(ExtraSize);
}
bool is_displayed() const override {
return F1->getOS().is_displayed() && F2->getOS().is_displayed();
}
bool has_colors() const override {
return F1->getOS().has_colors() && F2->getOS().has_colors();
}
void enable_colors(bool enable) override {
raw_pwrite_stream::enable_colors(enable);
F1->getOS().enable_colors(enable);
F2->getOS().enable_colors(enable);
}
MirroringOutput(std::unique_ptr<OutputFileImpl> F1,
std::unique_ptr<OutputFileImpl> F2)
: PreferredBufferSize(std::max(F1->getOS().GetBufferSize(),
F1->getOS().GetBufferSize())),
F1(std::move(F1)), F2(std::move(F2)) {
// Don't double buffer.
this->F1->getOS().SetUnbuffered();
this->F2->getOS().SetUnbuffered();
}
size_t PreferredBufferSize;
std::unique_ptr<OutputFileImpl> F1;
std::unique_ptr<OutputFileImpl> F2;
};
struct MirroringOutputBackend : public ProxyOutputBackend1,
public ProxyOutputBackend2 {
Expected<std::unique_ptr<OutputFileImpl>>
createFileImpl(StringRef Path,
std::optional<OutputConfig> Config) override {
std::unique_ptr<OutputFileImpl> File1;
std::unique_ptr<OutputFileImpl> File2;
if (Error E =
ProxyOutputBackend1::createFileImpl(Path, Config).moveInto(File1))
return std::move(E);
if (Error E =
ProxyOutputBackend2::createFileImpl(Path, Config).moveInto(File2))
return joinErrors(std::move(E), File1->discard());
// Skip the extra indirection if one of these is a null output.
if (isa<NullOutputFileImpl>(*File1)) {
consumeError(File1->discard());
return std::move(File2);
}
if (isa<NullOutputFileImpl>(*File2)) {
consumeError(File2->discard());
return std::move(File1);
}
return std::make_unique<MirroringOutput>(std::move(File1),
std::move(File2));
}
IntrusiveRefCntPtr<OutputBackend> cloneImpl() const override {
return IntrusiveRefCntPtr<ProxyOutputBackend1>(
makeIntrusiveRefCnt<MirroringOutputBackend>(
ProxyOutputBackend1::getUnderlyingBackend().clone(),
ProxyOutputBackend2::getUnderlyingBackend().clone()));
}
void Retain() const { ProxyOutputBackend1::Retain(); }
void Release() const { ProxyOutputBackend1::Release(); }
MirroringOutputBackend(IntrusiveRefCntPtr<OutputBackend> Backend1,
IntrusiveRefCntPtr<OutputBackend> Backend2)
: ProxyOutputBackend1(std::move(Backend1)),
ProxyOutputBackend2(std::move(Backend2)) {}
};
assert(Backend1 && "Expected actual backend");
assert(Backend2 && "Expected actual backend");
return IntrusiveRefCntPtr<ProxyOutputBackend1>(
makeIntrusiveRefCnt<MirroringOutputBackend>(std::move(Backend1),
std::move(Backend2)));
}
static OutputConfig
applySettings(std::optional<OutputConfig> &&Config,
const OnDiskOutputBackend::OutputSettings &Settings) {
if (!Config)
Config = Settings.DefaultConfig;
if (Settings.DisableTemporaries)
Config->setNoAtomicWrite();
if (Settings.DisableRemoveOnSignal)
Config->setNoDiscardOnSignal();
return *Config;
}
namespace {
class OnDiskOutputFile final : public OutputFileImpl {
public:
Error keep() override;
Error discard() override;
raw_pwrite_stream &getOS() override {
assert(FileOS && "Expected valid file");
if (BufferOS)
return *BufferOS;
return *FileOS;
}
/// Attempt to open a temporary file for \p OutputPath.
///
/// This tries to open a uniquely-named temporary file for \p OutputPath,
/// possibly also creating any missing directories if \a
/// OnDiskOutputConfig::UseTemporaryCreateMissingDirectories is set in \a
/// Config.
///
/// \post FD and \a TempPath are initialized if this is successful.
Error tryToCreateTemporary(std::optional<int> &FD);
Error initializeFD(std::optional<int> &FD);
Error initializeStream();
Error reset();
OnDiskOutputFile(StringRef OutputPath, std::optional<OutputConfig> Config,
const OnDiskOutputBackend::OutputSettings &Settings)
: Config(applySettings(std::move(Config), Settings)),
OutputPath(OutputPath.str()) {}
OutputConfig Config;
const std::string OutputPath;
std::optional<std::string> TempPath;
std::optional<raw_fd_ostream> FileOS;
std::optional<buffer_ostream> BufferOS;
};
} // end namespace
static Error createDirectoriesOnDemand(StringRef OutputPath,
OutputConfig Config,
llvm::function_ref<Error()> CreateFile) {
return handleErrors(CreateFile(), [&](std::unique_ptr<ECError> EC) {
if (EC->convertToErrorCode() != std::errc::no_such_file_or_directory ||
Config.getNoImplyCreateDirectories())
return Error(std::move(EC));
StringRef ParentPath = sys::path::parent_path(OutputPath);
if (std::error_code EC = sys::fs::create_directories(ParentPath))
return make_error<OutputError>(ParentPath, EC);
return CreateFile();
});
}
Error OnDiskOutputFile::tryToCreateTemporary(std::optional<int> &FD) {
// Create a temporary file.
// Insert -%%%%%%%% before the extension (if any), and because some tools
// (noticeable, clang's own GlobalModuleIndex.cpp) glob for build
// artifacts, also append .tmp.
StringRef OutputExtension = sys::path::extension(OutputPath);
SmallString<128> ModelPath =
StringRef(OutputPath).drop_back(OutputExtension.size());
ModelPath += "-%%%%%%%%";
ModelPath += OutputExtension;
ModelPath += ".tmp";
return createDirectoriesOnDemand(OutputPath, Config, [&]() -> Error {
int NewFD;
SmallString<128> UniquePath;
if (std::error_code EC =
sys::fs::createUniqueFile(ModelPath, NewFD, UniquePath))
return make_error<TempFileOutputError>(ModelPath, OutputPath, EC);
if (Config.getDiscardOnSignal())
sys::RemoveFileOnSignal(UniquePath);
TempPath = UniquePath.str().str();
FD.emplace(NewFD);
return Error::success();
});
}
Error OnDiskOutputFile::initializeFD(std::optional<int> &FD) {
assert(OutputPath != "-" && "Unexpected request for FD of stdout");
// Disable temporary file for other non-regular files, and if we get a status
// object, also check if we can write and disable write-through buffers if
// appropriate.
if (Config.getAtomicWrite()) {
sys::fs::file_status Status;
sys::fs::status(OutputPath, Status);
if (sys::fs::exists(Status)) {
if (!sys::fs::is_regular_file(Status))
Config.setNoAtomicWrite();
// Fail now if we can't write to the final destination.
if (!sys::fs::can_write(OutputPath))
return make_error<OutputError>(
OutputPath,
std::make_error_code(std::errc::operation_not_permitted));
}
}
// If (still) using a temporary file, try to create it (and return success if
// that works).
if (Config.getAtomicWrite())
if (!errorToBool(tryToCreateTemporary(FD)))
return Error::success();
// Not using a temporary file. Open the final output file.
return createDirectoriesOnDemand(OutputPath, Config, [&]() -> Error {
int NewFD;
sys::fs::OpenFlags OF = sys::fs::OF_None;
if (Config.getTextWithCRLF())
OF |= sys::fs::OF_TextWithCRLF;
else if (Config.getText())
OF |= sys::fs::OF_Text;
if (Config.getAppend())
OF |= sys::fs::OF_Append;
if (std::error_code EC = sys::fs::openFileForWrite(
OutputPath, NewFD, sys::fs::CD_CreateAlways, OF))
return convertToOutputError(OutputPath, EC);
FD.emplace(NewFD);
if (Config.getDiscardOnSignal())
sys::RemoveFileOnSignal(OutputPath);
return Error::success();
});
}
Error OnDiskOutputFile::initializeStream() {
// Open the file stream.
if (OutputPath == "-") {
std::error_code EC;
FileOS.emplace(OutputPath, EC);
if (EC)
return make_error<OutputError>(OutputPath, EC);
} else {
std::optional<int> FD;
if (Error E = initializeFD(FD))
return E;
FileOS.emplace(*FD, /*shouldClose=*/true);
}
// Buffer the stream if necessary.
if (!FileOS->supportsSeeking() && !Config.getText())
BufferOS.emplace(*FileOS);
return Error::success();
}
namespace {
class OpenFileRAII {
static const int InvalidFd = -1;
public:
int Fd = InvalidFd;
~OpenFileRAII() {
if (Fd != InvalidFd)
llvm::sys::Process::SafelyCloseFileDescriptor(Fd);
}
};
enum class FileDifference : uint8_t {
/// The source and destination paths refer to the exact same file.
IdenticalFile,
/// The source and destination paths refer to separate files with identical
/// contents.
SameContents,
/// The source and destination paths refer to separate files with different
/// contents.
DifferentContents
};
} // end anonymous namespace
static Expected<FileDifference>
areFilesDifferent(const llvm::Twine &Source, const llvm::Twine &Destination) {
if (sys::fs::equivalent(Source, Destination))
return FileDifference::IdenticalFile;
OpenFileRAII SourceFile;
sys::fs::file_status SourceStatus;
// If we can't open the source file, fail.
if (std::error_code EC = sys::fs::openFileForRead(Source, SourceFile.Fd))
return convertToOutputError(Source, EC);
// If we can't stat the source file, fail.
if (std::error_code EC = sys::fs::status(SourceFile.Fd, SourceStatus))
return convertToOutputError(Source, EC);
OpenFileRAII DestFile;
sys::fs::file_status DestStatus;
// If we can't open the destination file, report different.
if (std::error_code Error =
sys::fs::openFileForRead(Destination, DestFile.Fd))
return FileDifference::DifferentContents;
// If we can't open the destination file, report different.
if (std::error_code Error = sys::fs::status(DestFile.Fd, DestStatus))
return FileDifference::DifferentContents;
// If the files are different sizes, they must be different.
uint64_t Size = SourceStatus.getSize();
if (Size != DestStatus.getSize())
return FileDifference::DifferentContents;
// If both files are zero size, they must be the same.
if (Size == 0)
return FileDifference::SameContents;
// The two files match in size, so we have to compare the bytes to determine
// if they're the same.
std::error_code SourceRegionErr;
sys::fs::mapped_file_region SourceRegion(
sys::fs::convertFDToNativeFile(SourceFile.Fd),
sys::fs::mapped_file_region::readonly, Size, 0, SourceRegionErr);
if (SourceRegionErr)
return convertToOutputError(Source, SourceRegionErr);
std::error_code DestRegionErr;
sys::fs::mapped_file_region DestRegion(
sys::fs::convertFDToNativeFile(DestFile.Fd),
sys::fs::mapped_file_region::readonly, Size, 0, DestRegionErr);
if (DestRegionErr)
return FileDifference::DifferentContents;
if (memcmp(SourceRegion.const_data(), DestRegion.const_data(), Size) != 0)
return FileDifference::DifferentContents;
return FileDifference::SameContents;
}
Error OnDiskOutputFile::reset() {
// Destroy the streams to flush them.
BufferOS.reset();
if (!FileOS)
return Error::success();
// Remember the error in raw_fd_ostream to be reported later.
std::error_code EC = FileOS->error();
// Clear the error to avoid fatal error when reset.
FileOS->clear_error();
FileOS.reset();
return errorCodeToError(EC);
}
Error OnDiskOutputFile::keep() {
if (auto E = reset())
return E;
// Close the file descriptor and remove crash cleanup before exit.
auto RemoveDiscardOnSignal = make_scope_exit([&]() {
if (Config.getDiscardOnSignal())
sys::DontRemoveFileOnSignal(TempPath ? *TempPath : OutputPath);
});
if (!TempPath)
return Error::success();
// See if we should append instead of move.
if (Config.getAppend() && OutputPath != "-") {
// Read TempFile for the content to append.
auto Content = MemoryBuffer::getFile(*TempPath);
if (!Content)
return convertToTempFileOutputError(*TempPath, OutputPath,
Content.getError());
while (1) {
// Attempt to lock the output file.
// Only one process is allowed to append to this file at a time.
llvm::LockFileManager Locked(OutputPath);
switch (Locked) {
case llvm::LockFileManager::LFS_Error: {
// If we error acquiring a lock, we cannot ensure appends
// to the trace file are atomic - cannot ensure output correctness.
Locked.unsafeRemoveLockFile();
return convertToOutputError(
OutputPath, std::make_error_code(std::errc::no_lock_available));
}
case llvm::LockFileManager::LFS_Owned: {
// Lock acquired, perform the write and release the lock.
std::error_code EC;
llvm::raw_fd_ostream Out(OutputPath, EC, llvm::sys::fs::OF_Append);
if (EC)
return convertToOutputError(OutputPath, EC);
Out << (*Content)->getBuffer();
Out.close();
Locked.unsafeRemoveLockFile();
if (Out.has_error())
return convertToOutputError(OutputPath, Out.error());
// Remove temp file and done.
(void)sys::fs::remove(*TempPath);
return Error::success();
}
case llvm::LockFileManager::LFS_Shared: {
// Someone else owns the lock on this file, wait.
switch (Locked.waitForUnlock(256)) {
case llvm::LockFileManager::Res_Success:
LLVM_FALLTHROUGH;
case llvm::LockFileManager::Res_OwnerDied: {
continue; // try again to get the lock.
}
case llvm::LockFileManager::Res_Timeout: {
// We could error on timeout to avoid potentially hanging forever, but
// it may be more likely that an interrupted process failed to clear
// the lock, causing other waiting processes to time-out. Let's clear
// the lock and try again right away. If we do start seeing compiler
// hangs in this location, we will need to re-consider.
Locked.unsafeRemoveLockFile();
continue;
}
}
break;
}
}
}
}
if (Config.getOnlyIfDifferent()) {
auto Result = areFilesDifferent(*TempPath, OutputPath);
if (!Result)
return Result.takeError();
switch (*Result) {
case FileDifference::IdenticalFile:
// Do nothing for a self-move.
return Error::success();
case FileDifference::SameContents:
// Files are identical; remove the source file.
(void)sys::fs::remove(*TempPath);
return Error::success();
case FileDifference::DifferentContents:
break; // Rename the file.
}
}
// Move temporary to the final output path and remove it if that fails.
std::error_code RenameEC = sys::fs::rename(*TempPath, OutputPath);
if (!RenameEC)
return Error::success();
// FIXME: TempPath should be in the same directory as OutputPath but try to
// copy the output to see if makes any difference. If this path is used,
// investigate why we need to copy.
RenameEC = sys::fs::copy_file(*TempPath, OutputPath);
(void)sys::fs::remove(*TempPath);
if (!RenameEC)
return Error::success();
return make_error<TempFileOutputError>(*TempPath, OutputPath, RenameEC);
}
Error OnDiskOutputFile::discard() {
// Destroy the streams to flush them.
if (auto E = reset())
return E;
// Nothing on the filesystem to remove for stdout.
if (OutputPath == "-")
return Error::success();
auto discardPath = [&](StringRef Path) {
std::error_code EC = sys::fs::remove(Path);
sys::DontRemoveFileOnSignal(Path);
return EC;
};
// Clean up the file that's in-progress.
if (!TempPath)
return convertToOutputError(OutputPath, discardPath(OutputPath));
return convertToTempFileOutputError(*TempPath, OutputPath,
discardPath(*TempPath));
}
Error OnDiskOutputBackend::makeAbsolute(SmallVectorImpl<char> &Path) const {
return convertToOutputError(StringRef(Path.data(), Path.size()),
sys::fs::make_absolute(Path));
}
Expected<std::unique_ptr<OutputFileImpl>>
OnDiskOutputBackend::createFileImpl(StringRef Path,
std::optional<OutputConfig> Config) {
SmallString<256> AbsPath;
if (Path != "-") {
AbsPath = Path;
if (Error E = makeAbsolute(AbsPath))
return std::move(E);
Path = AbsPath;
}
auto File = std::make_unique<OnDiskOutputFile>(Path, Config, Settings);
if (Error E = File->initializeStream())
return std::move(E);
return std::move(File);
}