libutil: delete store paths through directory handles on Windows - #16359
libutil: delete store paths through directory handles on Windows#16359awsmadi wants to merge 6 commits into
Conversation
|
In the second commit void deletePath(const std::filesystem::path & path)
{
uint64_t dummy;
deletePath(path, dummy);
}should go back to the portable part of the code, since it would now be duplicated. |
|
@awsmadi O bot that is nonetheless somewhat useful: I will probably not merge this PR until we can have a descriptor-based recursive deletion on Windows too. |
|
Given that path canonicalisation isn't actually implemented on windows, this is not very worthwhile currently. Whatever chmod is doing is definitely not something we want to keep and carry along. I'd suggest figuring that out first. |
|
Thanks both. Taking the three points in turn, with a prototype result that I think changes the picture on the second one. Descriptor-based deletionAgreed, and I am implementing it. I prototyped the API sequence first to check it is actually available, cross-compiled for Shape: open each child relative to the parent's The part I did not expect: The cause is that libstdc++'s The duplicated overloadAgreed, will do — chmod, and the canonicalisation question@xokdvium this is the part worth arguing about, and I think there is a reason to disagree. The Unix walk already does exactly this. /* Make the directory accessible. */
const auto PERM_MASK = S_IRUSR | S_IWUSR | S_IXUSR;
if ((st.st_mode & PERM_MASK) != PERM_MASK)
unix::fchmodatTryNoFollow(parentfd, name, st.st_mode | PERM_MASK);Deletion already relaxes permissions to make a tree removable, and does it Worth separating from that: the attribute is there because canonicalisation chmods store contents to On the larger point — you are right that Windows path canonicalisation is unresolved, and it is broader than deletion. I have a write-up of that question and deliberately have not opened it as a separate issue, because the decision would then be spread across this PR, #16358, #16361 and a fourth thread. It seems more useful in one place. So, concretely, the question is: when Nix's logical POSIX path model and the host's I am not asking this PR to answer that. The deletion path has to work whatever the answer is, since a store path that is read-only on disk has to be removable either way. |
Both platforms spelled it identically:
void deletePath(const std::filesystem::path & path)
{
uint64_t dummy;
deletePath(path, dummy);
}
so it belongs in the portable file rather than in each. That means the
accounting overload has to be the primitive on both, which was already true on
Unix and is now true on Windows too.
No behaviour change.
Assisted-by: Claude Code (claude-opus-5)
7a1eb55 to
c858ffc
Compare
Groundwork for a recursive deletion that never re-resolves a path. Win32 has no `openat`. `NtCreateFile` does have one, via `OBJECT_ATTRIBUTES::RootDirectory`, and `windows/file-system-at.cc` already wrapped it as a file-local `ntOpenAt`. Move that into `nix::windows` proper and declare it in a private header so the deletion code in `file-system.cc` can use it too, rather than growing a second copy. Add `tryNtOpenAt` alongside, which reports a missing object as `std::nullopt` instead of throwing. That mirrors the `ENOENT`/`ENOTDIR` cases the Unix `maybeFstatat` already treats as absence. Then implement `maybeFstatat` for Windows. It has been declared in the portable `file-system-at.hh` all along with only a Unix definition, so any Windows caller would have failed to link; there were none. Opens no-follow, which is what `AT_SYMLINK_NOFOLLOW` gives the Unix version, so a symlink is reported as itself. No behaviour change: nothing calls the new code yet. Assisted-by: Claude Code (claude-opus-5)
c858ffc to
d912253
Compare
|
Flagging an overlap I should have found earlier. @Ericson2314 pointed at #15244 on a different thread, and having read it: my second commit here largely duplicates work already in that PR, and does it less well. #15244's
So commit 2 of this PR should go away in favour of #15244. I would rather say that than have someone review it. Commit 3 is not duplicated. #15244 leaves std::filesystem::remove_all(path, ec);with What I would suggestLand #15244 first, then reduce this PR to the deletion walk rebased onto it, using On reviving #15244, in case it is useful: I tried rebasing it onto current master. Its merge base is from 2026-04-23 and master is 800 commits past it, but the damage is small — two conflicting files, One caveat on that estimate: |
`std::filesystem::remove_all` re-resolves the path at every step, so each component can be swapped between the check and the removal. Mirror the Unix walk instead: hold a handle to the parent, and do every operation relative to it, so a name is resolved exactly once. That also fixes a case `remove_all` cannot do at all. A tree containing a directory symlink fails to delete, reporting `ENOENT` and leaving the tree in place, because libstdc++'s `std::filesystem` on MinGW does not recognise reparse points -- `symlink_status` calls a directory symlink a plain directory with no error set. Opening with `FILE_OPEN_REPARSE_POINT` removes the link as a link and leaves its target alone. Win32 has no `openat`; `NtCreateFile` does, via `OBJECT_ATTRIBUTES::RootDirectory`, which the previous commit exposed as `ntOpenAt`. Listing is `GetFileInformationByHandleEx(FileFullDirectoryInfo)` on the directory's own handle. Deletion is `SetFileInformationByHandle` with `FileDispositionInfo`. The `FileDispositionInfoEx` form with POSIX semantics would unlink the name immediately rather than on last-handle-close, but it needs a newer API level than the `_WIN32_WINNT=0x0602` this project sets, and the difference does not matter here: each handle is closed before its parent is deleted, so a child's name is already gone by then. One handle per entry carries classification, listing, the attribute change and the deletion. Opening twice would resolve the name twice, which is the race being removed. Names are collected before anything is deleted, since deleting entries while an enumeration of the same directory is in flight is not defined to visit each entry exactly once. The read-only attribute is cleared through that handle. A file carrying it cannot be deleted, and the store is full of them, because canonicalisation chmods store contents to 0444 and `chmod()` on Windows is `::_wchmod`, which turns a missing write bit into that attribute. This is the counterpart of the Unix walk relaxing permissions with `fchmodatTryNoFollow` before recursing, and doing it through the handle means the object whose attribute changes is necessarily the one about to be deleted. `bytesFreed` is now reported rather than left at zero. Policy copied from the Unix implementation instead of invented: count a non-directory's size at one or two links and nothing at three or more, two being assumed to mean an optimised store entry. Garbage collection previously reported freeing nothing on Windows however much it deleted, and `gc.cc` says "Rely on deletePath() accounting". Per-entry failures are collected and rethrown at the end, so one undeletable entry does not abandon the rest of the tree. Also Unix's behaviour. Assisted-by: Claude Code (claude-opus-5)
|
Force-pushed a fix for a defect in this PR that I found by running the cross-built test suites, not
assert(path.is_absolute());That assertion does not exist on master — master's Windows Not a failing test — an abort at test 28 of 47, taking the remaining 19 with it. The first fix was wrong, which is worth recordingI assumed the offending input was a relative or POSIX-rooted path, and replaced the assert with The VerifiedBuilt master + #16364 + this PR (#16364 is needed at all, or the suite dies during static
The four that this PR fixes are Of the 6 that remain, 5 are Combined branch used for the measurement, in case you want to reproduce: |
d912253 to
8578932
Compare
|
Yes I think this PR should contain #15244 (or your rebased version of it) as a starting point. |
|
Restructured onto it. The series is now your two commits from #15244, rebased onto master, then mine on My third commit, the one adding I have not pushed it here yet, because it does not build, and both remaining problems are in your 1. The handle-relative open is file-local. 2. What I would write, if you want me to pick: prefixing is meaningful only for the } catch (SymlinkNotAllowed & e) {
if (auto * cp = std::get_if<CanonPath>(&e.path)) {
auto full = anchor / *cp;
throw SymlinkNotAllowed(full, "path '%s' (or its ancestor) is a symlink", showPath(full));
}
throw; /* already a host path; there is nothing to anchor it to */
}That keeps today's behavior exactly, since nothing on those paths throws the host-path alternative yet. |
|
I attempted the rebase of #15244 onto master to use as this PR's base. Reporting what it needs, because Master is 808 commits past the branch point.
The hazard, in (4)
// master
int fd = openat(parentfd, name.rel_c_str(), O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC);
// #15244
int fd = openat(parentfd, name.c_str(), O_RDONLY | O_DIRECTORY | O_NOFOLLOW);Pair the accessor with the wrong type and the string handed to I would rather not guess my way through ~1340 lines of your abstraction when one wrong pairing is |
|
I went ahead and did the rebase, on the basis that a branch you can correct is more useful than another Explicitly for correction rather than for merge. Four of the resolutions are choices about how It builds: The cross build matters because The two worth your attention
On the open questionIt answered itself: Two other things you may not have hit yet: |
| /** | ||
| * Both platforms implement the accounting overload; this wrapper is the same | ||
| * either way, so it lives here rather than being duplicated in each. | ||
| */ |
There was a problem hiding this comment.
What's the point of a dozen comment in a .cc file which isn't part of the docs anyhow. Also doesn't seem like there's any need for a comment?
There was a problem hiding this comment.
Fair. It is in a .cc, so it renders nowhere, and the part of it worth keeping belongs on the declaration in file-system.hh instead. I moved it here only because deletePath stopped being Unix-only and I did not want the text to just vanish in the diff. I will put what is load-bearing on the header declaration and drop the rest.
There was a problem hiding this comment.
Correcting myself: there is nothing left to move. I went back through the branch and the comment you flagged was
/**
* Both platforms implement the accounting overload; this wrapper is the same
* either way, so it lives here rather than being duplicated in each.
*/
which sat above the moved deletePath overload in ffeea5ae5 through 8578932aa and is already gone as of 6858ed68b. GitHub is showing this thread against the current head, where the anchor line is now the wrapper body itself, which is what led me to describe it as a comment still needing rehoming. It does not -- you were right, and it was deleted before you asked.
So: no change owed here. The remaining three points in your review I am acting on.
xokdvium
left a comment
There was a problem hiding this comment.
This definitely needs more tests in libutil-tests? Esp the bits with making directories writable as needed?
| * A file carrying it cannot be deleted, and the store is full of them: | ||
| * canonicalisation chmods store contents to 0444, and `chmod()` on Windows is | ||
| * `::_wchmod`, which turns a missing write bit into exactly this attribute. |
There was a problem hiding this comment.
I don't think this actually makes much sense, considering that canonicalisation was never actually internationally thought out or implemented for windows. E.g. recent refractors to unix case basically made restorePath canonicalisation a no-op on windows.
So at the very least this comment isn't correct in the current state of things. Or maybe we can even drop the comment because it's pretty evident that it's what the existing deletePath does on Linux etc etc.
There was a problem hiding this comment.
You are right, and the specific thing I asserted is the part I had not checked. The comment leans on store canonicalisation being what produces these files, and if restorePath canonicalisation is effectively a no-op on Windows after the recent unix-case work then that justification does not hold.
What is load-bearing and does hold: chmod() on Windows is ::_wchmod, which maps a cleared write bit onto FILE_ATTRIBUTE_READONLY, and an object carrying it cannot be deleted. That is the mechanism the new tests exercise directly -- they create mode-0444 files, no store involved. I will narrow the comment to that and drop the canonicalisation claim rather than defend it.
| std::filesystem::remove_all(path, ec); // NOLINT(bugprone-unsafe-functions) | ||
| if (ec && ec != std::errc::no_such_file_or_directory) | ||
| throw SysError(ec.default_error_condition().value(), "recursively deleting %1%", PathFmt(path)); | ||
| std::vector<std::wstring> names; |
There was a problem hiding this comment.
Yes. This file is Windows-only so OsString is std::wstring here -- same type -- but OsString is what the rest of the tree spells it, so there is no reason for this one to differ. Switching it.
| for (auto & child : listByHandle(fd->get(), path)) | ||
| deletePathAt(fd->get(), path / child, bytesFreed, ex); | ||
|
|
||
| clearReadOnly(fd->get()); |
There was a problem hiding this comment.
Does this mean that attributes or clobbered on deletion failures? Doesn't seem right? Though this might already be the case with the posix implementation.
Probably needs tests for that or something?
There was a problem hiding this comment.
Confirmed, and your hedge is right too: POSIX already behaves this way.
If the disposition fails after we have cleared FILE_ATTRIBUTE_READONLY, the attribute stays cleared, so a failed deletePath leaves the tree more permissive than it found it. The Unix walk has exactly the same property -- fchmodatTryNoFollow relaxes the mode before recursing and nothing puts it back if the unlink then fails. So this is pre-existing cross-platform behaviour rather than something the Windows path introduces.
Restoring it would mean recording prior attributes per entry and reapplying them on every failure path. The POSIX side does not do that, so implementing it on Windows only would make the two diverge in a way that is harder to reason about than the current shared wart. I would rather leave the behaviour matched and say so in the comment. If you want it pinned rather than merely described I will add a test that asserts the attribute is left cleared after a failed delete, so a future change to either platform has to do it deliberately.
|
|
||
| /* Big enough that a typical directory needs one round trip, but the loop | ||
| below does not depend on that. */ | ||
| std::vector<char> buf(64 * 1024); |
There was a problem hiding this comment.
Are there any alignment requirements on the structure? Default allocator alignment is enough or not?
There was a problem hiding this comment.
Good question, and the current code gets the right answer for the wrong reason.
alignof(FILE_FULL_DIR_INFO) is 8 -- it has LARGE_INTEGER members. std::vector<char> is 1-byte aligned as a type, so the reinterpret_cast is not justified by the buffer's declared type. It happens to work because the storage comes from operator new, which guarantees __STDCPP_DEFAULT_NEW_ALIGNMENT__ (16 on x86-64), and subsequent entries are reached through NextEntryOffset, which the OS keeps 8-aligned.
So default allocator alignment is enough, but only via a guarantee the code never states, and it would break silently under an allocator that gave back 1-byte-aligned storage. I will make the requirement explicit at the declaration instead of leaving it incidental.
| `has_root_name() && has_root_directory()`, so a relative path -- or a | ||
| POSIX-rooted one like `/tmp/x` -- is not absolute even when it names a real | ||
| file, and `remove_all` accepted those too. */ | ||
| auto absPath = path.is_absolute() ? path : std::filesystem::absolute(path); |
There was a problem hiding this comment.
absPath maybe? Or do we even need that? Is there anything like AT_FDCWD we could make use of?
There was a problem hiding this comment.
There is no AT_FDCWD analogue. NtCreateFile does take a RootDirectory handle, but a null one means "treat ObjectName as a fully qualified NT path", not "resolve against the process working directory", so there is nothing to hand a relative path to.
The absolute() is not there for the open, though. The next two lines take parent_path() and open the parent by name, and parent_path() of a bare relative filename is empty -- the assert(parentPath != absPath) below is what catches that. So the path has to be absolute before it is split, independently of the AT_FDCWD question. I will say that in the comment, since right now it only explains the is_absolute() half.
| /* An empty path is a no-op. The `std::filesystem::remove_all` this replaces | ||
| treated it as one, and callers depend on that: `nix-fetchers-tests` reaches | ||
| here with an empty path while tearing down a skipped test. */ | ||
| if (path.empty()) | ||
| return; |
There was a problem hiding this comment.
This seems like a bug then? Does AutoDelete do this or some other caller? Either way this seems like a busted API somewhere.
There was a problem hiding this comment.
Agreed that it points at a caller, and I owe you a correction: I cannot reproduce the caller my comment names, so the comment overclaims and I should not have written it that confidently.
AutoDelete is specifically not it, which I checked after reading your question. The default constructor sets del{false} and deletePath() guards on if (del), so a default-constructed one never forwards its empty _path. Move-assignment goes through swap, so the temporary in delTmpDir = AutoDelete(tmpDir, true) ends up holding del == false as well. Neither route reaches here.
The only caller I can actually demonstrate is the test I added asserting the no-op, which is circular as justification. What does survive checking is behaviour compatibility: std::filesystem::remove_all, which this replaces, treats an empty path as a no-op and returns 0, so throwing here would be a behaviour change on an input callers could previously pass.
The genuinely awkward part is that the Unix _deletePath asserts is_absolute() and aborts on that same input, so the two platforms already disagree about it and neither documents why. I would rather resolve that deliberately than leave an unexplained guard on one side. Do you want both to no-op, or both to reject, with the caller fixed?
Review feedback from xokdvium on NixOS#16359. Adds a DeletePathTest fixture, modelled on MovePathTest: - readOnlyFile / treeOfReadOnlyFiles pin the behaviour the Windows walk needs clearReadOnly for. `nix::chmod(f, 0444)` is `::_wchmod` there, which sets FILE_ATTRIBUTE_READONLY and blocks deletion outright. Verified they catch it: stubbing clearReadOnly to a no-op fails exactly these two under Wine and leaves the other three passing. - nonWritableDirectory is the Unix counterpart, where the walk has to add write permission to the directory before unlinking its contents. Guarded to Unix because the read-only attribute is not honoured on Windows directories. - reportsBytesFreed, nonexistentIsNoop cover the accounting and the already-gone early return on both platforms. - emptyPathIsNoop is Windows-only. The Unix side asserts is_absolute() instead, so on a debug build the same call aborts rather than returning; that asymmetry predates this change and the test pins the side that guarantees it. Also drops the doxygen comment above the shared deletePath wrapper: it is a definition in a .cc, so it never reaches the docs, and the wrapper is self-evident.
|
Both done in 6858ed6. (Amended from a0e6126 to fold in a clang-format fixup — I had left a stray The comment — removed. You're right that a Tests — added a The two you asked about are the first two.
One thing the tests turned up that's worth knowing. Verified 816 ran / 814 passed / 2 skipped natively, and the full |
Review feedback from xokdvium on NixOS#16359. Adds a DeletePathTest fixture, modelled on MovePathTest: - readOnlyFile / treeOfReadOnlyFiles pin the behaviour the Windows walk needs clearReadOnly for. `nix::chmod(f, 0444)` is `::_wchmod` there, which sets FILE_ATTRIBUTE_READONLY and blocks deletion outright. Verified they catch it: stubbing clearReadOnly to a no-op fails exactly these two under Wine and leaves the other three passing. - nonWritableDirectory is the Unix counterpart, where the walk has to add write permission to the directory before unlinking its contents. Guarded to Unix because the read-only attribute is not honoured on Windows directories. - reportsBytesFreed, nonexistentIsNoop cover the accounting and the already-gone early return on both platforms. - emptyPathIsNoop is Windows-only. The Unix side asserts is_absolute() instead, so on a debug build the same call aborts rather than returning; that asymmetry predates this change and the test pins the side that guarantees it. Also drops the doxygen comment above the shared deletePath wrapper: it is a definition in a .cc, so it never reaches the docs, and the wrapper is self-evident.
a0e6126 to
6858ed6
Compare
The new DeletePathTest cases failed under Wine, and the cause was real rather
than a test artefact: read-only files could not be deleted at all there, which
is the case this walk exists to handle.
The delete open asked for FILE_WRITE_ATTRIBUTES so that clearReadOnly could drop
FILE_ATTRIBUTE_READONLY, which Windows requires before it honours DELETE. Wine
wants the opposite. It derives that attribute from the Unix write bits
(get_file_attributes in dlls/ntdll/unix/file.c) and refuses the open when the
mask asks for FILE_WRITE_ATTRIBUTES on a file lacking them, while allowing
DELETE. Measured on a mode-0444 file under Wine 11.0:
FILE_READ_ATTRIBUTES | SYNCHRONIZE OK
FILE_WRITE_ATTRIBUTES | SYNCHRONIZE ERROR_ACCESS_DENIED
DELETE | SYNCHRONIZE OK
So ask for it and fall back to the narrower mask when refused. DELETE access is
granted on a read-only file but the disposition is still refused while the
attribute is set, so where the handle route is unavailable the attribute is
cleared by path instead. That re-resolves the path, which this walk otherwise
avoids, and there is no handle-relative equivalent; the Unix side has the same
shape, relaxing permissions with fchmodatTryNoFollow by name rather than through
the object handle.
The full nix-util-tests suite now passes under Wine: 802 ran, 794 passed,
8 skipped.
|
Worth reporting that the tests you asked for immediately earned their keep — they failed, and for a real Read-only files could not be deleted at all under Wine, which is the case this walk exists to handle. The So it now asks for That by-path call re-resolves the path, which this walk otherwise exists to avoid, and I could not find a
Worth noting for anyone testing this by hand: running the test binary directly as root hides the bug |
- Narrow the read-only comment to the mechanism that holds. It claimed store canonicalisation produces these files, which was not verified and may not hold on Windows at all; what does hold is that `chmod()` is `::_wchmod`, which maps a cleared write bit onto `FILE_ATTRIBUTE_READONLY`. - Use `OsString` rather than spelling `std::wstring` directly. - Carry the `FILE_FULL_DIR_INFO` alignment requirement in the buffer's element type instead of relying on `operator new` over-aligning a `char` buffer.
|
@xokdvium thanks for the review, and sorry for the delay in answering it -- my thread monitoring keyed off "comments newer than the last one I handled", so once I commented on an unrelated PR your review stopped registering as outstanding. That is fixed; it is why all seven answers arrived at once. I have replied in each thread.
Two did not need a code change and I have said why in-thread. The Two are questions back to you rather than changes:
Verified before pushing: |
Rewritten to do what @Ericson2314 asked for: deletion relative to directory handles, so
std::filesystem::remove_allis out of the picture entirely. Three commits, each of which builds on its own.The previous version of this PR cleared the read-only attribute by path and kept
remove_all. That is gone.1. De-duplicate the non-accounting overload
Both platforms spelled
deletePath(path)identically, so it moves to the portable file. That requires the accounting overload to be the primitive on both, which was already true on Unix and is now true on Windows. No behaviour change.2. Handle-relative primitives on Windows
Win32 has no
openat.NtCreateFiledoes, viaOBJECT_ATTRIBUTES::RootDirectory, andwindows/file-system-at.ccalready wrapped it as a file-localntOpenAt. That moves intonix::windowsbehind a private header so the deletion code can use it rather than growing a second copy, with atryNtOpenAtalongside that reports absence asstd::nullopt.Also implements
maybeFstatatfor Windows. It has been declared in the portablefile-system-at.hhall along with only a Unix definition, so any Windows caller would have failed to link; there were none.Nothing calls this commit's code yet.
3. The walk
Mirrors
unix/file-system.cc's_deletePath. One handle per entry carries classification, listing, the attribute change and the deletion — opening twice would resolve the name twice, which is the race being removed. Listing isGetFileInformationByHandleEx(FileFullDirectoryInfo)on the directory's own handle; names are collected before anything is deleted, since deleting entries while an enumeration of the same directory is in flight is not defined to visit each entry exactly once.FILE_OPEN_REPARSE_POINTthroughout, so a symlink is removed as a link and its target is untouched.remove_all was not merely racy, it was wrong
The case that changed my mind about the shape of this. A tree containing a directory symlink cannot be deleted by
remove_allat all:libstdc++'s
std::filesystemon MinGW does not recognise reparse points —symlink_statuscalls a directory symlink a plain directory, withec = 0. Filed separately as #16361; fixed in GCC trunk, in no released GCC.On the read-only attribute
@xokdvium raised this, and it is worth being precise about. The Unix walk already relaxes permissions before recursing,
unix/file-system.cc:201-212:Clearing
FILE_ATTRIBUTE_READONLYis that same step written for Windows, and it is now done through the already-open handle rather than by path, so the object whose attribute changes is necessarily the one about to be deleted.Why the attribute is there at all is a separate question I am not trying to settle here: canonicalisation chmods store contents to 0444, and
chmod()on Windows is::_wchmod, which turns a missing write bit into exactly that attribute.posix-fs-canonicalise.cc:27sits above the#ifndef _WIN32on line 31, so it runs on Windows. Whether it should is fair to ask, but a store path that is read-only on disk has to be removable either way.bytesFreed
Was set to zero and discarded, so garbage collection on Windows reported freeing nothing however much it deleted — while
gc.ccsays "Rely ondeletePath()accounting". Now reported, with the policy copied from Unix rather than invented: count a non-directory's size at one or two links, nothing at three or more.Not using POSIX-semantics deletion, deliberately
FileDispositionInfoExwithFILE_DISPOSITION_FLAG_POSIX_SEMANTICSwould unlink the name immediately rather than on last-handle-close. It needs a newer API level than the_WIN32_WINNT=0x0602set innix-meson-build-support/windows-version, whose comment says "We currently don't use any API which requires higher than this", so I did not reach for it. The difference does not matter here: each handle closes before its parent is deleted, so a child's name is already gone by then.I found this the hard way — it compiled standalone and failed the real cross build, because my standalone check had not set that define.
Verification
Cross-built for
x86_64-w64-mingw32and exercised under Wine against the realnix::deletePath, not a reimplementation:bytesFreed = 30for 3×10 bytesRows two and four both fail on
master.Stated plainly: these measurements are under Wine, not on Windows hardware. The
NtCreateFile,GetFileInformationByHandleExandSetFileInformationByHandlebehaviour relied on is documented rather than emulator-specific, but I have not run it on Windows.