use remove_or_rename to fix deleting files in use on Windows

This commit is contained in:
Wolf Vollprecht 2021-12-01 23:00:38 +01:00
parent ccc00331f1
commit 6241df934d
8 changed files with 101 additions and 33 deletions

View File

@ -19,6 +19,7 @@
#include "fetch.hpp"
#include "mamba_fs.hpp"
#include "match_spec.hpp"
#include "output.hpp"
#include "package_cache.hpp"
#include "package_handling.hpp"
@ -152,6 +153,8 @@ namespace mamba
History::UserRequest m_history_entry;
Transaction* m_transaction;
std::vector<MatchSpec> m_requested_specs;
bool m_force_reinstall = false;
};

View File

@ -11,6 +11,7 @@
#include "context.hpp"
#include "mamba_fs.hpp"
#include "match_spec.hpp"
namespace mamba
{
@ -26,7 +27,9 @@ namespace mamba
{
public:
TransactionContext();
TransactionContext(const fs::path& prefix, const std::string& py_version);
TransactionContext(const fs::path& prefix,
const std::string& py_version,
const std::vector<MatchSpec>& requested_specs);
bool has_python;
fs::path target_prefix;
@ -38,6 +41,7 @@ namespace mamba
bool always_copy = false;
bool always_softlink = false;
bool compile_pyc = true;
std::vector<MatchSpec> requested_specs;
};
} // namespace mamba

View File

@ -385,7 +385,7 @@ namespace mamba
std::string quote_for_shell(const std::vector<std::string>& arguments,
const std::string& shell = "");
void remove_or_rename(const fs::path& path);
std::size_t remove_or_rename(const fs::path& path);
// Unindent a string literal
std::string unindent(const char* p);
@ -417,7 +417,7 @@ namespace mamba
#endif
if (!outfile.good())
{
LOG_ERROR << "Error opening " << path << ": " << strerror(errno);
LOG_ERROR << "Error opening for writing " << path << ": " << strerror(errno);
}
return outfile;
@ -434,7 +434,7 @@ namespace mamba
#endif
if (!infile.good())
{
LOG_ERROR << "Error opening " << path << ": " << strerror(errno);
LOG_ERROR << "Error opening for reading " << path << ": " << strerror(errno);
}
return infile;

View File

@ -684,7 +684,8 @@ namespace mamba
LOG_TRACE << "Unlinking '" << dst.string() << "'";
std::error_code err;
if (!fs::remove(dst, err))
if (remove_or_rename(dst) == 0)
LOG_DEBUG << "Error when removing file '" << dst.string() << "' will be ignored";
// TODO what do we do with empty directories?
@ -692,7 +693,7 @@ namespace mamba
auto parent_path = dst.parent_path();
while (fs::is_empty(parent_path))
{
fs::remove(parent_path);
remove_or_rename(parent_path);
parent_path = parent_path.parent_path();
}
return true;
@ -859,7 +860,6 @@ namespace mamba
}
return std::make_tuple(validate::sha256sum(dst), rel_dst);
}
#else
std::size_t padding_size
= (path_data.prefix_placeholder.size() > new_prefix.size())
@ -917,7 +917,6 @@ namespace mamba
}
}
#endif
return std::make_tuple(validate::sha256sum(dst), rel_dst);
}
@ -968,6 +967,9 @@ namespace mamba
LOG_TRACE << "soft-linked '" << src.string() << "'" << std::endl
<< " --> '" << dst.string() << "'";
fs::copy_symlink(src, dst);
// we need to wait until all files are linked to compute the SHA256 sum!
// otherwise the file that's pointed to might not be linked yet.
return std::make_tuple("", rel_dst);
}
else
{
@ -1084,13 +1086,12 @@ namespace mamba
LOG_TRACE << "Opening: " << m_source / "info" / "paths.json";
auto paths_data = read_paths(m_source);
LOG_TRACE << "Opening: " << m_source / "info" / "repodata_record.json";
LOG_INFO << "Opening: " << m_source / "info" / "repodata_record.json";
std::ifstream repodata_f = open_ifstream(m_source / "info" / "repodata_record.json");
repodata_f >> index_json;
std::string f_name = index_json["name"].get<std::string>() + "-"
+ index_json["version"].get<std::string>() + "-"
+ index_json["build"].get<std::string>();
std::string f_name = m_pkg_info.str();
LOG_DEBUG << "Linking package '" << f_name << "' from '" << m_source.string() << "'";
@ -1162,12 +1163,32 @@ namespace mamba
paths_json["paths"].push_back(json_record);
}
for (std::size_t i = 0; i < paths_data.size(); ++i)
{
auto& path = paths_data[i];
if (path.path_type == PathType::SOFTLINK)
{
paths_json["paths"][i]["sha256_in_prefix"]
= validate::sha256sum(m_context->target_prefix / files_record[i]);
}
}
LOG_DEBUG << paths_data.size() << " files linked";
out_json = index_json;
out_json["paths_data"] = paths_json;
out_json["files"] = files_record;
out_json["requested_spec"] = "TODO";
MatchSpec* requested_spec = nullptr;
for (auto& ms : m_context->requested_specs)
{
if (ms.name == m_pkg_info.name)
{
requested_spec = &ms;
}
}
out_json["requested_spec"] = requested_spec != nullptr ? requested_spec->str() : "";
out_json["package_tarball_full_path"] = std::string(m_source) + ".tar.bz2";
out_json["extracted_package_dir"] = m_source;

View File

@ -447,6 +447,9 @@ namespace mamba
queue_free(&q);
queue_free(&decision);
queue_free(&job);
m_transaction_context = TransactionContext(
Context::instance().target_prefix, find_python_version(), specs_to_install);
}
@ -533,6 +536,8 @@ namespace mamba
Console::instance().json_down("actions");
Console::instance().json_write({ { "PREFIX", Context::instance().target_prefix } });
}
m_transaction_context = TransactionContext(
Context::instance().target_prefix, find_python_version(), solver.install_specs());
}
MTransaction::~MTransaction()
@ -691,7 +696,6 @@ namespace mamba
}
Console::stream() << "\nTransaction starting";
m_transaction_context = TransactionContext(prefix.path(), find_python_version());
History::UserRequest ur = History::UserRequest::prefilled();
TransactionRollback rollback;

View File

@ -74,10 +74,13 @@ namespace mamba
compile_pyc = Context::instance().compile_pyc;
}
TransactionContext::TransactionContext(const fs::path& prefix, const std::string& py_version)
TransactionContext::TransactionContext(const fs::path& prefix,
const std::string& py_version,
const std::vector<MatchSpec>& requested_specs)
: has_python(py_version.size() != 0)
, target_prefix(prefix)
, python_version(py_version)
, requested_specs(requested_specs)
{
auto& ctx = Context::instance();
compile_pyc = ctx.compile_pyc;

View File

@ -586,32 +586,41 @@ namespace mamba
}
}
void remove_or_rename(const fs::path& path)
std::size_t remove_or_rename(const fs::path& path)
{
if (fs::is_directory(path))
std::error_code ec;
std::size_t result = 0;
if (!fs::exists(path, ec))
return 0;
if (fs::is_directory(path, ec))
{
try
{
fs::remove_all(path);
}
catch (const fs::filesystem_error& e)
{
LOG_ERROR << "Caught a filesystem error: " << e.what();
throw std::runtime_error("Could not remove directory " + path.string());
}
result = fs::remove_all(path, ec);
}
else
{
try
result = fs::remove(path, ec);
}
if (ec)
{
int counter = 0;
while (ec)
{
fs::remove(path);
}
catch (const fs::filesystem_error& e)
{
LOG_ERROR << "Caught a filesystem error: " << e.what();
throw std::runtime_error("Could not remove file " + path.string());
LOG_ERROR << "Caught a filesystem error: " << ec.message();
fs::path new_path = path;
new_path.replace_extension(new_path.extension().string() + ".mamba_trash");
fs::rename(path, new_path, ec);
if (!ec)
return 1;
counter += 1;
LOG_ERROR << "ERROR " << ec.message() << " sleeping for " << counter * 2;
if (counter > 5)
throw std::runtime_error("Couldnt delete file.");
std::this_thread::sleep_for(std::chrono::seconds(counter * 2));
}
}
return result;
}
std::string unindent(const char* p)

View File

@ -5,6 +5,7 @@ import random
import shutil
import string
import subprocess
from pathlib import Path
import pytest
@ -115,6 +116,29 @@ class TestRemove:
assert "xframe" in removed_names
assert res["actions"]["PREFIX"] == TestRemove.prefix
def test_remove_in_use(self, env_created):
install("python=3.9", "-n", self.env_name, "--json", no_dry_run=True)
if platform.system() == "Windows":
pyexe = Path(self.prefix) / "python.exe"
else:
pyexe = Path(self.prefix) / "bin" / "python"
pyproc = subprocess.Popen(pyexe, stdin=None, stdout=None, stderr=None)
res = remove("python", "-v", "-p", self.prefix, no_dry_run=True)
if platform.system() == "Windows":
print(pyexe.exists())
pyexe_trash = Path(str(pyexe) + ".mamba_trash")
print(pyexe_trash)
print(pyexe_trash.exists())
assert pyexe.exists() == False
assert pyexe_trash.exists()
subprocess.Popen("TASKKILL /F /PID {pid} /T".format(pid=pyproc.pid))
else:
assert pyexe.exists() == False
pyproc.kill()
class TestRemoveConfig: