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 // A new target keeps the umask default fopen would give it; an existing one gets its mode
71 // copied at commit, so the temp stays private until then
72 const mode_t createMode = wxFileName::FileExists( aTargetPath ) ? 0600 : 0666;
73
74 // Exclusive-create closes the TOCTOU window: if another process pre-created a file
75 // at the candidate path, O_EXCL fails and we retry with a new counter value.
76 for( unsigned attempt = 0; attempt < 32; ++attempt )
77 {
78 wxString candidate = MakeSiblingTempPath( aTargetPath );
79 int fd = open( candidate.fn_str(), O_WRONLY | O_CREAT | O_EXCL | O_CLOEXEC, createMode );
80
81 if( fd >= 0 )
82 {
83 FILE* fp = fdopen( fd, aMode.mb_str() );
84
85 if( !fp )
86 {
87 int err = errno;
88 close( fd );
89 unlink( candidate.fn_str() );
90
91 if( aError )
92 {
93 *aError = wxString::Format( wxT( "fdopen failed for temp file '%s': %s" ),
94 candidate, wxString::FromUTF8( strerror( err ) ) );
95 }
96
97 return nullptr;
98 }
99
100 if( aTempPathOut )
101 *aTempPathOut = candidate;
102
103 return fp;
104 }
105
106 if( errno != EEXIST )
107 {
108 if( aError )
109 {
110 *aError = wxString::Format( wxT( "Cannot create temp file '%s': %s" ), candidate,
111 wxString::FromUTF8( strerror( errno ) ) );
112 }
113
114 return nullptr;
115 }
116 }
117
118 if( aError )
119 *aError = wxT( "Exhausted temp-file retry budget" );
120
121 return nullptr;
122}
123
124
125wxString KIPLATFORM::IO::ResolveSymlinkTarget( const wxString& aPath )
126{
127 // Users commonly symlink shared config/project files into their working directory.
128 // Atomic rename would replace the symlink with a regular file; resolve so the save
129 // lands on the referent instead.
130 struct stat st;
131
132 if( lstat( aPath.fn_str(), &st ) != 0 || !S_ISLNK( st.st_mode ) )
133 return aPath;
134
135 char resolved[PATH_MAX];
136
137 if( realpath( aPath.fn_str(), resolved ) )
138 return wxString::FromUTF8( resolved );
139
140 return aPath;
141}
142
143
144bool KIPLATFORM::IO::FlushDirectory( const wxString& aDirPath )
145{
146 int fd = open( aDirPath.fn_str(), O_RDONLY
147#if defined( O_DIRECTORY )
148 | O_DIRECTORY
149#endif
150 );
151
152 if( fd < 0 )
153 {
154 // NFS and some FUSE mounts reject O_DIRECTORY or reject fsync on directories;
155 // treat those as non-fatal since the file's own fsync is what matters most.
156 return errno == EINVAL || errno == ENOTSUP;
157 }
158
159 int rc = fsync( fd );
160 int err = errno;
161 close( fd );
162
163 return rc == 0 || err == EINVAL;
164}
165
166
167bool KIPLATFORM::IO::AtomicRename( const wxString& aSrc, const wxString& aDst, wxString* aError )
168{
169 if( rename( aSrc.fn_str(), aDst.fn_str() ) == 0 )
170 return true;
171
172 if( aError )
173 *aError = wxString::FromUTF8( strerror( errno ) );
174
175 return false;
176}
177
178#endif // !_WIN32
179
180
181bool KIPLATFORM::IO::CommitTempFile( const wxString& aTempPath, const wxString& aTargetPath,
182 wxString* aError )
183{
184 TARGET_ATTRS snapshot;
185 const bool targetExists = wxFileName::FileExists( aTargetPath );
186
187 if( targetExists )
188 {
189 // Snapshot first so we can re-apply after rename. DuplicatePermissions must
190 // read target's original mode (POSIX) before any mutation, so it follows the
191 // snapshot and precedes MakeWriteable.
192 snapshot = CaptureTargetAttributes( aTargetPath );
193
194 if( !DuplicatePermissions( aTargetPath, aTempPath ) )
195 {
196 // Failing here means the new file would land with creation-default
197 // permissions instead of the target's. Bail out while the rename hasn't
198 // happened yet so the user's original file is still untouched.
199 if( aError )
200 {
201 *aError = wxString::Format( wxT( "Cannot copy permissions from '%s' to '%s'" ),
202 aTargetPath, aTempPath );
203 }
204
205 return false;
206 }
207
208#if defined( _WIN32 )
209 // Cloud-sync mounts (OneDrive/Drive/Dropbox) can reject MoveFileEx when the
210 // target has FILE_ATTRIBUTE_HIDDEN. Clear blocking bits here; the snapshot
211 // restores them below. POSIX rename() requires write on the containing
212 // directory only, not on the target file, so MakeWriteable is unnecessary.
213 MakeWriteable( aTargetPath );
214#endif
215 }
216
217 wxString renameError;
218 const bool renamed = AtomicRename( aTempPath, aTargetPath, &renameError );
219
220 if( targetExists )
221 {
222 // Unconditional re-apply. On rename failure this rolls back the MakeWriteable
223 // mutation on the original target. On rename success it restores HIDDEN/
224 // READONLY bits to the new file, since SetFileSecurity (used by
225 // DuplicatePermissions) copies ACLs but not those attribute bits. A failure
226 // here is logged but not fatal: the rename has already committed (or was
227 // going to be reported as failed below), so the user's data is consistent;
228 // only the attribute bits are off.
229 if( !ApplyTargetAttributes( aTargetPath, snapshot ) )
230 {
231 wxLogWarning( wxT( "Could not restore file attributes on '%s' after save" ),
232 aTargetPath );
233 }
234 }
235
236 if( !renamed )
237 {
238 if( aError )
239 {
240 *aError = wxString::Format( wxT( "Cannot rename temp file over '%s': %s" ),
241 aTargetPath, renameError );
242 }
243
244 return false;
245 }
246
247 wxFileName dst( aTargetPath );
248 wxString dirPath = dst.GetPath();
249
250 // A bare filename has no directory component; fall back to CWD so the dir fsync
251 // lands on the filesystem that actually holds the file.
252 if( dirPath.IsEmpty() )
253 dirPath = wxT( "." );
254
255 if( !FlushDirectory( dirPath ) )
256 {
257 // The rename has already committed, but without a dir fsync it may not survive
258 // power loss. Report so callers can warn the user; the file itself is present.
259 if( aError )
260 *aError = wxString::Format( wxT( "Cannot flush directory '%s' to disk" ), dirPath );
261
262 return false;
263 }
264
265 return true;
266}
267
268
269bool KIPLATFORM::IO::AtomicWriteFile( const wxString& aTargetPath, const void* aData, size_t aSize,
270 wxString* aError )
271{
272 wxString target = ResolveSymlinkTarget( aTargetPath );
273 wxString tempPath;
274 FILE* fp = OpenUniqueSiblingTempFile( target, wxT( "wb" ), &tempPath, aError );
275
276 if( !fp )
277 return false;
278
279 if( aSize > 0 && std::fwrite( aData, 1, aSize, fp ) != aSize )
280 {
281 if( aError )
282 *aError = wxString::Format( wxT( "Write failed to '%s'" ), tempPath );
283
284 std::fclose( fp );
285 wxRemoveFile( tempPath );
286 return false;
287 }
288
289 if( !FlushToDisk( fp ) )
290 {
291 if( aError )
292 *aError = wxString::Format( wxT( "fsync failed on '%s'" ), tempPath );
293
294 std::fclose( fp );
295 wxRemoveFile( tempPath );
296 return false;
297 }
298
299 // Buffered writes on NFS and quota'd volumes can surface their errors at close, not
300 // at write time, so an unchecked close could rename a short file into place.
301 if( std::fclose( fp ) != 0 )
302 {
303 int err = errno;
304
305 if( aError )
306 {
307 *aError = wxString::Format( wxT( "Cannot close temp file '%s': %s" ), tempPath,
308 wxString::FromUTF8( strerror( err ) ) );
309 }
310
311 wxRemoveFile( tempPath );
312 return false;
313 }
314
315 if( !CommitTempFile( tempPath, target, aError ) )
316 {
317 // CommitTempFile can fail after a successful rename (e.g. dir fsync error),
318 // in which case tempPath no longer exists. Suppress the expected log noise.
319 wxLogNull logNoise;
320 wxRemoveFile( tempPath );
321 return false;
322 }
323
324 return true;
325}
326
327
328
329
330
331void KIPLATFORM::IO::MAPPED_FILE::readIntoBuffer( const wxString& aFileName )
332{
333 FILE* fp = wxFopen( aFileName, wxS( "rb" ) );
334
335 if( !fp )
336 throw std::runtime_error( std::string( "Cannot open file: " ) + aFileName.ToStdString() );
337
338 fseek( fp, 0, SEEK_END );
339 long len = ftell( fp );
340
341 if( len < 0 )
342 {
343 fclose( fp );
344 throw std::runtime_error( std::string( "Cannot determine file size: " )
345 + aFileName.ToStdString() );
346 }
347
348 m_fallbackBuffer.resize( static_cast<size_t>( len ) );
349 fseek( fp, 0, SEEK_SET );
350
351 size_t bytesRead = fread( m_fallbackBuffer.data(), 1, static_cast<size_t>( len ), fp );
352 fclose( fp );
353
354 if( bytesRead != static_cast<size_t>( len ) )
355 {
356 throw std::runtime_error( std::string( "Failed to read file: " )
357 + aFileName.ToStdString() );
358 }
359
360 m_data = m_fallbackBuffer.data();
361 m_size = m_fallbackBuffer.size();
362}
363
364
365#if !defined( _WIN32 )
366
367
369 bool& aCreated )
370{
371 Release();
372
373 int fd = open( aPath.fn_str(), O_RDWR | O_CREAT | O_EXCL | O_CLOEXEC, 0666 );
374
375 aCreated = fd >= 0;
376
377 if( !aCreated )
378 fd = open( aPath.fn_str(), O_RDWR | O_CLOEXEC );
379
380 if( fd < 0 )
381 {
382 // Fall back to read-only so we can still report the lock owner
383 m_fd = open( aPath.fn_str(), O_RDONLY | O_CLOEXEC );
384
385 if( m_fd >= 0 )
387
388 return m_state;
389 }
390
391 m_fd = fd;
392
393 if( flock( fd, LOCK_EX | LOCK_NB ) == 0 )
395 else if( errno == EWOULDBLOCK )
397 else
399
400 return m_state;
401}
402
403
404bool KIPLATFORM::IO::FILE_LOCK::OpenForInspect( const wxString& aPath, bool& aHeldByAnother )
405{
406 Release();
407
408 aHeldByAnother = false;
409
410 m_fd = open( aPath.fn_str(), O_RDONLY | O_CLOEXEC );
411
412 if( m_fd < 0 )
413 return false;
414
415 // Briefly take the lock to test for a holder, then release; m_state stays NONE
416 if( flock( m_fd, LOCK_EX | LOCK_NB ) == 0 )
417 flock( m_fd, LOCK_UN );
418 else if( errno == EWOULDBLOCK )
419 aHeldByAnother = true;
420
421 return true;
422}
423
424
426{
427 return m_fd >= 0;
428}
429
430
431bool KIPLATFORM::IO::FILE_LOCK::ReadAll( std::string& aContents ) const
432{
433 if( !IsOpen() || lseek( m_fd, 0, SEEK_SET ) < 0 )
434 return false;
435
436 aContents.clear();
437
438 char buffer[4096];
439 ssize_t bytes;
440
441 while( ( bytes = read( m_fd, buffer, sizeof( buffer ) ) ) > 0 )
442 aContents.append( buffer, static_cast<size_t>( bytes ) );
443
444 return bytes >= 0;
445}
446
447
448bool KIPLATFORM::IO::FILE_LOCK::Rewrite( const std::string& aContents )
449{
450 if( !IsOpen() || ftruncate( m_fd, 0 ) < 0 || lseek( m_fd, 0, SEEK_SET ) < 0 )
451 return false;
452
453 size_t written = 0;
454
455 while( written < aContents.size() )
456 {
457 ssize_t bytes = write( m_fd, aContents.data() + written, aContents.size() - written );
458
459 if( bytes <= 0 )
460 return false;
461
462 written += static_cast<size_t>( bytes );
463 }
464
465 return true;
466}
467
468
470{
471 if( IsOpen() )
472 {
473 // Closing the descriptor releases the lock, same as process death would
474 close( m_fd );
475 m_fd = -1;
476 }
477
479}
480
481#endif // !_WIN32
482
483
484
489
490
492{
493 *this = std::move( aOther );
494}
495
496
498{
499 if( this == &aOther )
500 return *this;
501
502 Release();
503
504#ifdef _WIN32
505 m_handle = aOther.m_handle;
506 aOther.m_handle = nullptr;
507#else
508 m_fd = aOther.m_fd;
509 aOther.m_fd = -1;
510#endif
511
512 m_state = aOther.m_state;
513 aOther.m_state = STATE::NONE;
514
515 return *this;
516}
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, returns the canonical path of its referent so atomic-save operations replace t...
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:302