Stroika Library 3.0d23
 
Loading...
Searching...
No Matches
ThroughTmpFileWriter.cpp
1/*
2 * Copyright(c) Sophist Solutions, Inc. 1990-2026. All rights reserved
3 */
4#include "Stroika/Foundation/StroikaPreComp.h"
5
6#include <cstdio>
7#include <fstream>
8#include <random>
9
10#if qStroika_Foundation_Common_Platform_Windows
11#include <windows.h>
12#elif qStroika_Foundation_Common_Platform_POSIX
13#include <unistd.h>
14#endif
15
18#include "Stroika/Foundation/Containers/Common.h"
19#include "Stroika/Foundation/Execution/Activity.h"
20#include "Stroika/Foundation/Execution/Exceptions.h"
22#include "Stroika/Foundation/Execution/Throw.h"
23#if qStroika_Foundation_Common_Platform_Windows
24#include "Stroika/Foundation/Execution/Platform/Windows/Exception.h"
25#endif
28
30
31using namespace Stroika::Foundation;
34using namespace Stroika::Foundation::Execution;
35using namespace Stroika::Foundation::IO;
37
38#if qStroika_Foundation_Common_Platform_Windows
40#endif
41
42namespace {
43 // @todo - redo using open (O_CREAT, but as portably as possible - at least do windows/POSIX impl, and maybe fallback on fstream approach)
44 bool tryCreateFile_ (const filesystem::path& p)
45 {
46 error_code ec;
47 // Check if file already exists
48 if (filesystem::exists (p, ec)) {
49 if (!ec) {
50 return false; // File already exists
51 }
52 else {
53 DbgTrace ("Error checking file existence: {}"_f, String::FromNarrowSDKString (ec.message ()));
54 return false;
55 }
56 }
57
58 // Try creating the file - NOTE THIS IS STILL A RACE - TWO PROCESSES COULD DO SAME THING AT ONCE!
59 ofstream ofs{p};
60 if (ofs.is_open ()) {
61 ofs.close ();
62 return true; // File successfully created
63 }
64 else {
65 return false; // Creation failed (e.g., permission denied, invalid path)
66 }
67 }
68}
69
70/*
71 ********************************************************************************
72 ************************ FileSystem::ThroughTmpFileWriter **********************
73 ********************************************************************************
74 */
75ThroughTmpFileWriter::ThroughTmpFileWriter (const filesystem::path& realFileName, const String& tmpSuffix)
76 : fRealFilePath_{realFileName}
77{
78 Require (not realFileName.empty ());
79 Require (not tmpSuffix.empty ());
80 // keep generating random names, and trying to create til we succeed
81 filesystem::path useTmpPath = realFileName;
82 useTmpPath.replace_extension ();
83 String baseStem{useTmpPath.stem ()};
84 filesystem::path newExtension = tmpSuffix.As<filesystem::path> ();
85 create_directories (useTmpPath.parent_path ());
86 default_random_engine gen{random_device{}()}; //Standard mersenne_twister_engine seeded with rd()
87 uniform_int_distribution<int> distribution{1, 99999};
88 while (true) {
89 filesystem::path newFN = "{}-{}"_f(baseStem, distribution (gen)).As<filesystem::path> ();
90 useTmpPath.replace_filename (newFN);
91 useTmpPath.replace_extension (newExtension);
92 if (tryCreateFile_ (useTmpPath)) {
93 fTmpFilePath_ = useTmpPath;
94 return;
95 }
96 DbgTrace ("randomfile name conflict, so trying again (should be rare): f={}"_f, useTmpPath);
98 }
99}
100
101ThroughTmpFileWriter::~ThroughTmpFileWriter ()
102{
103 if (not fTmpFilePath_.empty ()) {
104 DbgTrace ("ThroughTmpFileWriter::DTOR - tmpfile not successfully commited to {}"_f, fRealFilePath_);
105 // ignore errors on unlink, cuz nothing to be done in DTOR anyhow...(@todo perhaps should at least tracelog)
106#if qStroika_Foundation_Common_Platform_POSIX
107 (void)::unlink (fTmpFilePath_.c_str ());
108#elif qStroika_Foundation_Common_Platform_Windows
109 // if antivirus scanning prevents a delete, use this to eventually delete the file
110 if (::DeleteFileW (fTmpFilePath_.c_str ()) == 0) {
111 (void)::MoveFileExW (fTmpFilePath_.c_str (), nullptr, MOVEFILE_DELAY_UNTIL_REBOOT);
112 }
113#else
115#endif
116 }
117}
118
120{
121 Require (not fTmpFilePath_.empty ()); // cannot Commit more than once
122 // Also - NOTE - you MUST close fTmpFilePath (any file descriptors that have opened it) BEFORE the Commit!
123
124 auto activity = LazyEvalActivity ([&] () -> String { return "committing temporary file '{}' to '{}'"_f(fTmpFilePath_, fRealFilePath_); });
125 DeclareActivity currentActivity{&activity};
126#if qStroika_Foundation_Common_Platform_POSIX
127 FileSystem::Exception::ThrowPOSIXErrNoIfNegative (::rename (fTmpFilePath_.c_str (), fRealFilePath_.c_str ()), fTmpFilePath_, fRealFilePath_);
128#elif qStroika_Foundation_Common_Platform_Windows
129 try {
130 ThrowIfZeroGetLastError (::MoveFileExW (fTmpFilePath_.c_str (), fRealFilePath_.c_str (), MOVEFILE_REPLACE_EXISTING | MOVEFILE_WRITE_THROUGH));
131 }
132 catch (const system_error& we) {
133 // On Win9x - this fails cuz OS not impl...
134 if (we.code () == error_code{ERROR_CALL_NOT_IMPLEMENTED, system_category ()}) {
135 ::DeleteFileW (fRealFilePath_.c_str ());
136 ThrowIfZeroGetLastError (::MoveFileW (fTmpFilePath_.c_str (), fRealFilePath_.c_str ()));
137 }
138 // Sadly this happens pretty often on Windoze, due to virus scanners. But when that is the cause, retrying
139 // a little later should do the trick --LGP 2026-02-07
140 else if (we.code () == error_code{ERROR_SHARING_VIOLATION, system_category ()} or
141 we.code () == error_code{ERROR_ACCESS_DENIED, system_category ()}) {
142 auto retryLoop = [&] () {
143 if (fRetryOnSharingViolationFor != kRetryOnSharingViolationFor_Disable) {
144 DbgTrace ("ThroughTmpFileWriter::Commit: {}, so retrying for {}"_f,
145 we.code ().value () == ERROR_SHARING_VIOLATION ? "ERROR_SHARING_VIOLATION"_k : "ERROR_ACCESS_DENIED"_k,
146 fRetryOnSharingViolationFor.value_or (kRetryOnSharingViolationFor_Default));
147 Time::TimePointSeconds until = Time::GetTickCount () + fRetryOnSharingViolationFor.value_or (kRetryOnSharingViolationFor_Default);
148 unsigned int nRetries = 0;
149 do {
150 Execution::Sleep (nRetries * 10ms);
151 if (BOOL r = ::MoveFileExW (fTmpFilePath_.c_str (), fRealFilePath_.c_str (), MOVEFILE_REPLACE_EXISTING | MOVEFILE_WRITE_THROUGH)) {
152 DbgTrace ("retry worked!"_f);
153 return; // return from retryLoop, not ThroughTmpFileWriter::Commit
154 }
155 else {
156 DWORD lastError = ::GetLastError ();
157 Assert (lastError != 0);
158 if (lastError != ERROR_SHARING_VIOLATION) {
159 Execution::ThrowSystemErrNo (lastError);
160 }
161 }
162 nRetries++;
163 } while (until < Time::GetTickCount ());
164 }
165 ReThrow ();
166 };
167 retryLoop ();
168 }
169 else {
170 ReThrow ();
171 }
172 }
173#else
175#endif
176 fTmpFilePath_.clear ();
177}
#define WeakAssertNotReached()
Definition Assertions.h:468
#define AssertNotImplemented()
Definition Assertions.h:402
time_point< RealtimeClock, DurationSeconds > TimePointSeconds
TimePointSeconds is a simpler approach to chrono::time_point, which doesn't require using templates e...
Definition Realtime.h:82
#define DbgTrace
Definition Trace.h:317
String is like std::u32string, except it is much easier to use, often much more space efficient,...
Definition String.h:201
static INT_TYPE ThrowPOSIXErrNoIfNegative(INT_TYPE returnCode, const path &p1={}, const path &p2={})
void Sleep(Time::Duration seconds2Wait)
Definition Sleep.inl:97