Upgrade file locking

wxSingleInstanceChecker is meant for running programs, not file locking.
This implements an RAII class for file locking that stores the lock
files next to the file being locked, allowing it to be easily found and
removed.  Also includes the ability to override the lock, with
information about the original owner

Fixes https://gitlab.com/kicad/code/kicad/-/issues/14734
This commit is contained in:
Seth Hillbrand 2023-05-24 13:19:48 -07:00
parent 76d66571e4
commit 122be418bb
10 changed files with 277 additions and 127 deletions

View File

@ -356,7 +356,6 @@ set( COMMON_SRCS
lib_tree_model.cpp
lib_tree_model_adapter.cpp
locale_io.cpp
lockfile.cpp
lset.cpp
marker_base.cpp
markup_parser.cpp

View File

@ -209,15 +209,20 @@ EDA_DRAW_FRAME::~EDA_DRAW_FRAME()
void EDA_DRAW_FRAME::ReleaseFile()
{
m_file_checker = nullptr;
if( m_file_checker.get() != nullptr )
m_file_checker->UnlockFile();
}
bool EDA_DRAW_FRAME::LockFile( const wxString& aFileName )
{
m_file_checker = ::LockFile( aFileName );
// We need to explicitly reset here to get the deletion before
// we create a new unique_ptr that may be for the same file
m_file_checker.reset();
return m_file_checker && !m_file_checker->IsAnotherRunning();
m_file_checker = std::make_unique<LOCKFILE>( aFileName );
return m_file_checker->Locked();
}

View File

@ -1,96 +0,0 @@
/*
* This program source code file is part of KiCad, a free EDA CAD application.
*
* Copyright (C) 2014-2015 SoftPLC Corporation, Dick Hollenbeck <dick@softplc.com>
* Copyright (C) 2014-2022 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 2
* 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/old-licenses/gpl-2.0.html
* or you may search the http://www.gnu.org website for the version 2 license,
* or you may write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
*/
#include <memory>
#include <build_version.h>
#include <lockfile.h>
#include <wx/filename.h>
#include <wx/snglinst.h>
std::unique_ptr<wxSingleInstanceChecker> LockFile( const wxString& aFileName )
{
// first make absolute and normalize, to avoid that different lock files
// for the same file can be created
wxFileName fn( aFileName );
fn.MakeAbsolute();
wxString lockFileName = fn.GetFullPath() + ".lock";
lockFileName.Replace( "/", "_" );
// We can have filenames coming from Windows, so also convert Windows separator
lockFileName.Replace( "\\", "_" );
auto p = std::make_unique<wxSingleInstanceChecker>( lockFileName, GetKicadLockFilePath() );
if( p->IsAnotherRunning() )
p = nullptr;
return p;
}
wxString GetKicadLockFilePath()
{
wxFileName lockpath;
lockpath.AssignDir( wxGetHomeDir() ); // Default wx behavior
#if defined( __WXMAC__ )
// In OSX use the standard per user cache directory
lockpath.AppendDir( "Library" );
lockpath.AppendDir( "Caches" );
lockpath.AppendDir( "kicad" );
#elif defined( __UNIX__ )
wxString envstr;
// Try first the standard XDG_RUNTIME_DIR, falling back to XDG_CACHE_HOME
if( wxGetEnv( "XDG_RUNTIME_DIR", &envstr ) && !envstr.IsEmpty() )
{
lockpath.AssignDir( envstr );
}
else if( wxGetEnv( "XDG_CACHE_HOME", &envstr ) && !envstr.IsEmpty() )
{
lockpath.AssignDir( envstr );
}
else
{
// If all fails, just use ~/.cache
lockpath.AppendDir( ".cache" );
}
lockpath.AppendDir( wxString::Format( "kicad_v%s", GetMajorMinorVersion() ) );
#endif
#if defined( __WXMAC__ ) || defined( __UNIX__ )
if( !lockpath.DirExists() )
{
// Lockfiles should be only readable by the user
lockpath.Mkdir( 0700, wxPATH_MKDIR_FULL );
}
#endif
return lockpath.GetPath();
}

View File

@ -54,7 +54,6 @@
#include <id.h>
#include <kicad_curl/kicad_curl.h>
#include <kiplatform/policy.h>
#include <lockfile.h>
#include <macros.h>
#include <menus_helpers.h>
#include <paths.h>
@ -916,4 +915,4 @@ void PGM_BASE::HandleException( std::exception_ptr aPtr )
{
wxLogError( wxT( "Unhandled exception of unknown type" ) );
}
}
}

View File

@ -892,7 +892,7 @@ bool SETTINGS_MANAGER::LoadProject( const wxString& aFullPath, bool aSetActive )
return true;
bool readOnly = false;
std::unique_ptr<wxSingleInstanceChecker> lockFile = ::LockFile( fullPath );
LOCKFILE lockFile( fullPath );
if( !lockFile )
{
@ -929,7 +929,7 @@ bool SETTINGS_MANAGER::LoadProject( const wxString& aFullPath, bool aSetActive )
project->SetReadOnly( readOnly || project->GetProjectFile().IsReadOnly() );
if( lockFile )
m_project_lock.reset( lockFile.release() );
m_project_lock.reset( new LOCKFILE( std::move( lockFile ) ) );
}
m_projects_list.push_back( std::move( project ) );

View File

@ -34,6 +34,7 @@
#include <id.h>
#include <kiface_base.h>
#include <kiplatform/app.h>
#include <lockfile.h>
#include <pgm_base.h>
#include <profile.h>
#include <project/project_file.h>
@ -103,6 +104,8 @@ bool SCH_EDIT_FRAME::OpenProjectFiles( const std::vector<wxString>& aFileSet, in
if( !OverrideLock( this, msg ) )
return false;
m_file_checker->OverrideLock( false );
}
if( !AskToSaveChanges() )
@ -1221,10 +1224,13 @@ bool SCH_EDIT_FRAME::importFile( const wxString& aFileName, int aFileType )
if( !LockFile( aFileName ) )
{
wxString msg;
msg.Printf( _( "Schematic '%s' is already open." ), filename.GetFullName() );
msg.Printf( _( "Schematic '%s' is already open by '%s' at '%s'." ), aFileName,
m_file_checker->GetUsername(), m_file_checker->GetHostname() );
if( !OverrideLock( this, msg ) )
return false;
m_file_checker->OverrideLock();
}
try

View File

@ -38,6 +38,7 @@ class EDA_ITEM;
class wxSingleInstanceChecker;
class ACTION_TOOLBAR;
class COLOR_SETTINGS;
class LOCKFILE;
class TOOL_MENU;
class APP_SETTINGS_BASE;
class wxFindReplaceData;
@ -489,7 +490,7 @@ protected:
wxSocketServer* m_socketServer;
///< Prevents opening same file multiple times.
std::unique_ptr<wxSingleInstanceChecker> m_file_checker;
std::unique_ptr<LOCKFILE> m_file_checker;
COLOR4D m_gridColor; // Grid color
COLOR4D m_drawBgColor; // The background color of the draw canvas; BLACK for

View File

@ -1,7 +1,7 @@
/*
* This program source code file is part of KiCad, a free EDA CAD application.
*
* Copyright (C) 2017-2020 KiCad Developers, see AUTHORS.txt for contributors.
* Copyright (C) 2023 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
@ -29,20 +29,255 @@
#ifndef INCLUDE__LOCK_FILE_H_
#define INCLUDE__LOCK_FILE_H_
#include <wx/string.h>
#include <memory>
#include <wx/wx.h>
#include <wx/file.h>
#include <wx/filefn.h>
#include <wx/log.h>
#include <wx/filename.h>
#include <nlohmann/json.hpp>
class wxSingleInstanceChecker;
#define LCK "KICAD_LOCKING"
/**
* Test to see if \a aFileName can be locked (is not already locked) and only then
* returns a wxSingleInstanceChecker protecting aFileName.
*/
std::unique_ptr<wxSingleInstanceChecker> LockFile( const wxString& aFileName );
class LOCKFILE
{
public:
LOCKFILE( const wxString &filename, bool aRemoveOnRelease = true ) :
m_originalFile( filename ), m_fileCreated( false ), m_status( false ),
m_removeOnRelease( aRemoveOnRelease ), m_errorMsg( "" )
{
if( filename.IsEmpty() )
return;
wxFileName fn( filename );
fn.SetName( "~" + fn.GetName() );
fn.SetExt( fn.GetExt() + ".lck" );
m_lockFilename = fn.GetFullPath();
wxFile file;
try
{
bool lock_success = false;
bool rw_success = false;
{
wxLogNull suppressExpectedErrorMessages;
lock_success = file.Open( m_lockFilename, wxFile::write_excl );
if( !lock_success )
rw_success = file.Open( m_lockFilename, wxFile::read );
}
if( lock_success )
{
// Lock file doesn't exist, create one
m_fileCreated = true;
m_status = true;
m_username = wxGetUserId();
m_hostname = wxGetHostName();
nlohmann::json j;
j["username"] = std::string( m_username.mb_str() );
j["hostname"] = std::string( m_hostname.mb_str() );
std::string lock_info = j.dump();
file.Write( lock_info );
file.Close();
wxLogTrace( LCK, "Locked %s", filename );
}
else if( rw_success )
{
// Lock file already exists, read the details
wxString lock_info;
file.ReadAll( &lock_info );
nlohmann::json j = nlohmann::json::parse( std::string( lock_info.mb_str() ) );
m_username = wxString( j["username"].get<std::string>() );
m_hostname = wxString( j["hostname"].get<std::string>() );
file.Close();
m_errorMsg = _( "Lock file already exists" );
wxLogTrace( LCK, "Existing Lock for %s", filename );
}
else
{
throw;
}
}
catch( std::exception& e )
{
wxLogError( "Got an error trying to lock %s: %s", filename, e.what() );
// Delete lock file if it was created above but we threw an exception somehow
if( m_fileCreated )
{
wxRemoveFile( m_lockFilename );
m_fileCreated = false; // Reset the flag since file has been deleted manually
}
m_errorMsg = _( "Failed to access lock file" );
m_status = false;
}
}
~LOCKFILE()
{
UnlockFile();
}
/**
* Unlocks and removes the file from the filesystem as long as we still own it.
*/
void UnlockFile()
{
wxLogTrace( LCK, "Unlocking %s", m_lockFilename );
// Delete lock file only if the file was created in the constructor and if the file contains the correct user and host names
if( m_fileCreated && checkUserAndHost() )
{
if( m_removeOnRelease )
wxRemoveFile( m_lockFilename );
m_fileCreated = false; // Reset the flag since file has been deleted manually
m_status = false;
m_errorMsg = wxEmptyString;
}
}
/**
* Forces the lock, overwriting the data that existed already
* @return True if we successfully overrode the lock
*/
bool OverrideLock( bool aRemoveOnRelease = true )
{
wxLogTrace( LCK, "Overriding lock on %s", m_lockFilename );
if( !m_fileCreated )
{
try
{
wxFile file;
bool success = false;
{
wxLogNull suppressExpectedErrorMessages;
success = file.Open( m_lockFilename, wxFile::write );
}
if( success )
{
m_username = wxGetUserId();
m_hostname = wxGetHostName();
nlohmann::json j;
j["username"] = std::string( m_username.mb_str() );
j["hostname"] = std::string( m_hostname.mb_str() );
std::string lock_info = j.dump();
file.Write( lock_info );
file.Close();
m_fileCreated = true;
m_status = true;
m_removeOnRelease = aRemoveOnRelease;
m_errorMsg = wxEmptyString;
wxLogTrace( LCK, "Successfully overrode lock on %s", m_lockFilename );
return true;
}
return false;
}
catch( std::exception& e )
{
wxLogError( "Got exception trying to override lock on %s: %s",
m_lockFilename, e.what() );
return false;
}
}
else
{
wxLogTrace( LCK, "Upgraded lock on %s to delete on release", m_lockFilename );
m_removeOnRelease = aRemoveOnRelease;
}
return true;
}
/**
* @return Current username. If we own the lock, this is us. Otherwise, this is the user that does own it
*/
wxString GetUsername(){ return m_username; }
/**
* @return Current hostname. If we own the lock this is our computer. Otherwise, this is the computer that does
*/
wxString GetHostname(){ return m_hostname; }
/**
* @return Last error message generated
*/
wxString GetErrorMsg(){ return m_errorMsg; }
bool Locked() const
{
return m_fileCreated;
}
bool Valid() const
{
return m_status;
}
explicit operator bool() const
{
return m_status;
}
private:
wxString m_originalFile;
wxString m_lockFilename;
wxString m_username;
wxString m_hostname;
bool m_fileCreated;
bool m_status;
bool m_removeOnRelease;
wxString m_errorMsg;
bool checkUserAndHost()
{
wxFileName fileName( m_lockFilename );
if( !fileName.FileExists() )
{
wxLogTrace
( LCK, "File does not exist: %s", m_lockFilename );
return false;
}
wxFile file;
try
{
if( file.Open( m_lockFilename, wxFile::read ) )
{
wxString lock_info;
file.ReadAll( &lock_info );
nlohmann::json j = nlohmann::json::parse( std::string( lock_info.mb_str() ) );
if( m_username == wxString( j["username"].get<std::string>() )
&& m_hostname == wxString( j["hostname"].get<std::string>() ) )
{
wxLogTrace
( LCK, "User and host match for lock %s", m_lockFilename );
return true;
}
}
}
catch( std::exception &e )
{
wxLogError
( "Got exception trying to check user/host for lock on %s: %s", m_lockFilename,
e.what() );
}
wxLogTrace
( LCK, "User and host DID NOT match for lock %s", m_lockFilename );
return false;
}
};
/**
* @return A wxString containing the path for lockfiles in Kicad.
*/
wxString GetKicadLockFilePath();
#endif // INCLUDE__LOCK_FILE_H_

View File

@ -34,6 +34,7 @@ class PROJECT;
class PROJECT_FILE;
class REPORTER;
class wxSingleInstanceChecker;
class LOCKFILE;
/// Project settings path will be <projectname> + this
@ -447,7 +448,7 @@ private:
std::map<wxString, PROJECT_FILE*> m_project_files;
/// Lock for loaded project (expand to multiple once we support MDI)
std::unique_ptr<wxSingleInstanceChecker> m_project_lock;
std::unique_ptr<LOCKFILE> m_project_lock;
static wxString backupDateTimeFormat;
};

View File

@ -602,14 +602,17 @@ bool PCB_EDIT_FRAME::OpenProjectFiles( const std::vector<wxString>& aFileSet, in
// We insist on caller sending us an absolute path, if it does not, we say it's a bug.
wxASSERT_MSG( wx_filename.IsAbsolute(), wxT( "Path is not absolute!" ) );
std::unique_ptr<wxSingleInstanceChecker> lockFile = ::LockFile( fullFileName );
std::unique_ptr<LOCKFILE> lock = std::make_unique<LOCKFILE>( fullFileName );
if( !lockFile || lockFile->IsAnotherRunning() )
if( !lock->Locked() )
{
msg.Printf( _( "PCB '%s' is already open." ), wx_filename.GetFullName() );
msg.Printf( _( "PCB '%s' is already open by '%s' at '%s'." ), wx_filename.GetFullName(),
lock->GetUsername(), lock->GetHostname() );
if( !OverrideLock( this, msg ) )
return false;
lock->OverrideLock();
}
if( IsContentModified() )
@ -624,9 +627,6 @@ bool PCB_EDIT_FRAME::OpenProjectFiles( const std::vector<wxString>& aFileSet, in
}
}
// Release the lock file, until the new file is actually loaded
ReleaseFile();
wxFileName pro = fullFileName;
pro.SetExt( ProjectFileExtension );
@ -942,7 +942,7 @@ bool PCB_EDIT_FRAME::OpenProjectFiles( const std::vector<wxString>& aFileSet, in
}
// Lock the file newly opened:
m_file_checker.reset( lockFile.release() );
m_file_checker.reset( lock.release() );
if( !converted )
UpdateFileHistory( GetBoard()->GetFileName() );