288 lines
8.6 KiB
C++
288 lines
8.6 KiB
C++
/*
|
|
* This program source code file is part of KiCad, a free EDA CAD application.
|
|
*
|
|
* Copyright The KiCad Developers, see AUTHORS.txt for contributors.
|
|
*
|
|
* This program is free software; you can redistribute it and/or
|
|
* modify it under the terms of the GNU General Public License
|
|
* as published by the Free Software Foundation; either version 3
|
|
* of the License, or (at your option) any later version.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License
|
|
* along with this program; if not, you may find one here:
|
|
* http://www.gnu.org/licenses/gpl-3.0.html
|
|
* or you may search the http://www.gnu.org website for the version 3 license,
|
|
* or you may write to the Free Software Foundation, Inc.,
|
|
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
|
|
*/
|
|
|
|
#include <history_lock.h>
|
|
#include <lockfile.h>
|
|
#include <advanced_config.h>
|
|
#include <wx/filename.h>
|
|
#include <wx/filefn.h>
|
|
#include <wx/log.h>
|
|
#include <wx/datetime.h>
|
|
|
|
#include <git2.h>
|
|
|
|
#define HISTORY_LOCK_TRACE wxT( "KICAD_HISTORY_LOCK" )
|
|
|
|
|
|
static wxString historyPath( const wxString& aProjectPath )
|
|
{
|
|
wxFileName p( aProjectPath, wxEmptyString );
|
|
p.AppendDir( wxS( ".history" ) );
|
|
return p.GetPath();
|
|
}
|
|
|
|
|
|
HISTORY_LOCK_MANAGER::HISTORY_LOCK_MANAGER( const wxString& aProjectPath, int aStaleTimeoutSec ) :
|
|
m_projectPath( aProjectPath ),
|
|
m_historyPath( historyPath( aProjectPath ) ),
|
|
m_repo( nullptr ),
|
|
m_index( nullptr ),
|
|
m_repoOwned( false ),
|
|
m_indexOwned( false ),
|
|
m_staleTimeoutSec( aStaleTimeoutSec <= 0 ? ADVANCED_CFG::GetCfg().m_HistoryLockStaleTimeout : aStaleTimeoutSec )
|
|
{
|
|
wxLogTrace( HISTORY_LOCK_TRACE, "Attempting to acquire lock for project: %s (timeout: %d sec)",
|
|
aProjectPath, m_staleTimeoutSec );
|
|
|
|
// Layer 1: Acquire file-based lock to prevent cross-instance conflicts
|
|
if( !acquireFileLock() )
|
|
{
|
|
wxLogTrace( HISTORY_LOCK_TRACE, "Failed to acquire file lock: %s", m_lockError );
|
|
return;
|
|
}
|
|
|
|
// Layer 2: Open git repository
|
|
if( !openRepository() )
|
|
{
|
|
wxLogTrace( HISTORY_LOCK_TRACE, "Failed to open repository: %s", m_lockError );
|
|
return;
|
|
}
|
|
|
|
// Layer 3: Acquire git index lock for fine-grained operation safety
|
|
if( !acquireIndexLock() )
|
|
{
|
|
wxLogTrace( HISTORY_LOCK_TRACE, "Failed to acquire index lock: %s", m_lockError );
|
|
return;
|
|
}
|
|
|
|
wxLogTrace( HISTORY_LOCK_TRACE, "Successfully acquired all locks for project: %s", aProjectPath );
|
|
}
|
|
|
|
|
|
HISTORY_LOCK_MANAGER::~HISTORY_LOCK_MANAGER()
|
|
{
|
|
wxLogTrace( HISTORY_LOCK_TRACE, "Releasing locks for project: %s", m_projectPath );
|
|
|
|
// Release in reverse order of acquisition
|
|
if( m_indexOwned && m_index )
|
|
{
|
|
git_index_free( m_index );
|
|
m_index = nullptr;
|
|
}
|
|
|
|
if( m_repoOwned && m_repo )
|
|
{
|
|
git_repository_free( m_repo );
|
|
m_repo = nullptr;
|
|
}
|
|
|
|
// m_fileLock automatically releases via RAII destructor
|
|
}
|
|
|
|
|
|
bool HISTORY_LOCK_MANAGER::IsLocked() const
|
|
{
|
|
return m_fileLock && m_fileLock->Locked() && m_repo && m_index;
|
|
}
|
|
|
|
|
|
wxString HISTORY_LOCK_MANAGER::GetLockError() const
|
|
{
|
|
return m_lockError;
|
|
}
|
|
|
|
|
|
wxString HISTORY_LOCK_MANAGER::GetLockHolder() const
|
|
{
|
|
if( m_fileLock && !m_fileLock->Locked() )
|
|
{
|
|
return wxString::Format( wxS( "%s@%s" ),
|
|
m_fileLock->GetUsername(),
|
|
m_fileLock->GetHostname() );
|
|
}
|
|
return wxEmptyString;
|
|
}
|
|
|
|
|
|
bool HISTORY_LOCK_MANAGER::acquireFileLock()
|
|
{
|
|
// Ensure history directory exists
|
|
if( !wxDirExists( m_historyPath ) )
|
|
{
|
|
if( !wxFileName::Mkdir( m_historyPath, 0777, wxPATH_MKDIR_FULL ) )
|
|
{
|
|
m_lockError = wxString::Format(
|
|
_( "Cannot create history directory: %s" ), m_historyPath );
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Check for stale lock before attempting acquisition
|
|
if( IsLockStale( m_projectPath, m_staleTimeoutSec ) )
|
|
{
|
|
wxLogWarning( "Detected stale lock file, removing it" );
|
|
BreakStaleLock( m_projectPath );
|
|
}
|
|
|
|
// Create lock file path: .history/.repo.lock
|
|
wxFileName lockPath( m_historyPath, wxS( ".repo" ) );
|
|
|
|
// Use existing LOCKFILE infrastructure
|
|
m_fileLock = std::make_unique<LOCKFILE>( lockPath.GetFullPath(), true );
|
|
|
|
if( !m_fileLock->Locked() )
|
|
{
|
|
m_lockError = wxString::Format(
|
|
_( "History repository is locked by %s@%s" ),
|
|
m_fileLock->GetUsername(),
|
|
m_fileLock->GetHostname() );
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
|
|
bool HISTORY_LOCK_MANAGER::openRepository()
|
|
{
|
|
if( !wxDirExists( m_historyPath ) )
|
|
{
|
|
m_lockError = wxString::Format(
|
|
_( "History directory does not exist: %s" ), m_historyPath );
|
|
return false;
|
|
}
|
|
|
|
// Attempt to open existing repository
|
|
int rc = git_repository_open( &m_repo, m_historyPath.mb_str().data() );
|
|
|
|
if( rc != 0 )
|
|
{
|
|
const git_error* err = git_error_last();
|
|
m_lockError = wxString::Format(
|
|
_( "Failed to open git repository: %s" ),
|
|
err ? wxString::FromUTF8( err->message ) : wxString( "Unknown error" ) );
|
|
return false;
|
|
}
|
|
|
|
// Attempt to set workdir
|
|
rc = git_repository_set_workdir( m_repo, m_projectPath.mb_str().data(), false );
|
|
|
|
if( rc != 0 )
|
|
{
|
|
const git_error* err = git_error_last();
|
|
m_lockError = wxString::Format( _( "Failed to set git repository workdir to %s: %s" ), m_projectPath,
|
|
err ? wxString::FromUTF8( err->message ) : wxString( "Unknown error" ) );
|
|
return false;
|
|
}
|
|
|
|
m_repoOwned = true;
|
|
return true;
|
|
}
|
|
|
|
|
|
bool HISTORY_LOCK_MANAGER::acquireIndexLock()
|
|
{
|
|
if( !m_repo )
|
|
{
|
|
m_lockError = _( "Cannot acquire index lock: repository not open" );
|
|
return false;
|
|
}
|
|
|
|
// git_repository_index will fail if another process has .git/index.lock
|
|
int rc = git_repository_index( &m_index, m_repo );
|
|
|
|
if( rc != 0 )
|
|
{
|
|
const git_error* err = git_error_last();
|
|
m_lockError = wxString::Format(
|
|
_( "Failed to acquire git index lock (another operation in progress?): %s" ),
|
|
err ? wxString::FromUTF8( err->message ) : wxString( "Unknown error" ) );
|
|
return false;
|
|
}
|
|
|
|
m_indexOwned = true;
|
|
return true;
|
|
}
|
|
|
|
|
|
bool HISTORY_LOCK_MANAGER::IsLockStale( const wxString& aProjectPath, int aStaleTimeoutSec )
|
|
{
|
|
// Use config value if not explicitly specified
|
|
if( aStaleTimeoutSec <= 0 )
|
|
aStaleTimeoutSec = ADVANCED_CFG::GetCfg().m_HistoryLockStaleTimeout;
|
|
|
|
wxString histPath = historyPath( aProjectPath );
|
|
wxFileName lockPath( histPath, wxS( ".repo.lock" ) );
|
|
|
|
if( !lockPath.FileExists() )
|
|
return false; // No lock file exists
|
|
|
|
wxDateTime modTime;
|
|
if( !lockPath.GetTimes( nullptr, &modTime, nullptr ) )
|
|
return false; // Can't determine age
|
|
|
|
wxDateTime now = wxDateTime::Now();
|
|
wxTimeSpan age = now - modTime;
|
|
|
|
wxLogTrace( HISTORY_LOCK_TRACE, "Lock file age: %d seconds (stale threshold: %d)",
|
|
(int)age.GetSeconds().ToLong(), aStaleTimeoutSec );
|
|
|
|
return age.GetSeconds().ToLong() > aStaleTimeoutSec;
|
|
}
|
|
|
|
|
|
bool HISTORY_LOCK_MANAGER::BreakStaleLock( const wxString& aProjectPath )
|
|
{
|
|
wxString histPath = historyPath( aProjectPath );
|
|
wxFileName lockPath( histPath, wxS( ".repo.lock" ) );
|
|
|
|
if( !lockPath.FileExists() )
|
|
return true; // Already removed
|
|
|
|
// Also remove the LOCKFILE-style lock file if it exists
|
|
wxFileName lockFilePath( histPath, wxS( ".repo" ) );
|
|
lockFilePath.SetName( FILEEXT::LockFilePrefix + lockFilePath.GetName() );
|
|
lockFilePath.SetExt( lockFilePath.GetExt() + wxS( "." ) + FILEEXT::LockFileExtension );
|
|
|
|
bool result = true;
|
|
|
|
if( lockPath.FileExists() )
|
|
{
|
|
result = wxRemoveFile( lockPath.GetFullPath() );
|
|
if( result )
|
|
wxLogTrace( HISTORY_LOCK_TRACE, "Removed stale lock: %s", lockPath.GetFullPath() );
|
|
else
|
|
wxLogError( "Failed to remove stale lock: %s", lockPath.GetFullPath() );
|
|
}
|
|
|
|
if( lockFilePath.FileExists() )
|
|
{
|
|
bool lockFileResult = wxRemoveFile( lockFilePath.GetFullPath() );
|
|
if( lockFileResult )
|
|
wxLogTrace( HISTORY_LOCK_TRACE, "Removed stale lockfile: %s", lockFilePath.GetFullPath() );
|
|
result = result && lockFileResult;
|
|
}
|
|
|
|
return result;
|
|
}
|