KiCad PCB EDA Suite
Loading...
Searching...
No Matches
history_lock.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
7 * modify it under the terms of the GNU General Public License
8 * as published by the Free Software Foundation; either version 3
9 * of the License, or (at your option) any later version.
10 *
11 * This program is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 * GNU 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 <history_lock.h>
21#include <lockfile.h>
22#include <advanced_config.h>
23#include <pgm_base.h>
25#include <wx/filename.h>
26#include <wx/filefn.h>
27#include <wx/ffile.h>
28#include <wx/log.h>
29#include <wx/datetime.h>
30
31#include <git2.h>
32
33#define HISTORY_LOCK_TRACE wxT( "KICAD_HISTORY_LOCK" )
34
35
36static wxString historyPath( const wxString& aProjectPath )
37{
38 return Pgm().GetSettingsManager().GetLocalHistoryDirForPath( aProjectPath );
39}
40
41
42static wxString historyLockPath( const wxString& aProjectPath )
43{
44 // The lock must survive replacement of the history directory during trimming.
45 return historyPath( aProjectPath ) + wxS( ".repo" );
46}
47
48
49HISTORY_LOCK_MANAGER::HISTORY_LOCK_MANAGER( const wxString& aProjectPath, int aStaleTimeoutSec ) :
50 HISTORY_LOCK_MANAGER( aProjectPath, historyPath( aProjectPath ), aStaleTimeoutSec )
51{
52}
53
54
55HISTORY_LOCK_MANAGER::HISTORY_LOCK_MANAGER( const wxString& aProjectPath, const wxString& aHistoryPath,
56 int aStaleTimeoutSec ) :
57 m_projectPath( aProjectPath ),
58 m_historyPath( aHistoryPath ),
59 m_repo( nullptr ),
60 m_index( nullptr ),
61 m_repoOwned( false ),
62 m_indexOwned( false ),
63 m_staleTimeoutSec( aStaleTimeoutSec <= 0 ? ADVANCED_CFG::GetCfg().m_HistoryLockStaleTimeout : aStaleTimeoutSec )
64{
65 wxLogTrace( HISTORY_LOCK_TRACE, "Attempting to acquire lock for project: %s (timeout: %d sec)",
66 aProjectPath, m_staleTimeoutSec );
67
68 // Layer 1: Acquire file-based lock to prevent cross-instance conflicts
69 if( !acquireFileLock() )
70 {
71 wxLogTrace( HISTORY_LOCK_TRACE, "Failed to acquire file lock: %s", m_lockError );
72 return;
73 }
74
75 // Layer 2: Open git repository
76 if( !openRepository() )
77 {
78 wxLogTrace( HISTORY_LOCK_TRACE, "Failed to open repository: %s", m_lockError );
79 return;
80 }
81
82 // Layer 3: Acquire git index lock for fine-grained operation safety
83 if( !acquireIndexLock() )
84 {
85 wxLogTrace( HISTORY_LOCK_TRACE, "Failed to acquire index lock: %s", m_lockError );
86 return;
87 }
88
89 wxLogTrace( HISTORY_LOCK_TRACE, "Successfully acquired all locks for project: %s", aProjectPath );
90}
91
92
94{
95 wxLogTrace( HISTORY_LOCK_TRACE, "Releasing locks for project: %s", m_projectPath );
96
97 // Release in reverse order of acquisition
98 if( m_indexOwned && m_index )
99 {
100 git_index_free( m_index );
101 m_index = nullptr;
102 }
103
104 if( m_repoOwned && m_repo )
105 {
106 git_repository_free( m_repo );
107 m_repo = nullptr;
108 }
109
110 // m_fileLock automatically releases via RAII destructor
111}
112
113
115{
116 return m_fileLock && m_fileLock->Locked() && m_repo && m_index;
117}
118
119
121{
122 return m_lockError;
123}
124
125
127{
128 if( m_fileLock && !m_fileLock->Locked() )
129 {
130 return wxString::Format( wxS( "%s@%s" ),
131 m_fileLock->GetUsername(),
132 m_fileLock->GetHostname() );
133 }
134 return wxEmptyString;
135}
136
137
139{
140 // Ensure history directory exists
141 if( !wxDirExists( m_historyPath ) )
142 {
143 wxLogNull suppressSysErrorPopups;
144
145 if( !wxFileName::Mkdir( m_historyPath, 0777, wxPATH_MKDIR_FULL ) )
146 {
147 m_lockError = wxString::Format(
148 _( "Cannot create history directory: %s" ), m_historyPath );
149 return false;
150 }
151 }
152
153 m_fileLock = std::make_unique<LOCKFILE>( m_historyPath + wxS( ".repo" ), true );
154
155 if( !m_fileLock->Locked() )
156 {
157 m_lockError = wxString::Format(
158 _( "History repository is locked by %s@%s" ),
159 m_fileLock->GetUsername(),
160 m_fileLock->GetHostname() );
161 return false;
162 }
163
164 LOCKFILE legacyLock = LOCKFILE::Inspect( wxFileName( m_historyPath, wxS( ".repo" ) ).GetFullPath() );
165
166 if( !legacyLock.Valid() )
167 {
168 m_lockError = _( "History repository is locked by an older KiCad instance." );
169 return false;
170 }
171
172 return true;
173}
174
175
177{
178 if( !wxDirExists( m_historyPath ) )
179 {
180 m_lockError = wxString::Format(
181 _( "History directory does not exist: %s" ), m_historyPath );
182 return false;
183 }
184
185 // Attempt to open existing repository
186 int rc = git_repository_open( &m_repo, m_historyPath.mb_str().data() );
187
188 if( rc != 0 )
189 {
190 const git_error* err = git_error_last();
191 m_lockError = wxString::Format(
192 _( "Failed to open git repository: %s" ),
193 err ? wxString::FromUTF8( err->message ) : wxString( "Unknown error" ) );
194 return false;
195 }
196
197 // Attempt to set workdir
198 rc = git_repository_set_workdir( m_repo, m_projectPath.mb_str().data(), false );
199
200 if( rc != 0 )
201 {
202 const git_error* err = git_error_last();
203 m_lockError = wxString::Format( _( "Failed to set git repository workdir to %s: %s" ), m_projectPath,
204 err ? wxString::FromUTF8( err->message ) : wxString( "Unknown error" ) );
205 return false;
206 }
207
208 // libgit2 will not read .history/.gitignore on its own (it sits outside the workdir).
209 // Inject its rules so users can edit that file and have them honoured.
210 wxFileName ignoreFile( m_historyPath, wxS( ".gitignore" ) );
211
212 if( ignoreFile.FileExists() )
213 {
214 wxFFile f( ignoreFile.GetFullPath(), wxT( "rb" ) );
215 wxString content;
216
217 if( f.IsOpened() && f.ReadAll( &content ) )
218 git_ignore_add_rule( m_repo, content.utf8_str() );
219 }
220
221 m_repoOwned = true;
222 return true;
223}
224
225
227{
228 if( !m_repo )
229 {
230 m_lockError = _( "Cannot acquire index lock: repository not open" );
231 return false;
232 }
233
234 // git_repository_index will fail if another process has .git/index.lock
235 int rc = git_repository_index( &m_index, m_repo );
236
237 if( rc != 0 )
238 {
239 const git_error* err = git_error_last();
240 m_lockError = wxString::Format(
241 _( "Failed to acquire git index lock (another operation in progress?): %s" ),
242 err ? wxString::FromUTF8( err->message ) : wxString( "Unknown error" ) );
243 return false;
244 }
245
246 m_indexOwned = true;
247 return true;
248}
249
250
251bool HISTORY_LOCK_MANAGER::IsLockStale( const wxString& aProjectPath, int aStaleTimeoutSec )
252{
253 // Use config value if not explicitly specified
254 if( aStaleTimeoutSec <= 0 )
256
257 wxFileName lockPath( LOCKFILE::LockPathFor( historyLockPath( aProjectPath ) ) );
258
259 if( !lockPath.FileExists() )
260 return false; // No lock file exists
261
262 wxDateTime modTime;
263 if( !lockPath.GetTimes( nullptr, &modTime, nullptr ) )
264 return false; // Can't determine age
265
266 wxDateTime now = wxDateTime::Now();
267 wxTimeSpan age = now - modTime;
268
269 wxLogTrace( HISTORY_LOCK_TRACE, "Lock file age: %d seconds (stale threshold: %d)",
270 (int)age.GetSeconds().ToLong(), aStaleTimeoutSec );
271
272 return age.GetSeconds().ToLong() > aStaleTimeoutSec
273 && LOCKFILE::Inspect( historyLockPath( aProjectPath ) ).Valid();
274}
275
276
277bool HISTORY_LOCK_MANAGER::BreakStaleLock( const wxString& aProjectPath )
278{
279 // Claim the abandoned lock before removing it, so a concurrent owner cannot be unlinked.
280 LOCKFILE lock( historyLockPath( aProjectPath ), true );
281 return lock.Locked();
282}
283
284
286{
287 if( m_indexOwned && m_index )
288 {
289 git_index_free( m_index );
290 m_index = nullptr;
291 m_indexOwned = false;
292 }
293
294 if( m_repoOwned && m_repo )
295 {
296 git_repository_free( m_repo );
297 m_repo = nullptr;
298 m_repoOwned = false;
299 }
300}
301
302
304{
305 if( m_repo && m_index )
306 return m_repo;
307
309
310 if( !m_fileLock || !m_fileLock->Locked() )
311 {
312 m_lockError = _( "Cannot re-open history repository without the file lock" );
313 return nullptr;
314 }
315
316 if( !openRepository() || !acquireIndexLock() )
317 return nullptr;
318
319 return m_repo;
320}
static const ADVANCED_CFG & GetCfg()
Get the singleton instance's config, which is shared by all consumers.
wxString GetLockHolder() const
Get information about who currently holds the lock.
HISTORY_LOCK_MANAGER(const wxString &aProjectPath, int aStaleTimeoutSec=0)
Construct a lock manager and attempt to acquire locks.
void ReleaseRepository()
Release git repository and index handles early, but keep the file lock.
~HISTORY_LOCK_MANAGER()
Destructor releases all locks and closes git repository.
wxString GetLockError() const
Get error message describing why lock could not be acquired.
git_repository * m_repo
static bool BreakStaleLock(const wxString &aProjectPath)
Claim and remove an abandoned lock belonging to this user.
std::unique_ptr< LOCKFILE > m_fileLock
git_repository * ReopenRepository()
Re-open the git repository and index after ReleaseRepository().
static bool IsLockStale(const wxString &aProjectPath, int aStaleTimeoutSec=0)
Check if a lock file exists and is stale (older than timeout).
bool IsLocked() const
Check if locks were successfully acquired.
Advisory lock over a file, taken by writing a sibling lock file and holding an exclusive lock on it f...
Definition lockfile.h:54
static wxString LockPathFor(const wxString &aFilename)
Definition lockfile.h:134
bool Valid() const
Definition lockfile.h:224
bool Locked() const
Definition lockfile.h:222
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:113
virtual SETTINGS_MANAGER & GetSettingsManager() const
Definition pgm_base.h:123
wxString GetLocalHistoryDirForPath(const wxString &aProjectPath) const
Resolve the local-history directory for a project given by its on-disk path.
#define _(s)
int m_HistoryLockStaleTimeout
Stale lock timeout for local history repository locks, in seconds.
static wxString historyPath(const wxString &aProjectPath)
static wxString historyLockPath(const wxString &aProjectPath)
#define HISTORY_LOCK_TRACE
PGM_BASE & Pgm()
The global program "get" accessor.
see class PGM_BASE