Move history autosave Prettify, file I/O, and git ops to background thread
Savers now serialize to in-memory HISTORY_FILE_DATA on the UI thread, then Prettify + file writes + git stage/diff/commit run on a background thread via GetKiCadThreadPool(). An atomic skip-if-busy flag prevents overlapping saves; WaitForPendingSave() ensures clean shutdown. Extracted FormatBoardToFormatter and FormatSchematicToFormatter so SaveToHistory can serialize without touching disk. PROJECT savers use sourcePath for file-copy semantics since project files are small.
This commit is contained in:
+107
-49
@@ -23,9 +23,11 @@
|
||||
|
||||
#include <local_history.h>
|
||||
#include <history_lock.h>
|
||||
#include <io/kicad/kicad_io_utils.h>
|
||||
#include <lockfile.h>
|
||||
#include <settings/common_settings.h>
|
||||
#include <pgm_base.h>
|
||||
#include <thread_pool.h>
|
||||
#include <trace_helpers.h>
|
||||
#include <wildcards_and_files_ext.h>
|
||||
#include <confirm.h>
|
||||
@@ -62,6 +64,7 @@ LOCAL_HISTORY::LOCAL_HISTORY()
|
||||
|
||||
LOCAL_HISTORY::~LOCAL_HISTORY()
|
||||
{
|
||||
WaitForPendingSave();
|
||||
}
|
||||
|
||||
void LOCAL_HISTORY::NoteFileChange( const wxString& aFile )
|
||||
@@ -75,8 +78,9 @@ void LOCAL_HISTORY::NoteFileChange( const wxString& aFile )
|
||||
}
|
||||
|
||||
|
||||
void LOCAL_HISTORY::RegisterSaver( const void* aSaverObject,
|
||||
const std::function<void( const wxString&, std::vector<wxString>& )>& aSaver )
|
||||
void LOCAL_HISTORY::RegisterSaver(
|
||||
const void* aSaverObject,
|
||||
const std::function<void( const wxString&, std::vector<HISTORY_FILE_DATA>& )>& aSaver )
|
||||
{
|
||||
if( m_savers.find( aSaverObject ) != m_savers.end() )
|
||||
{
|
||||
@@ -91,6 +95,8 @@ void LOCAL_HISTORY::RegisterSaver( const void* aSaverObject,
|
||||
|
||||
void LOCAL_HISTORY::UnregisterSaver( const void* aSaverObject )
|
||||
{
|
||||
WaitForPendingSave();
|
||||
|
||||
auto it = m_savers.find( aSaverObject );
|
||||
|
||||
if( it != m_savers.end() )
|
||||
@@ -103,6 +109,7 @@ void LOCAL_HISTORY::UnregisterSaver( const void* aSaverObject )
|
||||
|
||||
void LOCAL_HISTORY::ClearAllSavers()
|
||||
{
|
||||
WaitForPendingSave();
|
||||
m_savers.clear();
|
||||
wxLogTrace( traceAutoSave, wxS("[history] Cleared all savers") );
|
||||
}
|
||||
@@ -125,88 +132,129 @@ bool LOCAL_HISTORY::RunRegisteredSaversAndCommit( const wxString& aProjectPath,
|
||||
return false;
|
||||
}
|
||||
|
||||
std::vector<wxString> files;
|
||||
// Skip if previous background save is still running
|
||||
if( m_saveInProgress.load( std::memory_order_acquire ) )
|
||||
{
|
||||
wxLogTrace( traceAutoSave, wxS("[history] previous save still in progress; skipping cycle") );
|
||||
return false;
|
||||
}
|
||||
|
||||
// Phase 1 (UI thread): call savers to collect serialized data
|
||||
std::vector<HISTORY_FILE_DATA> fileData;
|
||||
|
||||
for( const auto& [saverObject, saver] : m_savers )
|
||||
{
|
||||
size_t before = files.size();
|
||||
saver( aProjectPath, files );
|
||||
wxLogTrace( traceAutoSave, wxS("[history] saver %p added %zu files (total=%zu)"),
|
||||
saverObject, files.size() - before, files.size() );
|
||||
size_t before = fileData.size();
|
||||
saver( aProjectPath, fileData );
|
||||
wxLogTrace( traceAutoSave, wxS("[history] saver %p produced %zu entries (total=%zu)"),
|
||||
saverObject, fileData.size() - before, fileData.size() );
|
||||
}
|
||||
|
||||
// Filter out any files not within the project directory
|
||||
// Filter out any entries not within the project directory
|
||||
wxString projectDir = aProjectPath;
|
||||
if( !projectDir.EndsWith( wxFileName::GetPathSeparator() ) )
|
||||
projectDir += wxFileName::GetPathSeparator();
|
||||
|
||||
auto it = std::remove_if( files.begin(), files.end(),
|
||||
[&projectDir]( const wxString& file )
|
||||
auto it = std::remove_if( fileData.begin(), fileData.end(),
|
||||
[&projectDir]( const HISTORY_FILE_DATA& entry )
|
||||
{
|
||||
if( !file.StartsWith( projectDir ) )
|
||||
if( !entry.path.StartsWith( projectDir ) )
|
||||
{
|
||||
wxLogTrace( traceAutoSave, wxS("[history] filtered out file outside project: %s"), file );
|
||||
wxLogTrace( traceAutoSave, wxS("[history] filtered out entry outside project: %s"), entry.path );
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
} );
|
||||
files.erase( it, files.end() );
|
||||
fileData.erase( it, fileData.end() );
|
||||
|
||||
if( files.empty() )
|
||||
if( fileData.empty() )
|
||||
{
|
||||
wxLogTrace( traceAutoSave, wxS("[history] saver set produced no files; skipping") );
|
||||
wxLogTrace( traceAutoSave, wxS("[history] saver set produced no entries; skipping") );
|
||||
return false;
|
||||
}
|
||||
|
||||
// Phase 2: submit Prettify + file I/O + git to background thread
|
||||
m_saveInProgress.store( true, std::memory_order_release );
|
||||
|
||||
m_pendingFuture = GetKiCadThreadPool().submit_task(
|
||||
[this, projectPath = aProjectPath, title = aTitle,
|
||||
data = std::move( fileData )]() mutable -> bool
|
||||
{
|
||||
bool result = commitInBackground( projectPath, title, data );
|
||||
m_saveInProgress.store( false, std::memory_order_release );
|
||||
return result;
|
||||
} );
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
bool LOCAL_HISTORY::commitInBackground( const wxString& aProjectPath, const wxString& aTitle,
|
||||
const std::vector<HISTORY_FILE_DATA>& aFileData )
|
||||
{
|
||||
wxLogTrace( traceAutoSave, wxS("[history] background: writing %zu entries for '%s'"),
|
||||
aFileData.size(), aProjectPath );
|
||||
|
||||
wxString hist = historyPath( aProjectPath );
|
||||
|
||||
// Write files to the .history mirror
|
||||
for( const HISTORY_FILE_DATA& entry : aFileData )
|
||||
{
|
||||
if( !entry.content.empty() )
|
||||
{
|
||||
std::string buf = entry.content;
|
||||
|
||||
if( entry.prettify )
|
||||
KICAD_FORMAT::Prettify( buf, entry.formatMode );
|
||||
|
||||
wxFFile fp( entry.path, wxS( "wb" ) );
|
||||
|
||||
if( fp.IsOpened() )
|
||||
{
|
||||
fp.Write( buf.data(), buf.size() );
|
||||
fp.Close();
|
||||
wxLogTrace( traceAutoSave, wxS("[history] background: wrote %zu bytes to '%s'"),
|
||||
buf.size(), entry.path );
|
||||
}
|
||||
else
|
||||
{
|
||||
wxLogTrace( traceAutoSave, wxS("[history] background: failed to open '%s' for writing"),
|
||||
entry.path );
|
||||
}
|
||||
}
|
||||
else if( !entry.sourcePath.IsEmpty() )
|
||||
{
|
||||
wxCopyFile( entry.sourcePath, entry.path, true );
|
||||
wxLogTrace( traceAutoSave, wxS("[history] background: copied '%s' -> '%s'"),
|
||||
entry.sourcePath, entry.path );
|
||||
}
|
||||
}
|
||||
|
||||
// Acquire locks using hybrid locking strategy
|
||||
HISTORY_LOCK_MANAGER lock( aProjectPath );
|
||||
|
||||
if( !lock.IsLocked() )
|
||||
{
|
||||
wxLogTrace( traceAutoSave, wxS("[history] failed to acquire lock: %s"), lock.GetLockError() );
|
||||
wxLogTrace( traceAutoSave, wxS("[history] background: failed to acquire lock: %s"), lock.GetLockError() );
|
||||
return false;
|
||||
}
|
||||
|
||||
git_repository* repo = lock.GetRepository();
|
||||
git_index* index = lock.GetIndex();
|
||||
wxString hist = historyPath( aProjectPath );
|
||||
|
||||
// Stage selected files (mirroring logic from CommitSnapshot but limited to given files)
|
||||
for( const wxString& file : files )
|
||||
// Stage all written files
|
||||
for( const HISTORY_FILE_DATA& entry : aFileData )
|
||||
{
|
||||
wxFileName src( file );
|
||||
wxFileName src( entry.path );
|
||||
|
||||
if( !src.FileExists() )
|
||||
{
|
||||
wxLogTrace( traceAutoSave, wxS("[history] skip missing '%s'"), file );
|
||||
continue;
|
||||
}
|
||||
|
||||
// If saver already produced a path inside history mirror, just stage it.
|
||||
if( src.GetFullPath().StartsWith( hist + wxFILE_SEP_PATH ) )
|
||||
{
|
||||
std::string relHist = src.GetFullPath().ToStdString().substr( hist.length() + 1 );
|
||||
git_index_add_bypath( index, relHist.c_str() );
|
||||
wxLogTrace( traceAutoSave, wxS("[history] staged pre-mirrored '%s'"), file );
|
||||
continue;
|
||||
}
|
||||
|
||||
wxString relStr;
|
||||
wxString proj = wxFileName( aProjectPath ).GetFullPath();
|
||||
|
||||
if( src.GetFullPath().StartsWith( proj + wxFILE_SEP_PATH ) )
|
||||
relStr = src.GetFullPath().Mid( proj.length() + 1 );
|
||||
else
|
||||
relStr = src.GetFullName();
|
||||
|
||||
wxFileName dst( hist + wxFILE_SEP_PATH + relStr );
|
||||
wxFileName dstDir( dst );
|
||||
dstDir.SetFullName( wxEmptyString );
|
||||
wxFileName::Mkdir( dstDir.GetPath(), 0777, wxPATH_MKDIR_FULL );
|
||||
wxCopyFile( src.GetFullPath(), dst.GetFullPath(), true );
|
||||
std::string rel = dst.GetFullPath().ToStdString().substr( hist.length() + 1 );
|
||||
git_index_add_bypath( index, rel.c_str() );
|
||||
wxLogTrace( traceAutoSave, wxS("[history] staged '%s' as '%s'"), file, wxString::FromUTF8( rel ) );
|
||||
}
|
||||
|
||||
// Compare index to HEAD; if no diff -> abort to avoid empty commit.
|
||||
@@ -225,7 +273,7 @@ bool LOCAL_HISTORY::RunRegisteredSaversAndCommit( const wxString& aProjectPath,
|
||||
{
|
||||
if( head_tree ) git_tree_free( head_tree );
|
||||
if( head_commit ) git_commit_free( head_commit );
|
||||
wxLogTrace( traceAutoSave, wxS("[history] failed to write index tree" ) );
|
||||
wxLogTrace( traceAutoSave, wxS("[history] background: failed to write index tree" ) );
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -241,7 +289,7 @@ bool LOCAL_HISTORY::RunRegisteredSaversAndCommit( const wxString& aProjectPath,
|
||||
if( git_diff_tree_to_tree( &diff, repo, head_tree, indexTree.get(), nullptr ) == 0 )
|
||||
{
|
||||
hasChanges = git_diff_num_deltas( diff ) > 0;
|
||||
wxLogTrace( traceAutoSave, wxS("[history] diff deltas=%u"), (unsigned) git_diff_num_deltas( diff ) );
|
||||
wxLogTrace( traceAutoSave, wxS("[history] background: diff deltas=%u"), (unsigned) git_diff_num_deltas( diff ) );
|
||||
git_diff_free( diff );
|
||||
}
|
||||
}
|
||||
@@ -251,7 +299,7 @@ bool LOCAL_HISTORY::RunRegisteredSaversAndCommit( const wxString& aProjectPath,
|
||||
|
||||
if( !hasChanges )
|
||||
{
|
||||
wxLogTrace( traceAutoSave, wxS("[history] no changes detected; no commit") );
|
||||
wxLogTrace( traceAutoSave, wxS("[history] background: no changes detected; no commit") );
|
||||
return false; // Nothing new; skip commit.
|
||||
}
|
||||
|
||||
@@ -278,10 +326,10 @@ bool LOCAL_HISTORY::RunRegisteredSaversAndCommit( const wxString& aProjectPath,
|
||||
parents ? &constParent : nullptr );
|
||||
|
||||
if( rc == 0 )
|
||||
wxLogTrace( traceAutoSave, wxS("[history] commit created %s (%s files=%zu)"),
|
||||
wxString::FromUTF8( git_oid_tostr_s( &commit_id ) ), msg, files.size() );
|
||||
wxLogTrace( traceAutoSave, wxS("[history] background: commit created %s (%s entries=%zu)"),
|
||||
wxString::FromUTF8( git_oid_tostr_s( &commit_id ) ), msg, aFileData.size() );
|
||||
else
|
||||
wxLogTrace( traceAutoSave, wxS("[history] commit failed rc=%d"), rc );
|
||||
wxLogTrace( traceAutoSave, wxS("[history] background: commit failed rc=%d"), rc );
|
||||
|
||||
if( parent ) git_commit_free( parent );
|
||||
|
||||
@@ -290,6 +338,16 @@ bool LOCAL_HISTORY::RunRegisteredSaversAndCommit( const wxString& aProjectPath,
|
||||
}
|
||||
|
||||
|
||||
void LOCAL_HISTORY::WaitForPendingSave()
|
||||
{
|
||||
if( m_pendingFuture.valid() )
|
||||
{
|
||||
wxLogTrace( traceAutoSave, wxS("[history] waiting for pending background save") );
|
||||
m_pendingFuture.get();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
bool LOCAL_HISTORY::CommitPending()
|
||||
{
|
||||
std::vector<wxString> files( m_pendingFiles.begin(), m_pendingFiles.end() );
|
||||
|
||||
+12
-6
@@ -458,7 +458,7 @@ void PROJECT::SetProjectLock( LOCKFILE* aLockFile )
|
||||
}
|
||||
|
||||
|
||||
void PROJECT::SaveToHistory( const wxString& aProjectPath, std::vector<wxString>& aFiles )
|
||||
void PROJECT::SaveToHistory( const wxString& aProjectPath, std::vector<HISTORY_FILE_DATA>& aFileData )
|
||||
{
|
||||
wxString projectFile = GetProjectFullName();
|
||||
|
||||
@@ -486,11 +486,14 @@ void PROJECT::SaveToHistory( const wxString& aProjectPath, std::vector<wxString>
|
||||
return;
|
||||
}
|
||||
|
||||
// Save project file (.kicad_pro)
|
||||
// Save project file (.kicad_pro) via file-copy
|
||||
wxFileName historyProFile( historyDir.GetFullPath(), projectFn.GetName(),
|
||||
projectFn.GetExt() );
|
||||
wxCopyFile( projectFile, historyProFile.GetFullPath(), true );
|
||||
aFiles.push_back( historyProFile.GetFullPath() );
|
||||
|
||||
HISTORY_FILE_DATA proEntry;
|
||||
proEntry.path = historyProFile.GetFullPath();
|
||||
proEntry.sourcePath = projectFile;
|
||||
aFileData.push_back( std::move( proEntry ) );
|
||||
|
||||
// Save project local settings (.kicad_prl) if it exists
|
||||
wxFileName prlFile( projectFn.GetPath(), projectFn.GetName(), FILEEXT::ProjectLocalSettingsFileExtension );
|
||||
@@ -499,7 +502,10 @@ void PROJECT::SaveToHistory( const wxString& aProjectPath, std::vector<wxString>
|
||||
{
|
||||
wxFileName historyPrlFile( historyDir.GetFullPath(), prlFile.GetName(),
|
||||
prlFile.GetExt() );
|
||||
wxCopyFile( prlFile.GetFullPath(), historyPrlFile.GetFullPath(), true );
|
||||
aFiles.push_back( historyPrlFile.GetFullPath() );
|
||||
|
||||
HISTORY_FILE_DATA prlEntry;
|
||||
prlEntry.path = historyPrlFile.GetFullPath();
|
||||
prlEntry.sourcePath = prlFile.GetFullPath();
|
||||
aFileData.push_back( std::move( prlEntry ) );
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1471,9 +1471,9 @@ void SCH_EDIT_FRAME::ProjectChanged()
|
||||
|
||||
// Register schematic saver for autosave history
|
||||
Kiway().LocalHistory().RegisterSaver( m_schematic,
|
||||
[this]( const wxString& aProjectPath, std::vector<wxString>& aFiles )
|
||||
[this]( const wxString& aProjectPath, std::vector<HISTORY_FILE_DATA>& aFileData )
|
||||
{
|
||||
m_schematic->SaveToHistory( aProjectPath, aFiles );
|
||||
m_schematic->SaveToHistory( aProjectPath, aFileData );
|
||||
} );
|
||||
|
||||
m_designBlocksPane->ProjectChanged();
|
||||
|
||||
@@ -367,8 +367,6 @@ void SCH_IO_KICAD_SEXPR::SaveSchematicFile( const wxString& aFileName, SCH_SHEET
|
||||
}
|
||||
}
|
||||
|
||||
init( aSchematic, aProperties );
|
||||
|
||||
wxFileName fn = aFileName;
|
||||
|
||||
// File names should be absolute. Don't assume everything relative to the project path
|
||||
@@ -376,16 +374,29 @@ void SCH_IO_KICAD_SEXPR::SaveSchematicFile( const wxString& aFileName, SCH_SHEET
|
||||
wxASSERT( fn.IsAbsolute() );
|
||||
|
||||
PRETTIFIED_FILE_OUTPUTFORMATTER formatter( fn.GetFullPath() );
|
||||
|
||||
m_out = &formatter; // no ownership
|
||||
|
||||
Format( aSheet );
|
||||
FormatSchematicToFormatter( &formatter, aSheet, aSchematic, aProperties );
|
||||
|
||||
if( aSheet->GetScreen() )
|
||||
aSheet->GetScreen()->SetFileExists( true );
|
||||
}
|
||||
|
||||
|
||||
void SCH_IO_KICAD_SEXPR::FormatSchematicToFormatter( OUTPUTFORMATTER* aOut, SCH_SHEET* aSheet,
|
||||
SCHEMATIC* aSchematic,
|
||||
const std::map<std::string, UTF8>* aProperties )
|
||||
{
|
||||
wxCHECK_RET( aSheet != nullptr, "NULL SCH_SHEET object." );
|
||||
|
||||
init( aSchematic, aProperties );
|
||||
|
||||
m_out = aOut;
|
||||
|
||||
Format( aSheet );
|
||||
|
||||
m_out = nullptr;
|
||||
}
|
||||
|
||||
|
||||
void SCH_IO_KICAD_SEXPR::Format( SCH_SHEET* aSheet )
|
||||
{
|
||||
wxCHECK_RET( aSheet != nullptr, "NULL SCH_SHEET* object." );
|
||||
|
||||
@@ -102,6 +102,13 @@ public:
|
||||
void SaveSchematicFile( const wxString& aFileName, SCH_SHEET* aSheet, SCHEMATIC* aSchematic,
|
||||
const std::map<std::string, UTF8>* aProperties = nullptr ) override;
|
||||
|
||||
/** Serialize a schematic sheet to an OUTPUTFORMATTER without file I/O or Prettify.
|
||||
* Handles init() and Format().
|
||||
* Skips GroupsSanityCheck and SetFileExists side effects. */
|
||||
void FormatSchematicToFormatter( OUTPUTFORMATTER* aOut, SCH_SHEET* aSheet,
|
||||
SCHEMATIC* aSchematic,
|
||||
const std::map<std::string, UTF8>* aProperties = nullptr );
|
||||
|
||||
void Format( SCH_SHEET* aSheet );
|
||||
|
||||
void Format( SCH_SELECTION* aSelection, SCH_SHEET_PATH* aSelectionPath,
|
||||
|
||||
+23
-9
@@ -57,7 +57,9 @@
|
||||
#include <tool/tool_manager.h>
|
||||
#include <undo_redo_container.h>
|
||||
#include <local_history.h>
|
||||
#include <richio.h>
|
||||
#include <sch_io/sch_io_mgr.h>
|
||||
#include <sch_io/kicad_sexpr/sch_io_kicad_sexpr.h>
|
||||
#include <sch_io/sch_io.h>
|
||||
|
||||
#include <wx/log.h>
|
||||
@@ -2355,7 +2357,7 @@ void SCHEMATIC::LoadVariants()
|
||||
}
|
||||
|
||||
|
||||
void SCHEMATIC::SaveToHistory( const wxString& aProjectPath, std::vector<wxString>& aFiles )
|
||||
void SCHEMATIC::SaveToHistory( const wxString& aProjectPath, std::vector<HISTORY_FILE_DATA>& aFileData )
|
||||
{
|
||||
if( !IsValid() )
|
||||
return;
|
||||
@@ -2384,8 +2386,12 @@ void SCHEMATIC::SaveToHistory( const wxString& aProjectPath, std::vector<wxStrin
|
||||
// Iterate full schematic hierarchy (all sheets & their screens).
|
||||
SCH_SHEET_LIST sheetList = Hierarchy();
|
||||
|
||||
// Acquire plugin once.
|
||||
IO_RELEASER<SCH_IO> pi( SCH_IO_MGR::FindPlugin( SCH_IO_MGR::SCH_KICAD ) );
|
||||
SCH_IO_KICAD_SEXPR pi;
|
||||
|
||||
KICAD_FORMAT::FORMAT_MODE mode = KICAD_FORMAT::FORMAT_MODE::NORMAL;
|
||||
|
||||
if( ADVANCED_CFG::GetCfg().m_CompactSave )
|
||||
mode = KICAD_FORMAT::FORMAT_MODE::COMPACT_TEXT_PROPERTIES;
|
||||
|
||||
for( const SCH_SHEET_PATH& path : sheetList )
|
||||
{
|
||||
@@ -2415,7 +2421,7 @@ void SCHEMATIC::SaveToHistory( const wxString& aProjectPath, std::vector<wxStrin
|
||||
else
|
||||
dst.SetPath( historyRootPath );
|
||||
|
||||
// Ensure destination directory exists
|
||||
// Ensure destination directory exists on UI thread
|
||||
wxFileName dstDir( dst );
|
||||
dstDir.SetFullName( wxEmptyString );
|
||||
|
||||
@@ -2424,14 +2430,22 @@ void SCHEMATIC::SaveToHistory( const wxString& aProjectPath, std::vector<wxStrin
|
||||
|
||||
try
|
||||
{
|
||||
pi->SaveSchematicFile( dst.GetFullPath(), sheet, this );
|
||||
aFiles.push_back( dst.GetFullPath() );
|
||||
wxLogTrace( traceAutoSave, wxS( "[history] sch saver exported sheet '%s' -> '%s'" ), absPath,
|
||||
dst.GetFullPath() );
|
||||
STRING_FORMATTER formatter;
|
||||
pi.FormatSchematicToFormatter( &formatter, sheet, this );
|
||||
|
||||
HISTORY_FILE_DATA entry;
|
||||
entry.path = dst.GetFullPath();
|
||||
entry.content = std::move( formatter.MutableString() );
|
||||
entry.prettify = true;
|
||||
entry.formatMode = mode;
|
||||
aFileData.push_back( std::move( entry ) );
|
||||
|
||||
wxLogTrace( traceAutoSave, wxS( "[history] sch saver serialized %zu bytes for '%s' -> '%s'" ),
|
||||
aFileData.back().content.size(), absPath, dst.GetFullPath() );
|
||||
}
|
||||
catch( const IO_ERROR& ioe )
|
||||
{
|
||||
wxLogTrace( traceAutoSave, wxS( "[history] sch saver export failed for '%s': %s" ), absPath,
|
||||
wxLogTrace( traceAutoSave, wxS( "[history] sch saver serialize failed for '%s': %s" ), absPath,
|
||||
wxString::FromUTF8( ioe.What() ) );
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
#include <project.h>
|
||||
|
||||
|
||||
struct HISTORY_FILE_DATA;
|
||||
class BUS_ALIAS;
|
||||
class CONNECTION_GRAPH;
|
||||
class EDA_BASE_FRAME;
|
||||
@@ -533,14 +534,15 @@ public:
|
||||
PROJECT::ELEM ProjectElementType() override { return PROJECT::ELEM::SCHEMATIC; }
|
||||
|
||||
/**
|
||||
* Save schematic files to the .history directory.
|
||||
* Serialize schematic sheets into HISTORY_FILE_DATA for non-blocking history commit.
|
||||
*
|
||||
* This method is used as a saver callback for LOCAL_HISTORY during autosave operations.
|
||||
* Serialization runs on the UI thread; Prettify and file I/O happen in the background.
|
||||
*
|
||||
* @param aProjectPath The path to check against this schematic's project path
|
||||
* @param aFiles Output vector to append absolute file paths for history inclusion
|
||||
* @param aFileData Output vector to append serialized data for history inclusion
|
||||
*/
|
||||
void SaveToHistory( const wxString& aProjectPath, std::vector<wxString>& aFiles );
|
||||
void SaveToHistory( const wxString& aProjectPath, std::vector<HISTORY_FILE_DATA>& aFileData );
|
||||
|
||||
private:
|
||||
friend class SCH_EDIT_FRAME;
|
||||
|
||||
+36
-5
@@ -24,13 +24,32 @@
|
||||
#pragma once
|
||||
|
||||
#include <kicommon.h>
|
||||
#include <io/kicad/kicad_io_utils.h>
|
||||
|
||||
#include <atomic>
|
||||
#include <future>
|
||||
#include <vector>
|
||||
#include <set>
|
||||
#include <map>
|
||||
#include <functional>
|
||||
#include <string>
|
||||
#include <wx/string.h>
|
||||
#include <wx/window.h>
|
||||
|
||||
|
||||
/**
|
||||
* Data produced by a registered saver on the UI thread, consumed by the background commit thread.
|
||||
* Each entry represents one file to write into the .history mirror.
|
||||
*/
|
||||
struct KICOMMON_API HISTORY_FILE_DATA
|
||||
{
|
||||
wxString path; ///< Destination inside .history/
|
||||
std::string content; ///< Serialized content (mutually exclusive with sourcePath)
|
||||
wxString sourcePath; ///< For file-copy savers (small files like .kicad_pro)
|
||||
bool prettify = false;
|
||||
KICAD_FORMAT::FORMAT_MODE formatMode = KICAD_FORMAT::FORMAT_MODE::NORMAL;
|
||||
};
|
||||
|
||||
/**
|
||||
* Simple local history manager built on libgit2. Stores history for project files in
|
||||
* a hidden .history git repository within the project directory.
|
||||
@@ -55,12 +74,13 @@ public:
|
||||
bool CommitFullProjectSnapshot( const wxString& aProjectPath, const wxString& aTitle );
|
||||
|
||||
/** Register a saver callback invoked during autosave history commits.
|
||||
* The callback receives the project path and should append absolute file paths
|
||||
* (within that project) to aFiles for inclusion.
|
||||
* The callback receives the project path and should populate aFileData with
|
||||
* serialized content or source paths for inclusion.
|
||||
* @param aSaverObject Unique object pointer identifier for this saver (to prevent duplicate registration)
|
||||
* @param aSaver The saver callback function */
|
||||
void RegisterSaver( const void* aSaverObject,
|
||||
const std::function<void( const wxString&, std::vector<wxString>& )>& aSaver );
|
||||
void RegisterSaver(
|
||||
const void* aSaverObject,
|
||||
const std::function<void( const wxString&, std::vector<HISTORY_FILE_DATA>& )>& aSaver );
|
||||
|
||||
/** Unregister a previously registered saver callback.
|
||||
* @param aSaverObject The object pointer that was used to register the saver */
|
||||
@@ -106,7 +126,18 @@ public:
|
||||
void ShowRestoreDialog( const wxString& aProjectPath, wxWindow* aParent );
|
||||
|
||||
private:
|
||||
/** Execute file writes and git commit on a background thread. */
|
||||
bool commitInBackground( const wxString& aProjectPath, const wxString& aTitle,
|
||||
const std::vector<HISTORY_FILE_DATA>& aFileData );
|
||||
|
||||
/** Block until any pending background save completes. */
|
||||
void WaitForPendingSave();
|
||||
|
||||
std::set<wxString> m_pendingFiles;
|
||||
std::map<const void*, std::function<void(const wxString&, std::vector<wxString>&)>> m_savers;
|
||||
std::map<const void*,
|
||||
std::function<void( const wxString&, std::vector<HISTORY_FILE_DATA>& )>> m_savers;
|
||||
|
||||
std::atomic<bool> m_saveInProgress{ false };
|
||||
std::future<bool> m_pendingFuture;
|
||||
};
|
||||
|
||||
|
||||
+5
-3
@@ -43,6 +43,7 @@
|
||||
/// default name for nameless projects
|
||||
#define NAMELESS_PROJECT _( "untitled" )
|
||||
|
||||
struct HISTORY_FILE_DATA;
|
||||
class DESIGN_BLOCK_LIBRARY_ADAPTER;
|
||||
class LEGACY_SYMBOL_LIBS;
|
||||
class SEARCH_STACK;
|
||||
@@ -302,14 +303,15 @@ public:
|
||||
LOCKFILE* GetProjectLock() const;
|
||||
|
||||
/**
|
||||
* Save project files (.kicad_pro and .kicad_prl) to the .history directory.
|
||||
* Produce HISTORY_FILE_DATA entries for project files (.kicad_pro and .kicad_prl).
|
||||
*
|
||||
* Uses sourcePath for file-copy semantics since project files are small.
|
||||
* This method is used as a saver callback for LOCAL_HISTORY during autosave operations.
|
||||
*
|
||||
* @param aProjectPath The path to check against this project's path
|
||||
* @param aFiles Output vector to append absolute file paths for history inclusion
|
||||
* @param aFileData Output vector to append file-copy data for history inclusion
|
||||
*/
|
||||
void SaveToHistory( const wxString& aProjectPath, std::vector<wxString>& aFiles );
|
||||
void SaveToHistory( const wxString& aProjectPath, std::vector<HISTORY_FILE_DATA>& aFileData );
|
||||
|
||||
private:
|
||||
friend class SETTINGS_MANAGER; // so that SM can set project path
|
||||
|
||||
@@ -1279,9 +1279,9 @@ void KICAD_MANAGER_FRAME::ProjectChanged()
|
||||
// Register project file saver. Ensures project file participates in
|
||||
// autosave history commits without affecting dirty state.
|
||||
Kiway().LocalHistory().RegisterSaver( &Prj(),
|
||||
[this]( const wxString& aProjectPath, std::vector<wxString>& aFiles )
|
||||
[this]( const wxString& aProjectPath, std::vector<HISTORY_FILE_DATA>& aFileData )
|
||||
{
|
||||
Prj().SaveToHistory( aProjectPath, aFiles );
|
||||
Prj().SaveToHistory( aProjectPath, aFileData );
|
||||
} );
|
||||
}
|
||||
|
||||
|
||||
+23
-8
@@ -78,6 +78,9 @@
|
||||
#include <pcb_board_outline.h>
|
||||
#include <local_history.h>
|
||||
#include <pcb_io/pcb_io_mgr.h>
|
||||
#include <pcb_io/kicad_sexpr/pcb_io_kicad_sexpr.h>
|
||||
#include <advanced_config.h>
|
||||
#include <richio.h>
|
||||
#include <trace_helpers.h>
|
||||
|
||||
// This is an odd place for this, but CvPcb won't link if it's in board_item.cpp like I first
|
||||
@@ -3690,7 +3693,7 @@ int BOARD::GetPadWithCastellatedAttrCount()
|
||||
}
|
||||
|
||||
|
||||
void BOARD::SaveToHistory( const wxString& aProjectPath, std::vector<wxString>& aFiles )
|
||||
void BOARD::SaveToHistory( const wxString& aProjectPath, std::vector<HISTORY_FILE_DATA>& aFileData )
|
||||
{
|
||||
wxString projPath = GetProject()->GetProjectPath();
|
||||
|
||||
@@ -3724,7 +3727,7 @@ void BOARD::SaveToHistory( const wxString& aProjectPath, std::vector<wxString>&
|
||||
historyRoot.AppendDir( wxS( ".history" ) );
|
||||
wxFileName dst( historyRoot.GetPath(), rel );
|
||||
|
||||
// Ensure destination directories exist.
|
||||
// Ensure destination directories exist on the UI thread so the background task can write.
|
||||
wxFileName dstDir( dst );
|
||||
dstDir.SetFullName( wxEmptyString );
|
||||
|
||||
@@ -3733,14 +3736,26 @@ void BOARD::SaveToHistory( const wxString& aProjectPath, std::vector<wxString>&
|
||||
|
||||
try
|
||||
{
|
||||
IO_RELEASER<PCB_IO> pi( PCB_IO_MGR::FindPlugin( PCB_IO_MGR::KICAD_SEXP ) );
|
||||
// Save directly to history mirror path.
|
||||
pi->SaveBoard( dst.GetFullPath(), this, nullptr );
|
||||
aFiles.push_back( dst.GetFullPath() );
|
||||
wxLogTrace( traceAutoSave, wxS( "[history] pcb saver exported '%s'" ), dst.GetFullPath() );
|
||||
PCB_IO_KICAD_SEXPR pi;
|
||||
STRING_FORMATTER formatter;
|
||||
|
||||
pi.FormatBoardToFormatter( &formatter, this, nullptr );
|
||||
|
||||
HISTORY_FILE_DATA entry;
|
||||
entry.path = dst.GetFullPath();
|
||||
entry.content = std::move( formatter.MutableString() );
|
||||
entry.prettify = true;
|
||||
|
||||
if( ADVANCED_CFG::GetCfg().m_CompactSave )
|
||||
entry.formatMode = KICAD_FORMAT::FORMAT_MODE::COMPACT_TEXT_PROPERTIES;
|
||||
|
||||
aFileData.push_back( std::move( entry ) );
|
||||
|
||||
wxLogTrace( traceAutoSave, wxS( "[history] pcb saver serialized %zu bytes for '%s'" ),
|
||||
aFileData.back().content.size(), dst.GetFullPath() );
|
||||
}
|
||||
catch( const IO_ERROR& ioe )
|
||||
{
|
||||
wxLogTrace( traceAutoSave, wxS( "[history] pcb saver export failed: %s" ), wxString::FromUTF8( ioe.What() ) );
|
||||
wxLogTrace( traceAutoSave, wxS( "[history] pcb saver serialize failed: %s" ), wxString::FromUTF8( ioe.What() ) );
|
||||
}
|
||||
}
|
||||
|
||||
+5
-3
@@ -43,6 +43,7 @@
|
||||
#include <project.h>
|
||||
#include <list>
|
||||
|
||||
struct HISTORY_FILE_DATA;
|
||||
class BOARD_DESIGN_SETTINGS;
|
||||
class BOARD_CONNECTED_ITEM;
|
||||
class BOARD_COMMIT;
|
||||
@@ -1409,14 +1410,15 @@ public:
|
||||
PROJECT::ELEM ProjectElementType() override { return PROJECT::ELEM::BOARD; }
|
||||
|
||||
/**
|
||||
* Save board file to the .history directory.
|
||||
* Serialize board into HISTORY_FILE_DATA for non-blocking history commit.
|
||||
*
|
||||
* This method is used as a saver callback for LOCAL_HISTORY during autosave operations.
|
||||
* Serialization runs on the UI thread; Prettify and file I/O happen in the background.
|
||||
*
|
||||
* @param aProjectPath The path to check against this board's project path
|
||||
* @param aFiles Output vector to append absolute file paths for history inclusion
|
||||
* @param aFileData Output vector to append serialized data for history inclusion
|
||||
*/
|
||||
void SaveToHistory( const wxString& aProjectPath, std::vector<wxString>& aFiles );
|
||||
void SaveToHistory( const wxString& aProjectPath, std::vector<HISTORY_FILE_DATA>& aFileData );
|
||||
|
||||
const std::unordered_map<KIID, BOARD_ITEM*>& GetItemByIdCache() const
|
||||
{
|
||||
|
||||
@@ -3065,14 +3065,14 @@ void PCB_EDIT_FRAME::ProjectChanged()
|
||||
PythonSyncProjectName();
|
||||
|
||||
// Register autosave history saver for the board.
|
||||
// Saver exports the in-memory BOARD into the history mirror preserving the original
|
||||
// relative path and file name (reparented under .history) without touching dirty flags.
|
||||
// Saver serializes the in-memory BOARD into HISTORY_FILE_DATA. Prettify and
|
||||
// file I/O happen on a background thread to avoid blocking the UI.
|
||||
if( GetBoard() )
|
||||
{
|
||||
Kiway().LocalHistory().RegisterSaver( GetBoard(),
|
||||
[this]( const wxString& aProjectPath, std::vector<wxString>& aFiles )
|
||||
[this]( const wxString& aProjectPath, std::vector<HISTORY_FILE_DATA>& aFileData )
|
||||
{
|
||||
GetBoard()->SaveToHistory( aProjectPath, aFiles );
|
||||
GetBoard()->SaveToHistory( aProjectPath, aFileData );
|
||||
} );
|
||||
}
|
||||
}
|
||||
|
||||
@@ -312,6 +312,15 @@ void PCB_IO_KICAD_SEXPR::SaveBoard( const wxString& aFileName, BOARD* aBoard,
|
||||
}
|
||||
}
|
||||
|
||||
PRETTIFIED_FILE_OUTPUTFORMATTER formatter( aFileName );
|
||||
FormatBoardToFormatter( &formatter, aBoard, aProperties );
|
||||
formatter.Finish();
|
||||
}
|
||||
|
||||
|
||||
void PCB_IO_KICAD_SEXPR::FormatBoardToFormatter( OUTPUTFORMATTER* aOut, BOARD* aBoard,
|
||||
const std::map<std::string, UTF8>* aProperties )
|
||||
{
|
||||
init( aProperties );
|
||||
|
||||
m_board = aBoard; // after init()
|
||||
@@ -323,9 +332,7 @@ void PCB_IO_KICAD_SEXPR::SaveBoard( const wxString& aFileName, BOARD* aBoard,
|
||||
else
|
||||
m_board->GetEmbeddedFiles()->ClearEmbeddedFonts();
|
||||
|
||||
PRETTIFIED_FILE_OUTPUTFORMATTER formatter( aFileName );
|
||||
|
||||
m_out = &formatter; // no ownership
|
||||
m_out = aOut;
|
||||
|
||||
m_out->Print( "(kicad_pcb (version %d) (generator \"pcbnew\") (generator_version %s)",
|
||||
SEXPR_BOARD_FILE_VERSION,
|
||||
@@ -334,7 +341,6 @@ void PCB_IO_KICAD_SEXPR::SaveBoard( const wxString& aFileName, BOARD* aBoard,
|
||||
Format( aBoard );
|
||||
|
||||
m_out->Print( ")" );
|
||||
m_out->Finish();
|
||||
|
||||
m_out = nullptr;
|
||||
}
|
||||
|
||||
@@ -349,6 +349,13 @@ public:
|
||||
void SaveBoard( const wxString& aFileName, BOARD* aBoard,
|
||||
const std::map<std::string, UTF8>* aProperties = nullptr ) override;
|
||||
|
||||
/** Serialize a BOARD to an OUTPUTFORMATTER without file I/O or Prettify.
|
||||
* Handles init(), EmbedFonts/ClearEmbeddedFonts, header, Format(), and footer.
|
||||
* The caller owns the formatter and is responsible for flushing/closing it.
|
||||
* Skips GroupsSanityCheck (no UI interaction allowed from timer callbacks). */
|
||||
void FormatBoardToFormatter( OUTPUTFORMATTER* aOut, BOARD* aBoard,
|
||||
const std::map<std::string, UTF8>* aProperties = nullptr );
|
||||
|
||||
BOARD* LoadBoard( const wxString& aFileName, BOARD* aAppendToMe,
|
||||
const std::map<std::string, UTF8>* aProperties = nullptr,
|
||||
PROJECT* aProject = nullptr ) override;
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
*/
|
||||
|
||||
#include <boost/test/unit_test.hpp>
|
||||
#include <local_history.h>
|
||||
#include <sch_screen.h>
|
||||
#include <sch_sheet.h>
|
||||
#include <schematic.h>
|
||||
@@ -67,15 +68,16 @@ BOOST_FIXTURE_TEST_CASE( SavesLegacySheetIntoHistoryPath, HISTORY_AUTOSAVE_FIXTU
|
||||
sheet->SetFileName( wxS( "legacy/legacy.sch" ) );
|
||||
sheet->GetScreen()->SetFileName( wxS( "legacy/legacy.sch" ) );
|
||||
|
||||
std::vector<wxString> savedFiles;
|
||||
m_schematic->SaveToHistory( m_settingsManager.Prj().GetProjectPath(), savedFiles );
|
||||
std::vector<HISTORY_FILE_DATA> fileData;
|
||||
m_schematic->SaveToHistory( m_settingsManager.Prj().GetProjectPath(), fileData );
|
||||
|
||||
BOOST_REQUIRE_EQUAL( savedFiles.size(), 1 );
|
||||
BOOST_REQUIRE_EQUAL( fileData.size(), 1 );
|
||||
|
||||
wxFileName saved( savedFiles[0] );
|
||||
BOOST_CHECK( saved.FileExists() );
|
||||
wxFileName saved( fileData[0].path );
|
||||
BOOST_CHECK_EQUAL( saved.GetExt(), FILEEXT::LegacySchematicFileExtension );
|
||||
BOOST_CHECK( saved.GetPath().Contains( wxS( "legacy" ) ) );
|
||||
BOOST_CHECK( !fileData[0].content.empty() );
|
||||
BOOST_CHECK( fileData[0].prettify );
|
||||
}
|
||||
|
||||
BOOST_AUTO_TEST_SUITE_END()
|
||||
|
||||
Reference in New Issue
Block a user