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>
22#include <lockfile.h>
23#include <advanced_config.h>
24#include <pgm_base.h>
26#include <wx/filename.h>
27#include <wx/filefn.h>
28#include <wx/ffile.h>
29#include <wx/log.h>
30#include <wx/datetime.h>
31
32#include <git2.h>
33
34#define HISTORY_LOCK_TRACE wxT( "KICAD_HISTORY_LOCK" )
35
36
37static wxString historyPath( const wxString& aProjectPath )
38{
39 return Pgm().GetSettingsManager().GetLocalHistoryDirForPath( aProjectPath );
40}
41
42
43static wxString historyLockPath( const wxString& aProjectPath )
44{
45 // The lock must survive replacement of the history directory during trimming.
46 return historyPath( aProjectPath ) + wxS( ".repo" );
47}
48
49
50HISTORY_LOCK_MANAGER::HISTORY_LOCK_MANAGER( const wxString& aProjectPath, int aStaleTimeoutSec ) :
51 HISTORY_LOCK_MANAGER( aProjectPath, historyPath( aProjectPath ), aStaleTimeoutSec )
52{
53}
54
55
56HISTORY_LOCK_MANAGER::HISTORY_LOCK_MANAGER( const wxString& aProjectPath, const wxString& aHistoryPath,
57 int aStaleTimeoutSec ) :
58 m_projectPath( aProjectPath ),
59 m_historyPath( aHistoryPath ),
60 m_repo( nullptr ),
61 m_index( nullptr ),
62 m_repoOwned( false ),
63 m_indexOwned( false ),
64 m_staleTimeoutSec( aStaleTimeoutSec <= 0 ? ADVANCED_CFG::GetCfg().m_HistoryLockStaleTimeout : aStaleTimeoutSec )
65{
66 wxLogTrace( HISTORY_LOCK_TRACE, "Attempting to acquire lock for project: %s (timeout: %d sec)",
67 aProjectPath, m_staleTimeoutSec );
68
69 // Layer 1: Acquire file-based lock to prevent cross-instance conflicts
70 if( !acquireFileLock() )
71 {
72 wxLogTrace( HISTORY_LOCK_TRACE, "Failed to acquire file lock: %s", m_lockError );
73 return;
74 }
75
76 // Layer 2: Open git repository
77 if( !openRepository() )
78 {
79 wxLogTrace( HISTORY_LOCK_TRACE, "Failed to open repository: %s", m_lockError );
80 return;
81 }
82
83 // Layer 3: Acquire git index lock for fine-grained operation safety
84 if( !acquireIndexLock() )
85 {
86 wxLogTrace( HISTORY_LOCK_TRACE, "Failed to acquire index lock: %s", m_lockError );
87 return;
88 }
89
90 wxLogTrace( HISTORY_LOCK_TRACE, "Successfully acquired all locks for project: %s", aProjectPath );
91}
92
93
95{
96 wxLogTrace( HISTORY_LOCK_TRACE, "Releasing locks for project: %s", m_projectPath );
97
98 // Release in reverse order of acquisition
99 if( m_indexOwned && m_index )
100 {
101 git_index_free( m_index );
102 m_index = nullptr;
103 }
104
105 if( m_repoOwned && m_repo )
106 {
107 git_repository_free( m_repo );
108 m_repo = nullptr;
109 }
110
111 // m_fileLock automatically releases via RAII destructor
112}
113
114
116{
117 return m_fileLock && m_fileLock->Locked() && m_repo && m_index;
118}
119
120
122{
123 return m_lockError;
124}
125
126
128{
129 if( m_fileLock && !m_fileLock->Locked() )
130 {
131 return wxString::Format( wxS( "%s@%s" ),
132 m_fileLock->GetUsername(),
133 m_fileLock->GetHostname() );
134 }
135 return wxEmptyString;
136}
137
138
140{
141 // Ensure history directory exists
142 if( !wxDirExists( m_historyPath ) )
143 {
144 wxLogNull suppressSysErrorPopups;
145
146 if( !wxFileName::Mkdir( m_historyPath, 0777, wxPATH_MKDIR_FULL ) )
147 {
148 m_lockError = wxString::Format(
149 _( "Cannot create history directory: %s" ), m_historyPath );
150 return false;
151 }
152 }
153
154 m_fileLock = std::make_unique<LOCKFILE>( m_historyPath + wxS( ".repo" ), true );
155
156 if( !m_fileLock->Locked() )
157 {
158 m_lockError = wxString::Format(
159 _( "History repository is locked by %s@%s" ),
160 m_fileLock->GetUsername(),
161 m_fileLock->GetHostname() );
162 return false;
163 }
164
165 LOCKFILE legacyLock = LOCKFILE::Inspect( wxFileName( m_historyPath, wxS( ".repo" ) ).GetFullPath() );
166
167 if( !legacyLock.Valid() )
168 {
169 m_lockError = _( "History repository is locked by an older KiCad instance." );
170 return false;
171 }
172
173 return true;
174}
175
176
178{
179 if( !wxDirExists( m_historyPath ) )
180 {
181 m_lockError = wxString::Format(
182 _( "History directory does not exist: %s" ), m_historyPath );
183 return false;
184 }
185
186 // Attempt to open existing repository
187 int rc = git_repository_open( &m_repo, m_historyPath.mb_str().data() );
188
189 if( rc != 0 )
190 {
191 m_lockError = wxString::Format( _( "Failed to open git repository: %s" ), KIGIT_COMMON::GetLastGitError() );
192 return false;
193 }
194
195 // Attempt to set workdir
196 rc = git_repository_set_workdir( m_repo, m_projectPath.mb_str().data(), false );
197
198 if( rc != 0 )
199 {
200 m_lockError = wxString::Format( _( "Failed to set git repository workdir to %s: %s" ), m_projectPath,
202 return false;
203 }
204
205 // libgit2 will not read .history/.gitignore on its own (it sits outside the workdir).
206 // Inject its rules so users can edit that file and have them honoured.
207 wxFileName ignoreFile( m_historyPath, wxS( ".gitignore" ) );
208
209 if( ignoreFile.FileExists() )
210 {
211 wxFFile f( ignoreFile.GetFullPath(), wxT( "rb" ) );
212 wxString content;
213
214 if( f.IsOpened() && f.ReadAll( &content ) )
215 git_ignore_add_rule( m_repo, content.utf8_str() );
216 }
217
218 m_repoOwned = true;
219 return true;
220}
221
222
224{
225 if( !m_repo )
226 {
227 m_lockError = _( "Cannot acquire index lock: repository not open" );
228 return false;
229 }
230
231 // git_repository_index will fail if another process has .git/index.lock
232 int rc = git_repository_index( &m_index, m_repo );
233
234 if( rc != 0 )
235 {
236 m_lockError = wxString::Format( _( "Failed to acquire git index lock (another operation in progress?): %s" ),
238 return false;
239 }
240
241 m_indexOwned = true;
242 return true;
243}
244
245
246bool HISTORY_LOCK_MANAGER::IsLockStale( const wxString& aProjectPath, int aStaleTimeoutSec )
247{
248 // Use config value if not explicitly specified
249 if( aStaleTimeoutSec <= 0 )
251
252 wxFileName lockPath( LOCKFILE::LockPathFor( historyLockPath( aProjectPath ) ) );
253
254 if( !lockPath.FileExists() )
255 return false; // No lock file exists
256
257 wxDateTime modTime;
258 if( !lockPath.GetTimes( nullptr, &modTime, nullptr ) )
259 return false; // Can't determine age
260
261 wxDateTime now = wxDateTime::Now();
262 wxTimeSpan age = now - modTime;
263
264 wxLogTrace( HISTORY_LOCK_TRACE, "Lock file age: %d seconds (stale threshold: %d)",
265 (int)age.GetSeconds().ToLong(), aStaleTimeoutSec );
266
267 return age.GetSeconds().ToLong() > aStaleTimeoutSec
268 && LOCKFILE::Inspect( historyLockPath( aProjectPath ) ).Valid();
269}
270
271
272bool HISTORY_LOCK_MANAGER::BreakStaleLock( const wxString& aProjectPath )
273{
274 // Claim the abandoned lock before removing it, so a concurrent owner cannot be unlinked.
275 LOCKFILE lock( historyLockPath( aProjectPath ), true );
276 return lock.Locked();
277}
278
279
281{
282 if( m_indexOwned && m_index )
283 {
284 git_index_free( m_index );
285 m_index = nullptr;
286 m_indexOwned = false;
287 }
288
289 if( m_repoOwned && m_repo )
290 {
291 git_repository_free( m_repo );
292 m_repo = nullptr;
293 m_repoOwned = false;
294 }
295}
296
297
299{
300 if( m_repo && m_index )
301 return m_repo;
302
304
305 if( !m_fileLock || !m_fileLock->Locked() )
306 {
307 m_lockError = _( "Cannot re-open history repository without the file lock" );
308 return nullptr;
309 }
310
311 if( !openRepository() || !acquireIndexLock() )
312 return nullptr;
313
314 return m_repo;
315}
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.
static wxString GetLastGitError()
libgit2's last message, without credentials from any URL it quotes, so it is safe to show or trace.
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