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
42HISTORY_LOCK_MANAGER::HISTORY_LOCK_MANAGER( const wxString& aProjectPath, int aStaleTimeoutSec ) :
43 m_projectPath( aProjectPath ),
44 m_historyPath( historyPath( aProjectPath ) ),
45 m_repo( nullptr ),
46 m_index( nullptr ),
47 m_repoOwned( false ),
48 m_indexOwned( false ),
49 m_staleTimeoutSec( aStaleTimeoutSec <= 0 ? ADVANCED_CFG::GetCfg().m_HistoryLockStaleTimeout : aStaleTimeoutSec )
50{
51 wxLogTrace( HISTORY_LOCK_TRACE, "Attempting to acquire lock for project: %s (timeout: %d sec)",
52 aProjectPath, m_staleTimeoutSec );
53
54 // Layer 1: Acquire file-based lock to prevent cross-instance conflicts
55 if( !acquireFileLock() )
56 {
57 wxLogTrace( HISTORY_LOCK_TRACE, "Failed to acquire file lock: %s", m_lockError );
58 return;
59 }
60
61 // Layer 2: Open git repository
62 if( !openRepository() )
63 {
64 wxLogTrace( HISTORY_LOCK_TRACE, "Failed to open repository: %s", m_lockError );
65 return;
66 }
67
68 // Layer 3: Acquire git index lock for fine-grained operation safety
69 if( !acquireIndexLock() )
70 {
71 wxLogTrace( HISTORY_LOCK_TRACE, "Failed to acquire index lock: %s", m_lockError );
72 return;
73 }
74
75 wxLogTrace( HISTORY_LOCK_TRACE, "Successfully acquired all locks for project: %s", aProjectPath );
76}
77
78
80{
81 wxLogTrace( HISTORY_LOCK_TRACE, "Releasing locks for project: %s", m_projectPath );
82
83 // Release in reverse order of acquisition
84 if( m_indexOwned && m_index )
85 {
86 git_index_free( m_index );
87 m_index = nullptr;
88 }
89
90 if( m_repoOwned && m_repo )
91 {
92 git_repository_free( m_repo );
93 m_repo = nullptr;
94 }
95
96 // m_fileLock automatically releases via RAII destructor
97}
98
99
101{
102 return m_fileLock && m_fileLock->Locked() && m_repo && m_index;
103}
104
105
107{
108 return m_lockError;
109}
110
111
113{
114 if( m_fileLock && !m_fileLock->Locked() )
115 {
116 return wxString::Format( wxS( "%s@%s" ),
117 m_fileLock->GetUsername(),
118 m_fileLock->GetHostname() );
119 }
120 return wxEmptyString;
121}
122
123
125{
126 // Ensure history directory exists
127 if( !wxDirExists( m_historyPath ) )
128 {
129 wxLogNull suppressSysErrorPopups;
130
131 if( !wxFileName::Mkdir( m_historyPath, 0777, wxPATH_MKDIR_FULL ) )
132 {
133 m_lockError = wxString::Format(
134 _( "Cannot create history directory: %s" ), m_historyPath );
135 return false;
136 }
137 }
138
139 // Check for stale lock before attempting acquisition
141 {
142 wxLogWarning( "Detected stale lock file, removing it" );
144 }
145
146 // Create lock file path: .history/.repo.lock
147 wxFileName lockPath( m_historyPath, wxS( ".repo" ) );
148
149 // Use existing LOCKFILE infrastructure
150 m_fileLock = std::make_unique<LOCKFILE>( lockPath.GetFullPath(), true );
151
152 if( !m_fileLock->Locked() )
153 {
154 m_lockError = wxString::Format(
155 _( "History repository is locked by %s@%s" ),
156 m_fileLock->GetUsername(),
157 m_fileLock->GetHostname() );
158 return false;
159 }
160
161 return true;
162}
163
164
166{
167 if( !wxDirExists( m_historyPath ) )
168 {
169 m_lockError = wxString::Format(
170 _( "History directory does not exist: %s" ), m_historyPath );
171 return false;
172 }
173
174 // Attempt to open existing repository
175 int rc = git_repository_open( &m_repo, m_historyPath.mb_str().data() );
176
177 if( rc != 0 )
178 {
179 const git_error* err = git_error_last();
180 m_lockError = wxString::Format(
181 _( "Failed to open git repository: %s" ),
182 err ? wxString::FromUTF8( err->message ) : wxString( "Unknown error" ) );
183 return false;
184 }
185
186 // Attempt to set workdir
187 rc = git_repository_set_workdir( m_repo, m_projectPath.mb_str().data(), false );
188
189 if( rc != 0 )
190 {
191 const git_error* err = git_error_last();
192 m_lockError = wxString::Format( _( "Failed to set git repository workdir to %s: %s" ), m_projectPath,
193 err ? wxString::FromUTF8( err->message ) : wxString( "Unknown error" ) );
194 return false;
195 }
196
197 // libgit2 will not read .history/.gitignore on its own (it sits outside the workdir).
198 // Inject its rules so users can edit that file and have them honoured.
199 wxFileName ignoreFile( m_historyPath, wxS( ".gitignore" ) );
200
201 if( ignoreFile.FileExists() )
202 {
203 wxFFile f( ignoreFile.GetFullPath(), wxT( "rb" ) );
204 wxString content;
205
206 if( f.IsOpened() && f.ReadAll( &content ) )
207 git_ignore_add_rule( m_repo, content.utf8_str() );
208 }
209
210 m_repoOwned = true;
211 return true;
212}
213
214
216{
217 if( !m_repo )
218 {
219 m_lockError = _( "Cannot acquire index lock: repository not open" );
220 return false;
221 }
222
223 // git_repository_index will fail if another process has .git/index.lock
224 int rc = git_repository_index( &m_index, m_repo );
225
226 if( rc != 0 )
227 {
228 const git_error* err = git_error_last();
229 m_lockError = wxString::Format(
230 _( "Failed to acquire git index lock (another operation in progress?): %s" ),
231 err ? wxString::FromUTF8( err->message ) : wxString( "Unknown error" ) );
232 return false;
233 }
234
235 m_indexOwned = true;
236 return true;
237}
238
239
240bool HISTORY_LOCK_MANAGER::IsLockStale( const wxString& aProjectPath, int aStaleTimeoutSec )
241{
242 // Use config value if not explicitly specified
243 if( aStaleTimeoutSec <= 0 )
245
246 wxString histPath = historyPath( aProjectPath );
247 wxFileName lockPath( histPath, wxS( ".repo.lock" ) );
248
249 if( !lockPath.FileExists() )
250 return false; // No lock file exists
251
252 wxDateTime modTime;
253 if( !lockPath.GetTimes( nullptr, &modTime, nullptr ) )
254 return false; // Can't determine age
255
256 wxDateTime now = wxDateTime::Now();
257 wxTimeSpan age = now - modTime;
258
259 wxLogTrace( HISTORY_LOCK_TRACE, "Lock file age: %d seconds (stale threshold: %d)",
260 (int)age.GetSeconds().ToLong(), aStaleTimeoutSec );
261
262 return age.GetSeconds().ToLong() > aStaleTimeoutSec;
263}
264
265
266bool HISTORY_LOCK_MANAGER::BreakStaleLock( const wxString& aProjectPath )
267{
268 wxString histPath = historyPath( aProjectPath );
269 wxFileName lockPath( histPath, wxS( ".repo.lock" ) );
270
271 if( !lockPath.FileExists() )
272 return true; // Already removed
273
274 // Also remove the LOCKFILE-style lock file if it exists
275 wxFileName lockFilePath( histPath, wxS( ".repo" ) );
276 lockFilePath.SetName( FILEEXT::LockFilePrefix + lockFilePath.GetName() );
277 lockFilePath.SetExt( lockFilePath.GetExt() + wxS( "." ) + FILEEXT::LockFileExtension );
278
279 bool result = true;
280
281 if( lockPath.FileExists() )
282 {
283 result = wxRemoveFile( lockPath.GetFullPath() );
284 if( result )
285 wxLogTrace( HISTORY_LOCK_TRACE, "Removed stale lock: %s", lockPath.GetFullPath() );
286 else
287 wxLogError( "Failed to remove stale lock: %s", lockPath.GetFullPath() );
288 }
289
290 if( lockFilePath.FileExists() )
291 {
292 bool lockFileResult = wxRemoveFile( lockFilePath.GetFullPath() );
293 if( lockFileResult )
294 wxLogTrace( HISTORY_LOCK_TRACE, "Removed stale lockfile: %s", lockFilePath.GetFullPath() );
295 result = result && lockFileResult;
296 }
297
298 return result;
299}
300
301
303{
304 if( m_indexOwned && m_index )
305 {
306 git_index_free( m_index );
307 m_index = nullptr;
308 m_indexOwned = false;
309 }
310
311 if( m_repoOwned && m_repo )
312 {
313 git_repository_free( m_repo );
314 m_repo = nullptr;
315 m_repoOwned = false;
316 }
317}
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)
Forcibly remove a stale lock file.
std::unique_ptr< LOCKFILE > m_fileLock
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.
virtual SETTINGS_MANAGER & GetSettingsManager() const
Definition pgm_base.h:124
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 const std::string LockFileExtension
static const std::string LockFilePrefix
static wxString historyPath(const wxString &aProjectPath)
#define HISTORY_LOCK_TRACE
File locking utilities.
PGM_BASE & Pgm()
The global program "get" accessor.
see class PGM_BASE
wxString result
Test unit parsing edge cases and error handling.