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, you may find one here:
18 * http://www.gnu.org/licenses/gpl-3.0.html
19 * or you may search the http://www.gnu.org website for the version 3 license,
20 * or you may write to the Free Software Foundation, Inc.,
21 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
22 */
23
24#include <history_lock.h>
25#include <lockfile.h>
26#include <advanced_config.h>
27#include <wx/filename.h>
28#include <wx/filefn.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 wxFileName p( aProjectPath, wxEmptyString );
40 p.AppendDir( wxS( ".history" ) );
41 return p.GetPath();
42}
43
44
45HISTORY_LOCK_MANAGER::HISTORY_LOCK_MANAGER( const wxString& aProjectPath, int aStaleTimeoutSec )
46 : m_projectPath( aProjectPath )
47 , m_historyPath( historyPath( aProjectPath ) )
48 , m_repo( nullptr )
49 , m_index( nullptr )
50 , m_repoOwned( false )
51 , m_indexOwned( false )
52 , m_staleTimeoutSec( aStaleTimeoutSec <= 0 ?
53 ADVANCED_CFG::GetCfg().m_HistoryLockStaleTimeout :
54 aStaleTimeoutSec )
55{
56 wxLogTrace( HISTORY_LOCK_TRACE, "Attempting to acquire lock for project: %s (timeout: %d sec)",
57 aProjectPath, m_staleTimeoutSec );
58
59 // Layer 1: Acquire file-based lock to prevent cross-instance conflicts
60 if( !acquireFileLock() )
61 {
62 wxLogTrace( HISTORY_LOCK_TRACE, "Failed to acquire file lock: %s", m_lockError );
63 return;
64 }
65
66 // Layer 2: Open git repository
67 if( !openRepository() )
68 {
69 wxLogTrace( HISTORY_LOCK_TRACE, "Failed to open repository: %s", m_lockError );
70 return;
71 }
72
73 // Layer 3: Acquire git index lock for fine-grained operation safety
74 if( !acquireIndexLock() )
75 {
76 wxLogTrace( HISTORY_LOCK_TRACE, "Failed to acquire index lock: %s", m_lockError );
77 return;
78 }
79
80 wxLogTrace( HISTORY_LOCK_TRACE, "Successfully acquired all locks for project: %s", aProjectPath );
81}
82
83
85{
86 wxLogTrace( HISTORY_LOCK_TRACE, "Releasing locks for project: %s", m_projectPath );
87
88 // Release in reverse order of acquisition
89 if( m_indexOwned && m_index )
90 {
91 git_index_free( m_index );
92 m_index = nullptr;
93 }
94
95 if( m_repoOwned && m_repo )
96 {
97 git_repository_free( m_repo );
98 m_repo = nullptr;
99 }
100
101 // m_fileLock automatically releases via RAII destructor
102}
103
104
106{
107 return m_fileLock && m_fileLock->Locked() && m_repo && m_index;
108}
109
110
112{
113 return m_lockError;
114}
115
116
118{
119 if( m_fileLock && !m_fileLock->Locked() )
120 {
121 return wxString::Format( wxS( "%s@%s" ),
122 m_fileLock->GetUsername(),
123 m_fileLock->GetHostname() );
124 }
125 return wxEmptyString;
126}
127
128
130{
131 // Ensure history directory exists
132 if( !wxDirExists( m_historyPath ) )
133 {
134 if( !wxFileName::Mkdir( m_historyPath, 0777, wxPATH_MKDIR_FULL ) )
135 {
136 m_lockError = wxString::Format(
137 _( "Cannot create history directory: %s" ), m_historyPath );
138 return false;
139 }
140 }
141
142 // Check for stale lock before attempting acquisition
144 {
145 wxLogWarning( "Detected stale lock file, removing it" );
147 }
148
149 // Create lock file path: .history/.repo.lock
150 wxFileName lockPath( m_historyPath, wxS( ".repo" ) );
151
152 // Use existing LOCKFILE infrastructure
153 m_fileLock = std::make_unique<LOCKFILE>( lockPath.GetFullPath(), 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 return true;
165}
166
167
169{
170 if( !wxDirExists( m_historyPath ) )
171 {
172 m_lockError = wxString::Format(
173 _( "History directory does not exist: %s" ), m_historyPath );
174 return false;
175 }
176
177 // Attempt to open existing repository
178 int rc = git_repository_open( &m_repo, m_historyPath.mb_str().data() );
179
180 if( rc != 0 )
181 {
182 const git_error* err = git_error_last();
183 m_lockError = wxString::Format(
184 _( "Failed to open git repository: %s" ),
185 err ? wxString::FromUTF8( err->message ) : wxString( "Unknown error" ) );
186 return false;
187 }
188
189 m_repoOwned = true;
190 return true;
191}
192
193
195{
196 if( !m_repo )
197 {
198 m_lockError = _( "Cannot acquire index lock: repository not open" );
199 return false;
200 }
201
202 // git_repository_index will fail if another process has .git/index.lock
203 int rc = git_repository_index( &m_index, m_repo );
204
205 if( rc != 0 )
206 {
207 const git_error* err = git_error_last();
208 m_lockError = wxString::Format(
209 _( "Failed to acquire git index lock (another operation in progress?): %s" ),
210 err ? wxString::FromUTF8( err->message ) : wxString( "Unknown error" ) );
211 return false;
212 }
213
214 m_indexOwned = true;
215 return true;
216}
217
218
219bool HISTORY_LOCK_MANAGER::IsLockStale( const wxString& aProjectPath, int aStaleTimeoutSec )
220{
221 // Use config value if not explicitly specified
222 if( aStaleTimeoutSec <= 0 )
224
225 wxString histPath = historyPath( aProjectPath );
226 wxFileName lockPath( histPath, wxS( ".repo.lock" ) );
227
228 if( !lockPath.FileExists() )
229 return false; // No lock file exists
230
231 wxDateTime modTime;
232 if( !lockPath.GetTimes( nullptr, &modTime, nullptr ) )
233 return false; // Can't determine age
234
235 wxDateTime now = wxDateTime::Now();
236 wxTimeSpan age = now - modTime;
237
238 wxLogTrace( HISTORY_LOCK_TRACE, "Lock file age: %d seconds (stale threshold: %d)",
239 (int)age.GetSeconds().ToLong(), aStaleTimeoutSec );
240
241 return age.GetSeconds().ToLong() > aStaleTimeoutSec;
242}
243
244
245bool HISTORY_LOCK_MANAGER::BreakStaleLock( const wxString& aProjectPath )
246{
247 wxString histPath = historyPath( aProjectPath );
248 wxFileName lockPath( histPath, wxS( ".repo.lock" ) );
249
250 if( !lockPath.FileExists() )
251 return true; // Already removed
252
253 // Also remove the LOCKFILE-style lock file if it exists
254 wxFileName lockFilePath( histPath, wxS( ".repo" ) );
255 lockFilePath.SetName( FILEEXT::LockFilePrefix + lockFilePath.GetName() );
256 lockFilePath.SetExt( lockFilePath.GetExt() + wxS( "." ) + FILEEXT::LockFileExtension );
257
258 bool result = true;
259
260 if( lockPath.FileExists() )
261 {
262 result = wxRemoveFile( lockPath.GetFullPath() );
263 if( result )
264 wxLogTrace( HISTORY_LOCK_TRACE, "Removed stale lock: %s", lockPath.GetFullPath() );
265 else
266 wxLogError( "Failed to remove stale lock: %s", lockPath.GetFullPath() );
267 }
268
269 if( lockFilePath.FileExists() )
270 {
271 bool lockFileResult = wxRemoveFile( lockFilePath.GetFullPath() );
272 if( lockFileResult )
273 wxLogTrace( HISTORY_LOCK_TRACE, "Removed stale lockfile: %s", lockFilePath.GetFullPath() );
274 result = result && lockFileResult;
275 }
276
277 return result;
278}
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.
~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.
#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.
wxString result
Test unit parsing edge cases and error handling.