KiCad PCB EDA Suite
Loading...
Searching...
No Matches
common/io.cpp
Go to the documentation of this file.
1/*
2* This program source code file is part of KiCad, a free EDA CAD application.
3*
4* Copyright The KiCad Developers, see AUTHORS.txt for contributors.
5*
6* This program is free software: you can redistribute it and/or modify it
7* under the terms of the GNU General Public License as published by the
8* Free Software Foundation, either version 3 of the License, or (at your
9* option) any later version.
10*
11* This program is distributed in the hope that it will be useful, but
12* WITHOUT ANY WARRANTY; without even the implied warranty of
13* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14* General Public License for more details.
15*
16* You should have received a copy of the GNU General Public License
17* along with this program. If not, see <https://www.gnu.org/licenses/>.
18*/
19
20#include <kiplatform/io.h>
21
22#include <wx/crt.h>
23#include <wx/filename.h>
24#include <wx/log.h>
25#include <wx/string.h>
26
27#include <atomic>
28#include <cstdio>
29#include <cstring>
30#include <cerrno>
31#include <stdexcept>
32#include <string>
33
34#if defined( _WIN32 )
35#include <process.h>
36#else
37#include <climits>
38#include <cstdlib>
39#include <fcntl.h>
40#include <sys/file.h>
41#include <sys/stat.h>
42#include <unistd.h>
43#endif
44
45
46wxString KIPLATFORM::IO::MakeSiblingTempPath( const wxString& aTargetPath )
47{
48 // Keeping the temp file on the same filesystem as the final target is required:
49 // rename() across filesystems is not atomic, and MoveFileEx on Windows will fail or
50 // fall back to copy+delete across volumes.
51 static std::atomic<unsigned> s_counter{ 0 };
52
53 unsigned counter = s_counter.fetch_add( 1, std::memory_order_relaxed );
54
55#if defined( _WIN32 )
56 unsigned pid = static_cast<unsigned>( _getpid() );
57#else
58 unsigned pid = static_cast<unsigned>( getpid() );
59#endif
60
61 return aTargetPath + wxString::Format( wxT( ".kicad-save-%u-%u" ), pid, counter );
62}
63
64
65#if !defined( _WIN32 )
66
67FILE* KIPLATFORM::IO::OpenUniqueSiblingTempFile( const wxString& aTargetPath, const wxString& aMode,
68 wxString* aTempPathOut, wxString* aError )
69{
70 // Exclusive-create closes the TOCTOU window: if another process pre-created a file
71 // at the candidate path, O_EXCL fails and we retry with a new counter value.
72 for( unsigned attempt = 0; attempt < 32; ++attempt )
73 {
74 wxString candidate = MakeSiblingTempPath( aTargetPath );
75 int fd = open( candidate.fn_str(), O_WRONLY | O_CREAT | O_EXCL | O_CLOEXEC, 0600 );
76
77 if( fd >= 0 )
78 {
79 FILE* fp = fdopen( fd, aMode.mb_str() );
80
81 if( !fp )
82 {
83 int err = errno;
84 close( fd );
85 unlink( candidate.fn_str() );
86
87 if( aError )
88 {
89 *aError = wxString::Format( wxT( "fdopen failed for temp file '%s': %s" ),
90 candidate, wxString::FromUTF8( strerror( err ) ) );
91 }
92
93 return nullptr;
94 }
95
96 if( aTempPathOut )
97 *aTempPathOut = candidate;
98
99 return fp;
100 }
101
102 if( errno != EEXIST )
103 {
104 if( aError )
105 {
106 *aError = wxString::Format( wxT( "Cannot create temp file '%s': %s" ), candidate,
107 wxString::FromUTF8( strerror( errno ) ) );
108 }
109
110 return nullptr;
111 }
112 }
113
114 if( aError )
115 *aError = wxT( "Exhausted temp-file retry budget" );
116
117 return nullptr;
118}
119
120
121wxString KIPLATFORM::IO::ResolveSymlinkTarget( const wxString& aPath )
122{
123 // Users commonly symlink shared config/project files into their working directory.
124 // Atomic rename would replace the symlink with a regular file; resolve so the save
125 // lands on the referent instead.
126 struct stat st;
127
128 if( lstat( aPath.fn_str(), &st ) != 0 || !S_ISLNK( st.st_mode ) )
129 return aPath;
130
131 char resolved[PATH_MAX];
132
133 if( realpath( aPath.fn_str(), resolved ) )
134 return wxString::FromUTF8( resolved );
135
136 return aPath;
137}
138
139
140bool KIPLATFORM::IO::FlushDirectory( const wxString& aDirPath )
141{
142 int fd = open( aDirPath.fn_str(), O_RDONLY
143#if defined( O_DIRECTORY )
144 | O_DIRECTORY
145#endif
146 );
147
148 if( fd < 0 )
149 {
150 // NFS and some FUSE mounts reject O_DIRECTORY or reject fsync on directories;
151 // treat those as non-fatal since the file's own fsync is what matters most.
152 return errno == EINVAL || errno == ENOTSUP;
153 }
154
155 int rc = fsync( fd );
156 int err = errno;
157 close( fd );
158
159 return rc == 0 || err == EINVAL;
160}
161
162
163bool KIPLATFORM::IO::AtomicRename( const wxString& aSrc, const wxString& aDst, wxString* aError )
164{
165 if( rename( aSrc.fn_str(), aDst.fn_str() ) == 0 )
166 return true;
167
168 if( aError )
169 *aError = wxString::FromUTF8( strerror( errno ) );
170
171 return false;
172}
173
174#endif // !_WIN32
175
176
177bool KIPLATFORM::IO::CommitTempFile( const wxString& aTempPath, const wxString& aTargetPath,
178 wxString* aError )
179{
180 TARGET_ATTRS snapshot;
181 const bool targetExists = wxFileName::FileExists( aTargetPath );
182
183 if( targetExists )
184 {
185 // Snapshot first so we can re-apply after rename. DuplicatePermissions must
186 // read target's original mode (POSIX) before any mutation, so it follows the
187 // snapshot and precedes MakeWriteable.
188 snapshot = CaptureTargetAttributes( aTargetPath );
189
190 if( !DuplicatePermissions( aTargetPath, aTempPath ) )
191 {
192 // Failing here means the new file would land with creation-default
193 // permissions instead of the target's. Bail out while the rename hasn't
194 // happened yet so the user's original file is still untouched.
195 if( aError )
196 {
197 *aError = wxString::Format( wxT( "Cannot copy permissions from '%s' to '%s'" ),
198 aTargetPath, aTempPath );
199 }
200
201 return false;
202 }
203
204#if defined( _WIN32 )
205 // Cloud-sync mounts (OneDrive/Drive/Dropbox) can reject MoveFileEx when the
206 // target has FILE_ATTRIBUTE_HIDDEN. Clear blocking bits here; the snapshot
207 // restores them below. POSIX rename() requires write on the containing
208 // directory only, not on the target file, so MakeWriteable is unnecessary.
209 MakeWriteable( aTargetPath );
210#endif
211 }
212
213 wxString renameError;
214 const bool renamed = AtomicRename( aTempPath, aTargetPath, &renameError );
215
216 if( targetExists )
217 {
218 // Unconditional re-apply. On rename failure this rolls back the MakeWriteable
219 // mutation on the original target. On rename success it restores HIDDEN/
220 // READONLY bits to the new file, since SetFileSecurity (used by
221 // DuplicatePermissions) copies ACLs but not those attribute bits. A failure
222 // here is logged but not fatal: the rename has already committed (or was
223 // going to be reported as failed below), so the user's data is consistent;
224 // only the attribute bits are off.
225 if( !ApplyTargetAttributes( aTargetPath, snapshot ) )
226 {
227 wxLogWarning( wxT( "Could not restore file attributes on '%s' after save" ),
228 aTargetPath );
229 }
230 }
231
232 if( !renamed )
233 {
234 if( aError )
235 {
236 *aError = wxString::Format( wxT( "Cannot rename temp file over '%s': %s" ),
237 aTargetPath, renameError );
238 }
239
240 return false;
241 }
242
243 wxFileName dst( aTargetPath );
244 wxString dirPath = dst.GetPath();
245
246 // A bare filename has no directory component; fall back to CWD so the dir fsync
247 // lands on the filesystem that actually holds the file.
248 if( dirPath.IsEmpty() )
249 dirPath = wxT( "." );
250
251 if( !FlushDirectory( dirPath ) )
252 {
253 // The rename has already committed, but without a dir fsync it may not survive
254 // power loss. Report so callers can warn the user; the file itself is present.
255 if( aError )
256 *aError = wxString::Format( wxT( "Cannot flush directory '%s' to disk" ), dirPath );
257
258 return false;
259 }
260
261 return true;
262}
263
264
265bool KIPLATFORM::IO::AtomicWriteFile( const wxString& aTargetPath, const void* aData, size_t aSize,
266 wxString* aError )
267{
268 wxString target = ResolveSymlinkTarget( aTargetPath );
269 wxString tempPath;
270 FILE* fp = OpenUniqueSiblingTempFile( target, wxT( "wb" ), &tempPath, aError );
271
272 if( !fp )
273 return false;
274
275 if( aSize > 0 && std::fwrite( aData, 1, aSize, fp ) != aSize )
276 {
277 if( aError )
278 *aError = wxString::Format( wxT( "Write failed to '%s'" ), tempPath );
279
280 std::fclose( fp );
281 wxRemoveFile( tempPath );
282 return false;
283 }
284
285 if( !FlushToDisk( fp ) )
286 {
287 if( aError )
288 *aError = wxString::Format( wxT( "fsync failed on '%s'" ), tempPath );
289
290 std::fclose( fp );
291 wxRemoveFile( tempPath );
292 return false;
293 }
294
295 // Buffered writes on NFS and quota'd volumes can surface their errors at close, not
296 // at write time, so an unchecked close could rename a short file into place.
297 if( std::fclose( fp ) != 0 )
298 {
299 int err = errno;
300
301 if( aError )
302 {
303 *aError = wxString::Format( wxT( "Cannot close temp file '%s': %s" ), tempPath,
304 wxString::FromUTF8( strerror( err ) ) );
305 }
306
307 wxRemoveFile( tempPath );
308 return false;
309 }
310
311 if( !CommitTempFile( tempPath, target, aError ) )
312 {
313 // CommitTempFile can fail after a successful rename (e.g. dir fsync error),
314 // in which case tempPath no longer exists. Suppress the expected log noise.
315 wxLogNull logNoise;
316 wxRemoveFile( tempPath );
317 return false;
318 }
319
320 return true;
321}
322
323
324
325
326
327void KIPLATFORM::IO::MAPPED_FILE::readIntoBuffer( const wxString& aFileName )
328{
329 FILE* fp = wxFopen( aFileName, wxS( "rb" ) );
330
331 if( !fp )
332 throw std::runtime_error( std::string( "Cannot open file: " ) + aFileName.ToStdString() );
333
334 fseek( fp, 0, SEEK_END );
335 long len = ftell( fp );
336
337 if( len < 0 )
338 {
339 fclose( fp );
340 throw std::runtime_error( std::string( "Cannot determine file size: " )
341 + aFileName.ToStdString() );
342 }
343
344 m_fallbackBuffer.resize( static_cast<size_t>( len ) );
345 fseek( fp, 0, SEEK_SET );
346
347 size_t bytesRead = fread( m_fallbackBuffer.data(), 1, static_cast<size_t>( len ), fp );
348 fclose( fp );
349
350 if( bytesRead != static_cast<size_t>( len ) )
351 {
352 throw std::runtime_error( std::string( "Failed to read file: " )
353 + aFileName.ToStdString() );
354 }
355
356 m_data = m_fallbackBuffer.data();
357 m_size = m_fallbackBuffer.size();
358}
359
360
361#if !defined( _WIN32 )
362
363
365 bool& aCreated )
366{
367 Release();
368
369 int fd = open( aPath.fn_str(), O_RDWR | O_CREAT | O_EXCL | O_CLOEXEC, 0666 );
370
371 aCreated = fd >= 0;
372
373 if( !aCreated )
374 fd = open( aPath.fn_str(), O_RDWR | O_CLOEXEC );
375
376 if( fd < 0 )
377 {
378 // Fall back to read-only so we can still report the lock owner
379 m_fd = open( aPath.fn_str(), O_RDONLY | O_CLOEXEC );
380
381 if( m_fd >= 0 )
383
384 return m_state;
385 }
386
387 m_fd = fd;
388
389 if( flock( fd, LOCK_EX | LOCK_NB ) == 0 )
391 else if( errno == EWOULDBLOCK )
393 else
395
396 return m_state;
397}
398
399
400bool KIPLATFORM::IO::FILE_LOCK::OpenForInspect( const wxString& aPath, bool& aHeldByAnother )
401{
402 Release();
403
404 aHeldByAnother = false;
405
406 m_fd = open( aPath.fn_str(), O_RDONLY | O_CLOEXEC );
407
408 if( m_fd < 0 )
409 return false;
410
411 // Briefly take the lock to test for a holder, then release; m_state stays NONE
412 if( flock( m_fd, LOCK_EX | LOCK_NB ) == 0 )
413 flock( m_fd, LOCK_UN );
414 else if( errno == EWOULDBLOCK )
415 aHeldByAnother = true;
416
417 return true;
418}
419
420
422{
423 return m_fd >= 0;
424}
425
426
427bool KIPLATFORM::IO::FILE_LOCK::ReadAll( std::string& aContents ) const
428{
429 if( !IsOpen() || lseek( m_fd, 0, SEEK_SET ) < 0 )
430 return false;
431
432 aContents.clear();
433
434 char buffer[4096];
435 ssize_t bytes;
436
437 while( ( bytes = read( m_fd, buffer, sizeof( buffer ) ) ) > 0 )
438 aContents.append( buffer, static_cast<size_t>( bytes ) );
439
440 return bytes >= 0;
441}
442
443
444bool KIPLATFORM::IO::FILE_LOCK::Rewrite( const std::string& aContents )
445{
446 if( !IsOpen() || ftruncate( m_fd, 0 ) < 0 || lseek( m_fd, 0, SEEK_SET ) < 0 )
447 return false;
448
449 size_t written = 0;
450
451 while( written < aContents.size() )
452 {
453 ssize_t bytes = write( m_fd, aContents.data() + written, aContents.size() - written );
454
455 if( bytes <= 0 )
456 return false;
457
458 written += static_cast<size_t>( bytes );
459 }
460
461 return true;
462}
463
464
466{
467 if( IsOpen() )
468 {
469 // Closing the descriptor releases the lock, same as process death would
470 close( m_fd );
471 m_fd = -1;
472 }
473
475}
476
477#endif // !_WIN32
478
479
480
485
486
488{
489 *this = std::move( aOther );
490}
491
492
494{
495 if( this == &aOther )
496 return *this;
497
498 Release();
499
500#ifdef _WIN32
501 m_handle = aOther.m_handle;
502 aOther.m_handle = nullptr;
503#else
504 m_fd = aOther.m_fd;
505 aOther.m_fd = -1;
506#endif
507
508 m_state = aOther.m_state;
509 aOther.m_state = STATE::NONE;
510
511 return *this;
512}
An exclusive advisory lock on a file, held for the lifetime of this object.
Definition io.h:91
bool ReadAll(std::string &aContents) const
Read the whole file through the descriptor we hold.
void Release()
Release the lock and close the file.
bool Rewrite(const std::string &aContents)
Replace the file contents through the descriptor we hold, keeping the same inode.
@ UNSUPPORTED
The file is open but the filesystem cannot answer.
Definition io.h:98
@ BUSY
Another process holds the lock.
Definition io.h:97
@ HELD
We hold the lock.
Definition io.h:96
@ NONE
No file is open.
Definition io.h:95
STATE Acquire(const wxString &aPath, bool &aCreated)
Open aPath, creating it if it does not exist, and try to take the lock without ever blocking on it.
FILE_LOCK & operator=(FILE_LOCK &&aOther) noexcept
bool OpenForInspect(const wxString &aPath, bool &aHeldByAnother)
Open an existing file and report whether another process holds its lock, creating nothing and keeping...
void readIntoBuffer(const wxString &aFileName)
const uint8_t * m_data
Definition io.h:57
std::vector< uint8_t > m_fallbackBuffer
Definition io.h:67
wxString MakeSiblingTempPath(const wxString &aTargetPath)
Returns a unique sibling path of aTargetPath suitable as an atomic-save temp file.
Definition common/io.cpp:46
bool FlushDirectory(const wxString &aDirPath)
Forces a directory entry's metadata to stable storage.
TARGET_ATTRS CaptureTargetAttributes(const wxString &aPath)
Captures attributes of an existing aPath that must survive an atomic rename.
Definition unix/io.cpp:94
bool DuplicatePermissions(const wxString &aSrc, const wxString &aDest)
Duplicates the file security data from one file to another ensuring that they are the same between bo...
Definition unix/io.cpp:55
wxString ResolveSymlinkTarget(const wxString &aPath)
If aPath is a symlink on POSIX, returns the canonical path of its referent so atomic-save operations ...
bool AtomicRename(const wxString &aSrc, const wxString &aDst, wxString *aError=nullptr)
Atomically replaces aDst with aSrc.
FILE * OpenUniqueSiblingTempFile(const wxString &aTargetPath, const wxString &aMode, wxString *aTempPathOut, wxString *aError=nullptr)
Opens a fresh sibling temp file next to aTargetPath with exclusive-create semantics (POSIX O_CREAT|O_...
Definition common/io.cpp:67
bool CommitTempFile(const wxString &aTempPath, const wxString &aTargetPath, wxString *aError=nullptr)
Completes an atomic save.
bool AtomicWriteFile(const wxString &aTargetPath, const void *aData, size_t aSize, wxString *aError=nullptr)
Writes aData to aTargetPath via a sibling temp file, fsyncs the data and directory,...
bool MakeWriteable(const wxString &aFilePath)
Ensures that a file has write permissions.
Definition unix/io.cpp:78
bool ApplyTargetAttributes(const wxString &aPath, const TARGET_ATTRS &aAttrs)
Re-applies attributes previously captured by CaptureTargetAttributes.
Definition unix/io.cpp:103
bool FlushToDisk(FILE *aFp)
Flushes user-space buffers for aFp and forces the kernel/filesystem to commit the file's data blocks ...
Definition unix/io.cpp:182
Opaque snapshot of filesystem attributes that MakeWriteable may alter and that the atomic rename sequ...
Definition io.h:301