KiCad PCB EDA Suite
Loading...
Searching...
No Matches
test_lockfile.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 <boost/test/unit_test.hpp>
21
22#include <lockfile.h>
23#include <json_common.h>
24#include <kiplatform/io.h>
25
26#include <wx/ffile.h>
27#include <wx/filefn.h>
28#include <wx/filename.h>
29#include <wx/utils.h>
30
31#if !defined( __WINDOWS__ )
32#include <unistd.h>
33#endif
34
35
36namespace
37{
38
39using FILE_LOCK = KIPLATFORM::IO::FILE_LOCK;
40
41
42wxString makeTempTargetPath( const wxString& aTag )
43{
44 wxString tempDir = wxFileName::GetTempDir();
45 wxString leaf = wxString::Format( wxT( "kicad-lockfile-%s-%ld.kicad_pcb" ), aTag,
46 static_cast<long>( wxGetLocalTimeMillis().GetValue() ) );
47 return tempDir + wxFileName::GetPathSeparator() + leaf;
48}
49
50
51void writeRaw( const wxString& aPath, const std::string& aContent )
52{
53 wxFFile fp( aPath, wxT( "wb" ) );
54 BOOST_REQUIRE( fp.IsOpened() );
55
56 if( !aContent.empty() )
57 BOOST_REQUIRE( fp.Write( aContent.data(), aContent.size() ) == aContent.size() );
58
59 fp.Close();
60}
61
62
63nlohmann::json readLockJson( const wxString& aPath )
64{
65 wxFFile fp( aPath, wxT( "rb" ) );
66 BOOST_REQUIRE( fp.IsOpened() );
67
68 wxString contents;
69 BOOST_REQUIRE( fp.ReadAll( &contents ) );
70
71 return nlohmann::json::parse( std::string( contents.mb_str() ) );
72}
73
74
75std::string selfOwnedLockJson()
76{
77 nlohmann::json j;
78 j["username"] = std::string( wxGetUserId().mb_str() );
79 j["hostname"] = std::string( wxGetHostName().mb_str() );
80 j["token"] = "0123456789abcdef0123456789abcdef";
81 return j.dump();
82}
83
84
85// Simulates another live KiCad process holding the OS lock
86FILE_LOCK holdLockOn( const wxString& aLockPath )
87{
88 FILE_LOCK holder;
89 bool created = false;
90
91 BOOST_REQUIRE( holder.Acquire( aLockPath, created ) == FILE_LOCK::STATE::HELD );
92
93 return holder;
94}
95
96
97// Root ignores directory permissions, so the read-only cases can't be tested as root
98bool canTestUnwritableDirs()
99{
100#if defined( __WINDOWS__ )
101 return false;
102#else
103 return geteuid() != 0;
104#endif
105}
106
107} // anonymous namespace
108
109
110BOOST_AUTO_TEST_SUITE( LockFileTests )
111
112
113BOOST_AUTO_TEST_CASE( AcquireFreshLock )
114{
115 wxString target = makeTempTargetPath( wxT( "fresh" ) );
116 wxString lockPath = LOCKFILE::LockPathFor( target );
117
118 BOOST_REQUIRE( !wxFileName::FileExists( lockPath ) );
119
120 {
121 LOCKFILE lock( target );
122 BOOST_CHECK( lock.Valid() );
123 BOOST_CHECK( lock.Locked() );
124 BOOST_CHECK( wxFileName::FileExists( lockPath ) );
125 }
126
127 BOOST_CHECK( !wxFileName::FileExists( lockPath ) );
128}
129
130
131// A foreign lock is never stale to us and must survive so its owner can still be named
132BOOST_AUTO_TEST_CASE( ForeignLockNotOwned )
133{
134 wxString target = makeTempTargetPath( wxT( "foreign" ) );
135 wxString lockPath = LOCKFILE::LockPathFor( target );
136
137 writeRaw( lockPath, R"({"username":"someone-else","hostname":"another-host"})" );
138
139 {
140 LOCKFILE lock( target );
141 BOOST_CHECK( !lock.Valid() );
142 BOOST_CHECK( !lock.IsLockedByMe() );
143 BOOST_CHECK_EQUAL( lock.GetUsername(), wxString( "someone-else" ) );
144 BOOST_CHECK_EQUAL( lock.GetHostname(), wxString( "another-host" ) );
145 }
146
147 BOOST_REQUIRE( wxFileName::FileExists( lockPath ) );
148
149 LOCKFILE reread( target );
150 BOOST_CHECK_EQUAL( reread.GetUsername(), wxString( "someone-else" ) );
151
152 wxRemoveFile( lockPath );
153}
154
155
156// Regression: #23734 — empty lock (unfinished cloud sync) must be reclaimable, not a hard error.
157BOOST_AUTO_TEST_CASE( EmptyStaleLockIsReclaimable )
158{
159 wxString target = makeTempTargetPath( wxT( "empty" ) );
160 wxString lockPath = LOCKFILE::LockPathFor( target );
161
162 writeRaw( lockPath, "" );
163
164 {
165 LOCKFILE lock( target );
166 BOOST_CHECK( lock.Valid() );
167 BOOST_CHECK_EQUAL( lock.GetUsername(), wxGetUserId() );
168 }
169
170 BOOST_CHECK( !wxFileName::FileExists( lockPath ) );
171}
172
173
174// Regression: #23734 — corrupt/truncated lock (partial sync) must behave the same as an empty one.
175BOOST_AUTO_TEST_CASE( CorruptStaleLockIsReclaimable )
176{
177 wxString target = makeTempTargetPath( wxT( "corrupt" ) );
178 wxString lockPath = LOCKFILE::LockPathFor( target );
179
180 writeRaw( lockPath, R"({"username":"partia)" );
181
182 {
183 LOCKFILE lock( target );
184 BOOST_CHECK( lock.Valid() );
185 }
186
187 BOOST_CHECK( !wxFileName::FileExists( lockPath ) );
188}
189
190
191// Issue #11458 - our own crashed session's lock must not pin the target read-only
192BOOST_AUTO_TEST_CASE( AbandonedSelfLockIsReclaimed )
193{
194 wxString target = makeTempTargetPath( wxT( "abandoned" ) );
195 wxString lockPath = LOCKFILE::LockPathFor( target );
196
197 writeRaw( lockPath, selfOwnedLockJson() );
198
199 {
200 LOCKFILE lock( target );
201 BOOST_CHECK( lock.Valid() );
202 BOOST_CHECK( lock.Locked() );
203 BOOST_CHECK_EQUAL( lock.GetUsername(), wxGetUserId() );
204
205 // Reclaim must replace the owner record, or release would refuse to clean up
206 BOOST_CHECK( readLockJson( lockPath ).value( "token", std::string() )
207 != "0123456789abcdef0123456789abcdef" );
208 }
209
210 BOOST_CHECK( !wxFileName::FileExists( lockPath ) );
211}
212
213
214// A live same-user lock, as when kicad holds it while pcbnew opens the same project, must never
215// be taken
216BOOST_AUTO_TEST_CASE( LiveLockIsNeverTaken )
217{
218 wxString target = makeTempTargetPath( wxT( "live" ) );
219 wxString lockPath = LOCKFILE::LockPathFor( target );
220
221 writeRaw( lockPath, selfOwnedLockJson() );
222
223 FILE_LOCK owner = holdLockOn( lockPath );
224
225 {
226 LOCKFILE lock( target );
227 BOOST_CHECK( !lock.Valid() );
228 BOOST_CHECK( !lock.Locked() );
229 BOOST_CHECK_EQUAL( lock.GetUsername(), wxGetUserId() );
230 }
231
232 BOOST_REQUIRE( wxFileName::FileExists( lockPath ) );
233 BOOST_CHECK_EQUAL( readLockJson( lockPath ).value( "token", std::string() ),
234 std::string( "0123456789abcdef0123456789abcdef" ) );
235
236 owner.Release();
237 wxRemoveFile( lockPath );
238}
239
240
241// Inspect must not create, claim, or remove the lock it looks at
242BOOST_AUTO_TEST_CASE( InspectDisturbsNothing )
243{
244 wxString target = makeTempTargetPath( wxT( "inspect" ) );
245 wxString lockPath = LOCKFILE::LockPathFor( target );
246
247 {
248 LOCKFILE probe = LOCKFILE::Inspect( target );
249 BOOST_CHECK( probe.Valid() );
250 BOOST_CHECK( !probe.Locked() );
251 }
252
253 BOOST_CHECK( !wxFileName::FileExists( lockPath ) );
254
255 writeRaw( lockPath, selfOwnedLockJson() );
256
257 {
258 LOCKFILE probe = LOCKFILE::Inspect( target );
259 BOOST_CHECK( probe.Valid() );
260 BOOST_CHECK( !probe.Locked() );
261 BOOST_CHECK_EQUAL( probe.GetUsername(), wxGetUserId() );
262 }
263
264 BOOST_REQUIRE( wxFileName::FileExists( lockPath ) );
265 BOOST_CHECK_EQUAL( readLockJson( lockPath ).value( "token", std::string() ),
266 std::string( "0123456789abcdef0123456789abcdef" ) );
267
268 FILE_LOCK owner = holdLockOn( lockPath );
269
270 {
271 LOCKFILE probe = LOCKFILE::Inspect( target );
272 BOOST_CHECK( !probe.Valid() );
273 BOOST_CHECK_EQUAL( probe.GetUsername(), wxGetUserId() );
274 }
275
276 owner.Release();
277 wxRemoveFile( lockPath );
278}
279
280
281// A second holder in this process is as foreign as any other process
282BOOST_AUTO_TEST_CASE( SecondHolderOfTheSameTargetIsReadOnly )
283{
284 wxString target = makeTempTargetPath( wxT( "second" ) );
285
286 LOCKFILE first( target );
287 BOOST_REQUIRE( first.Valid() );
288
289 {
290 LOCKFILE second( target );
291 BOOST_CHECK( !second.Valid() );
292 BOOST_CHECK( !second.Locked() );
293 }
294
295 // Releasing the reader must not have disturbed the lock the first holder still owns
296 BOOST_CHECK( wxFileName::FileExists( LOCKFILE::LockPathFor( target ) ) );
297 BOOST_CHECK( first.Valid() );
298}
299
300
301// The user is still allowed to force a lock away from a live owner
302BOOST_AUTO_TEST_CASE( LiveLockCanBeOverridden )
303{
304 wxString target = makeTempTargetPath( wxT( "override" ) );
305 wxString lockPath = LOCKFILE::LockPathFor( target );
306
307 writeRaw( lockPath, selfOwnedLockJson() );
308
309 FILE_LOCK owner = holdLockOn( lockPath );
310
311 {
312 LOCKFILE lock( target );
313 BOOST_REQUIRE( !lock.Valid() );
314
315 BOOST_CHECK( lock.OverrideLock() );
316 BOOST_CHECK( lock.Valid() );
317 }
318
319 owner.Release();
320
321 if( wxFileName::FileExists( lockPath ) )
322 wxRemoveFile( lockPath );
323}
324
325
326// Releasing a lock that someone else has since taken over must leave their lock alone
327BOOST_AUTO_TEST_CASE( TakenOverLockNotRemovedOnRelease )
328{
329 wxString target = makeTempTargetPath( wxT( "taken-over" ) );
330 wxString lockPath = LOCKFILE::LockPathFor( target );
331
332 {
333 LOCKFILE lock( target );
334 BOOST_REQUIRE( lock.Valid() );
335
336 nlohmann::json newOwner = readLockJson( lockPath );
337 newOwner["token"] = "ffffffffffffffffffffffffffffffff";
338 writeRaw( lockPath, newOwner.dump() );
339 }
340
341 BOOST_CHECK( wxFileName::FileExists( lockPath ) );
342 wxRemoveFile( lockPath );
343}
344
345
346// A lock we can read but not write must still name its owner
347BOOST_AUTO_TEST_CASE( UnwritableLockStillNamesItsOwner )
348{
349 if( !canTestUnwritableDirs() )
350 return;
351
352 wxString target = makeTempTargetPath( wxT( "unwritable" ) );
353 wxString lockPath = LOCKFILE::LockPathFor( target );
354
355 writeRaw( lockPath, R"({"username":"someone-else","hostname":"another-host"})" );
356 BOOST_REQUIRE( wxFileName( lockPath ).SetPermissions( wxS_IRUSR ) );
357
358 {
359 LOCKFILE lock( target );
360 BOOST_CHECK( !lock.Valid() );
361 BOOST_CHECK_EQUAL( lock.GetUsername(), wxString( "someone-else" ) );
362 }
363
364 wxFileName( lockPath ).SetPermissions( wxS_IRUSR | wxS_IWUSR );
365 wxRemoveFile( lockPath );
366}
367
368
369// An unwritable directory must not block opening a read-only project
370BOOST_AUTO_TEST_CASE( UnwritableDirectoryDegradesToAllow )
371{
372 if( !canTestUnwritableDirs() )
373 return;
374
375 wxString dir = wxFileName::GetTempDir() + wxFileName::GetPathSeparator()
376 + wxString::Format( wxT( "kicad-lockfile-ro-%ld" ),
377 static_cast<long>( wxGetLocalTimeMillis().GetValue() ) );
378
379 BOOST_REQUIRE( wxFileName::Mkdir( dir, 0700, wxPATH_MKDIR_FULL ) );
380
381 wxString target = dir + wxFileName::GetPathSeparator() + wxT( "ro.kicad_pro" );
382 writeRaw( target, "{}" );
383
384 BOOST_REQUIRE( wxFileName( dir ).SetPermissions( wxS_IRUSR | wxS_IXUSR ) );
385
386 {
387 LOCKFILE lock( target );
388 BOOST_CHECK( lock.Valid() );
389 BOOST_CHECK( !lock.Locked() );
390 BOOST_CHECK( !wxFileName::FileExists( LOCKFILE::LockPathFor( target ) ) );
391 }
392
393 wxFileName( dir ).SetPermissions( wxS_IRUSR | wxS_IWUSR | wxS_IXUSR );
394 wxRemoveFile( target );
395 wxFileName::Rmdir( dir );
396}
397
398
An exclusive advisory lock on a file, held for the lifetime of this object.
Definition io.h:91
Advisory lock over a file, taken by writing a sibling lock file and holding an exclusive lock on it f...
Definition lockfile.h:60
static wxString LockPathFor(const wxString &aFilename)
Definition lockfile.h:140
bool Valid() const
Definition lockfile.h:233
wxString GetUsername()
Definition lockfile.h:220
wxString GetHostname()
Definition lockfile.h:226
bool Locked() const
Definition lockfile.h:228
bool IsLockedByMe()
Definition lockfile.h:206
static LOCKFILE Inspect(const wxString &aFilename)
Look at a lock without taking it: nothing is created, nothing is claimed and nothing is removed on re...
Definition lockfile.h:119
File locking utilities.
BOOST_AUTO_TEST_CASE(HorizontalAlignment)
BOOST_AUTO_TEST_SUITE(CadstarPartParser)
BOOST_REQUIRE(intersection.has_value()==c.ExpectedIntersection.has_value())
BOOST_AUTO_TEST_SUITE_END()
BOOST_AUTO_TEST_CASE(AcquireFreshLock)
BOOST_CHECK_EQUAL(result, "25.4")