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 // Best effort, volumes without ACL or mode support (cloud drives, FAT) reject the copy
195 // and the temp then keeps the permissions it was created with
196 if( !DuplicatePermissions( aTargetPath, aTempPath ) )
197 {
198 wxLogTrace( wxT( "KICAD_ATOMIC_SAVE" ), wxT( "Cannot copy permissions from '%s' to '%s'" ),
199 aTargetPath, aTempPath );
200 }
201
202#if defined( _WIN32 )
203 // Cloud-sync mounts (OneDrive/Drive/Dropbox) can reject MoveFileEx when the
204 // target has FILE_ATTRIBUTE_HIDDEN. Clear blocking bits here; the snapshot
205 // restores them below. POSIX rename() requires write on the containing
206 // directory only, not on the target file, so MakeWriteable is unnecessary.
207 MakeWriteable( aTargetPath );
208#endif
209 }
210
211 wxString renameError;
212 const bool renamed = AtomicRename( aTempPath, aTargetPath, &renameError );
213
214 if( targetExists )
215 {
216 // Unconditional re-apply. On rename failure this rolls back the MakeWriteable
217 // mutation on the original target. On rename success it restores HIDDEN/
218 // READONLY bits to the new file, since SetFileSecurity (used by
219 // DuplicatePermissions) copies ACLs but not those attribute bits. A failure
220 // here is logged but not fatal: the rename has already committed (or was
221 // going to be reported as failed below), so the user's data is consistent;
222 // only the attribute bits are off.
223 if( !ApplyTargetAttributes( aTargetPath, snapshot ) )
224 {
225 wxLogWarning( wxT( "Could not restore file attributes on '%s' after save" ),
226 aTargetPath );
227 }
228 }
229
230 if( !renamed )
231 {
232 if( aError )
233 {
234 *aError = wxString::Format( wxT( "Cannot rename temp file over '%s': %s" ),
235 aTargetPath, renameError );
236 }
237
238 return false;
239 }
240
241 wxFileName dst( aTargetPath );
242 wxString dirPath = dst.GetPath();
243
244 // A bare filename has no directory component; fall back to CWD so the dir fsync
245 // lands on the filesystem that actually holds the file.
246 if( dirPath.IsEmpty() )
247 dirPath = wxT( "." );
248
249 if( !FlushDirectory( dirPath ) )
250 {
251 // The rename has already committed, but without a dir fsync it may not survive
252 // power loss. Report so callers can warn the user; the file itself is present.
253 if( aError )
254 *aError = wxString::Format( wxT( "Cannot flush directory '%s' to disk" ), dirPath );
255
256 return false;
257 }
258
259 return true;
260}
261
262
263bool KIPLATFORM::IO::AtomicWriteFile( const wxString& aTargetPath, const void* aData, size_t aSize,
264 wxString* aError )
265{
266 wxString target = ResolveSymlinkTarget( aTargetPath );
267 wxString tempPath;
268 FILE* fp = OpenUniqueSiblingTempFile( target, wxT( "wb" ), &tempPath, aError );
269
270 if( !fp )
271 return false;
272
273 if( aSize > 0 && std::fwrite( aData, 1, aSize, fp ) != aSize )
274 {
275 if( aError )
276 *aError = wxString::Format( wxT( "Write failed to '%s'" ), tempPath );
277
278 std::fclose( fp );
279 wxRemoveFile( tempPath );
280 return false;
281 }
282
283 if( !FlushToDisk( fp ) )
284 {
285 if( aError )
286 *aError = wxString::Format( wxT( "fsync failed on '%s'" ), tempPath );
287
288 std::fclose( fp );
289 wxRemoveFile( tempPath );
290 return false;
291 }
292
293 // Buffered writes on NFS and quota'd volumes can surface their errors at close, not
294 // at write time, so an unchecked close could rename a short file into place.
295 if( std::fclose( fp ) != 0 )
296 {
297 int err = errno;
298
299 if( aError )
300 {
301 *aError = wxString::Format( wxT( "Cannot close temp file '%s': %s" ), tempPath,
302 wxString::FromUTF8( strerror( err ) ) );
303 }
304
305 wxRemoveFile( tempPath );
306 return false;
307 }
308
309 if( !CommitTempFile( tempPath, target, aError ) )
310 {
311 // CommitTempFile can fail after a successful rename (e.g. dir fsync error),
312 // in which case tempPath no longer exists. Suppress the expected log noise.
313 wxLogNull logNoise;
314 wxRemoveFile( tempPath );
315 return false;
316 }
317
318 return true;
319}
320
321
322
323
324
325void KIPLATFORM::IO::MAPPED_FILE::readIntoBuffer( const wxString& aFileName )
326{
327 FILE* fp = wxFopen( aFileName, wxS( "rb" ) );
328
329 if( !fp )
330 throw std::runtime_error( std::string( "Cannot open file: " ) + aFileName.ToStdString() );
331
332 fseek( fp, 0, SEEK_END );
333 long len = ftell( fp );
334
335 if( len < 0 )
336 {
337 fclose( fp );
338 throw std::runtime_error( std::string( "Cannot determine file size: " )
339 + aFileName.ToStdString() );
340 }
341
342 m_fallbackBuffer.resize( static_cast<size_t>( len ) );
343 fseek( fp, 0, SEEK_SET );
344
345 size_t bytesRead = fread( m_fallbackBuffer.data(), 1, static_cast<size_t>( len ), fp );
346 fclose( fp );
347
348 if( bytesRead != static_cast<size_t>( len ) )
349 {
350 throw std::runtime_error( std::string( "Failed to read file: " )
351 + aFileName.ToStdString() );
352 }
353
354 m_data = m_fallbackBuffer.data();
355 m_size = m_fallbackBuffer.size();
356}
357
358
359#if !defined( _WIN32 )
360
361
363 bool& aCreated )
364{
365 Release();
366
367 int fd = open( aPath.fn_str(), O_RDWR | O_CREAT | O_EXCL | O_CLOEXEC, 0666 );
368
369 aCreated = fd >= 0;
370
371 if( !aCreated )
372 fd = open( aPath.fn_str(), O_RDWR | O_CLOEXEC );
373
374 if( fd < 0 )
375 {
376 // Fall back to read-only so we can still report the lock owner
377 m_fd = open( aPath.fn_str(), O_RDONLY | O_CLOEXEC );
378
379 if( m_fd >= 0 )
381
382 return m_state;
383 }
384
385 m_fd = fd;
386
387 if( flock( fd, LOCK_EX | LOCK_NB ) == 0 )
389 else if( errno == EWOULDBLOCK )
391 else
393
394 return m_state;
395}
396
397
398bool KIPLATFORM::IO::FILE_LOCK::OpenForInspect( const wxString& aPath, bool& aHeldByAnother )
399{
400 Release();
401
402 aHeldByAnother = false;
403
404 m_fd = open( aPath.fn_str(), O_RDONLY | O_CLOEXEC );
405
406 if( m_fd < 0 )
407 return false;
408
409 // Briefly take the lock to test for a holder, then release; m_state stays NONE
410 if( flock( m_fd, LOCK_EX | LOCK_NB ) == 0 )
411 flock( m_fd, LOCK_UN );
412 else if( errno == EWOULDBLOCK )
413 aHeldByAnother = true;
414
415 return true;
416}
417
418
420{
421 return m_fd >= 0;
422}
423
424
425bool KIPLATFORM::IO::FILE_LOCK::ReadAll( std::string& aContents ) const
426{
427 if( !IsOpen() || lseek( m_fd, 0, SEEK_SET ) < 0 )
428 return false;
429
430 aContents.clear();
431
432 char buffer[4096];
433 ssize_t bytes;
434
435 while( ( bytes = read( m_fd, buffer, sizeof( buffer ) ) ) > 0 )
436 aContents.append( buffer, static_cast<size_t>( bytes ) );
437
438 return bytes >= 0;
439}
440
441
442bool KIPLATFORM::IO::FILE_LOCK::Rewrite( const std::string& aContents )
443{
444 if( !IsOpen() || ftruncate( m_fd, 0 ) < 0 || lseek( m_fd, 0, SEEK_SET ) < 0 )
445 return false;
446
447 size_t written = 0;
448
449 while( written < aContents.size() )
450 {
451 ssize_t bytes = write( m_fd, aContents.data() + written, aContents.size() - written );
452
453 if( bytes <= 0 )
454 return false;
455
456 written += static_cast<size_t>( bytes );
457 }
458
459 return true;
460}
461
462
464{
465 if( IsOpen() )
466 {
467 // Closing the descriptor releases the lock, same as process death would
468 close( m_fd );
469 m_fd = -1;
470 }
471
473}
474
475#endif // !_WIN32
476
477
478
483
484
486{
487 *this = std::move( aOther );
488}
489
490
492{
493 if( this == &aOther )
494 return *this;
495
496 Release();
497
498#ifdef _WIN32
499 m_handle = aOther.m_handle;
500 aOther.m_handle = nullptr;
501#else
502 m_fd = aOther.m_fd;
503 aOther.m_fd = -1;
504#endif
505
506 m_state = aOther.m_state;
507 aOther.m_state = STATE::NONE;
508
509 return *this;
510}
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
static std::vector< uint8_t > bytes(const std::string &aText)