24#include <fmt/format.h>
27#include <wx/process.h>
50 ACTION_PROCESS( std::function<
void(
int,
const wxString&,
const wxString& )> aCallback ) :
59 wxString output, error;
61 if( wxInputStream* processOut = GetInputStream() )
63 while( processOut->CanRead() )
66 buffer[ processOut->Read( buffer,
sizeof( buffer ) - 1 ).LastRead() ] =
'\0';
67 output.append( buffer, processOut->LastRead() );
71 if( wxInputStream* processErr = GetErrorStream() )
73 while( processErr->CanRead() )
76 buffer[ processErr->Read( buffer,
sizeof( buffer ) - 1 ).LastRead() ] =
'\0';
77 error.append( buffer, processErr->LastRead() );
84 wxProcess::OnTerminate( aPid, aStatus );
88 std::function<void(
int,
const wxString&,
const wxString& )>
m_callback;
93 const wxString& aMessage )
95 if( !aReporter || aMessage.IsEmpty() )
98 aReporter->
Report( wxString::Format(
_(
"Plugin action '%s': %s" ), aActionName, aMessage ),
104 const wxString& aMessage )
106 if( !aReporter || aMessage.IsEmpty() )
109 aReporter->
Report( wxString::Format(
_(
"Plugin '%s': %s" ), aPluginName, aMessage ),
115 const wxString& aDescription,
const wxString& aDebugText )
117 if( !aReporter || ( aDescription.IsEmpty() && aDebugText.IsEmpty() ) )
121 error.
SetTitle( wxString::Format(
_(
"Error loading plugin '%s'" ), aPluginName ) );
123 if( !aDescription.IsEmpty() )
126 if( !aDebugText.IsEmpty() )
129 aReporter->
Report( error );
134 int aRetVal,
const wxString& aError )
136 wxString trimmedError = aError;
138 trimmedError.Trim(
false );
143 wxString::Format(
_(
"exited with code %d" ), aRetVal ) );
146 if( !trimmedError.IsEmpty() )
160 schemaFile.AppendDir( wxS(
"schemas" ) );
179 wxDirTraverseResult
OnFile(
const wxString& aFilePath )
override
181 wxFileName file( aFilePath );
183 if( file.GetFullName() == wxS(
"plugin.json" ) )
186 return wxDIR_CONTINUE;
189 wxDirTraverseResult
OnDir(
const wxString& dirPath )
override
191 return wxDIR_CONTINUE;
197 std::shared_ptr<REPORTER> aReporter )
210 [&](
const wxFileName& aFile )
212 wxLogTrace(
traceApi, wxString::Format(
"Manager: loading plugin from %s",
213 aFile.GetFullPath() ) );
219 const wxString&
id = plugin->Identifier();
223 wxLogTrace(
traceApi, wxString::Format(
"Manager: identifier %s already present, reloading",
id ) );
241 wxLogTrace(
traceApi,
"Manager: loading failed" );
244 plugin->ErrorMessage() );
248 if( aDirectoryToScan )
250 wxDir customDir( *aDirectoryToScan );
251 wxLogTrace(
traceApi, wxString::Format(
"Manager: scanning custom path (%s) for plugins...",
252 customDir.GetName() ) );
253 customDir.Traverse( loader );
259 if( systemPluginsDir.IsOpened() )
261 wxLogTrace(
traceApi, wxString::Format(
"Manager: scanning system path (%s) for plugins...",
262 systemPluginsDir.GetName() ) );
263 systemPluginsDir.Traverse( loader );
266 wxString thirdPartyPath;
274 wxDir thirdParty( thirdPartyPath );
276 if( thirdParty.IsOpened() )
278 wxLogTrace(
traceApi, wxString::Format(
"Manager: scanning PCM path (%s) for plugins...",
279 thirdParty.GetName() ) );
280 thirdParty.Traverse( loader );
285 if( userPluginsDir.IsOpened() )
287 wxLogTrace(
traceApi, wxString::Format(
"Manager: scanning user path (%s) for plugins...",
288 userPluginsDir.GetName() ) );
289 userPluginsDir.Traverse( loader );
315 wxCHECK( env.has_value(), );
317 wxFileName envConfigPath( *env, wxS(
"pyvenv.cfg" ) );
318 envConfigPath.MakeAbsolute();
320 if( envConfigPath.DirExists() && envConfigPath.Rmdir( wxPATH_RMDIR_RECURSIVE ) )
323 wxString::Format(
"Manager: Removed existing Python environment at %s for %s",
324 envConfigPath.GetPath(), plugin->
Identifier() ) );
330 job.
env_path = envConfigPath.GetPath();
331 m_jobs.emplace_back( job );
333 wxCommandEvent* evt =
new wxCommandEvent( EDA_EVT_PLUGIN_MANAGER_JOB_FINISHED, wxID_ANY );
349 bool aSync, wxString* aStdout, wxString* aStderr,
350 std::shared_ptr<REPORTER> aReporter )
363 wxLogTrace(
traceApi, wxString::Format(
"Manager: Plugin %s is not ready",
369 pluginFile.MakeAbsolute( plugin.
BasePath() );
370 pluginFile.Normalize( wxPATH_NORM_ABSOLUTE | wxPATH_NORM_SHORTCUT | wxPATH_NORM_DOTS
371 | wxPATH_NORM_TILDE, plugin.
BasePath() );
372 wxString pluginPath = pluginFile.GetFullPath();
374 std::vector<const wchar_t*> args;
375 std::optional<wxString> py;
385 wxLogTrace(
traceApi, wxString::Format(
"Manager: Python interpreter for %s not found",
388 _(
"missing plugin environment" ) );
392 if( !pluginFile.IsFileReadable() )
394 wxLogTrace(
traceApi, wxString::Format(
"Manager: Python entrypoint %s is not readable",
395 pluginFile.GetFullPath() ) );
397 wxString::Format(
_(
"entrypoint '%s' could not be read" ),
398 pluginFile.GetFullPath() ) );
402 std::optional<wxString> pythonHome =
407 wxGetEnvMap( &env.env );
409 if(
Pgm().ApiServerOrNull() )
415 env.cwd = pluginFile.GetPath();
419 wxGetEnv( wxS(
"SYSTEMROOT" ), &systemRoot );
420 env.env[wxS(
"SYSTEMROOT" )] = systemRoot;
422 if(
Pgm().GetCommonSettings()->m_Api.python_interpreter ==
FindKicadFile(
"pythonw.exe" )
423 || wxGetEnv( wxT(
"KICAD_RUN_FROM_BUILD_DIR" ),
nullptr ) )
425 wxLogTrace(
traceApi,
"Configured Python is the KiCad one; erasing path overrides..." );
426 env.env.erase(
"PYTHONHOME" );
427 env.env.erase(
"PYTHONPATH" );
432 env.env[wxS(
"VIRTUAL_ENV" )] = *pythonHome;
434 std::vector<wxString> pyArgs( aExtraArgs );
435 pyArgs.insert( pyArgs.begin(), pluginFile.GetFullPath() );
441 wxString* stdoutSink = aStdout ? aStdout : &stdOut;
442 wxString* stderrSink = aStderr ? aStderr : &stdErr;
443 int ret = manager.
ExecuteSync( pyArgs, stdoutSink, stderrSink, &env );
448 [[maybe_unused]]
long pid = manager.
Execute( pyArgs,
449 [aReporter, action](
int aRetVal,
const wxString& aOutput,
450 const wxString& aError )
453 wxString::Format(
"Manager: action exited with code %d", aRetVal ) );
455 if( !aError.IsEmpty() )
456 wxLogTrace(
traceApi, wxString::Format(
"Manager: action stderr: %s", aError ) );
465 _(
"process could not be created" ) );
479 wxString script = wxString::Format(
480 wxS(
"tell application \"System Events\"\n"
481 " set plist to every process whose unix id is %ld\n"
482 " repeat with proc in plist\n"
483 " set the frontmost of proc to true\n"
487 wxString cmd = wxString::Format(
"osascript -e '%s'", script );
488 wxLogTrace(
traceApi, wxString::Format(
"Execute: %s", cmd ) );
504 if( !pluginFile.IsFileExecutable() )
506 wxLogTrace(
traceApi, wxString::Format(
"Manager: Exec entrypoint %s is not executable",
507 pluginFile.GetFullPath() ) );
509 wxString::Format(
_(
"entrypoint '%s' is not executable" ),
510 pluginFile.GetFullPath() ) );
515 wxGetEnvMap( &env.env );
517 if(
Pgm().ApiServerOrNull() )
523 env.cwd = pluginFile.GetPath();
525 long pidOrRetCode = 0;
529 wxString cmd = pluginPath;
531 for(
const wxString& arg : action->
args )
534 wxArrayString out, err;
536 pidOrRetCode = wxExecute( cmd, out, err, wxEXEC_BLOCK, &env );
540 for(
const wxString& line : out )
541 *aStdout << line <<
"\n";
546 for(
const wxString& line : err )
547 stdErr << line <<
"\n";
558 [aReporter, action](
int aRetVal,
const wxString& aOutput,
559 const wxString& aError )
562 wxString::Format(
"Manager: action exited with code %d", aRetVal ) );
564 if( !aError.IsEmpty() )
566 wxString::Format(
"Manager: action stderr: %s", aError ) );
572 args.emplace_back( pluginPath.wc_str() );
574 for(
const wxString& arg : action->
args )
575 args.emplace_back( arg.wc_str() );
577 args.emplace_back(
nullptr );
579 pidOrRetCode = wxExecute(
const_cast<wchar_t**
>( args.data() ),
580 wxEXEC_ASYNC | wxEXEC_HIDE_CONSOLE,
process, &env );
588 wxLogTrace(
traceApi, wxString::Format(
"Manager: launching action %s failed",
594 wxLogTrace(
traceApi, wxString::Format(
"Manager: launching action %s -> pid %ld",
601 wxLogTrace(
traceApi, wxString::Format(
"Manager: unhandled runtime for action %s",
610 std::shared_ptr<REPORTER> aReporter )
612 doInvokeAction( aIdentifier, {},
false,
nullptr,
nullptr, std::move( aReporter ) );
617 wxString* aStdout, wxString* aStderr,
618 std::shared_ptr<REPORTER> aReporter )
620 return doInvokeAction( aIdentifier, aExtraArgs,
true, aStdout, aStderr,
621 std::move( aReporter ) );
627 std::vector<const PLUGIN_ACTION*> actions;
634 if( action->scopes.count( aScope ) )
635 actions.emplace_back( action );
653 bool addedAnyJobs =
false;
655 for(
const std::unique_ptr<API_PLUGIN>& plugin :
m_plugins )
660 wxLogTrace(
traceApi, wxString::Format(
"Manager: processing dependencies for %s",
661 plugin->Identifier() ) );
666 wxLogTrace(
traceApi, wxString::Format(
"Manager: %s is not a Python plugin, all set",
667 plugin->Identifier() ) );
676 wxLogTrace(
traceApi, wxString::Format(
"Manager: could not create env for %s",
677 plugin->Identifier() ) );
683 wxFileName envConfigPath( *env, wxS(
"pyvenv.cfg" ) );
684 envConfigPath.MakeAbsolute();
686 if( envConfigPath.IsFileReadable() )
688 wxLogTrace(
traceApi, wxString::Format(
"Manager: Python env for %s exists at %s",
689 plugin->Identifier(),
690 envConfigPath.GetPath() ) );
695 job.
env_path = envConfigPath.GetPath();
696 m_jobs.emplace_back( job );
701 wxLogTrace(
traceApi, wxString::Format(
"Manager: will create Python env for %s at %s",
702 plugin->Identifier(), envConfigPath.GetPath() ) );
707 job.
env_path = envConfigPath.GetPath();
708 m_jobs.emplace_back( job );
714 wxCommandEvent* evt =
new wxCommandEvent( EDA_EVT_PLUGIN_MANAGER_JOB_FINISHED, wxID_ANY );
724 wxLogTrace(
traceApi,
"Manager: no more jobs to process" );
728 wxLogTrace(
traceApi, wxString::Format(
"Manager: begin processing; %zu jobs left in queue",
735 wxLogTrace(
traceApi,
"Manager: Using Python interpreter at %s",
736 Pgm().GetCommonSettings()->m_Api.python_interpreter );
737 wxLogTrace(
traceApi, wxString::Format(
"Manager: creating Python env at %s",
744 wxGetEnv( wxS(
"SYSTEMROOT" ), &systemRoot );
745 env.env[wxS(
"SYSTEMROOT" )] = systemRoot;
747 if(
Pgm().GetCommonSettings()->m_Api.python_interpreter ==
FindKicadFile(
"pythonw.exe" )
748 || wxGetEnv( wxT(
"KICAD_RUN_FROM_BUILD_DIR" ),
nullptr ) )
750 wxLogTrace(
traceApi,
"Configured Python is the KiCad one; erasing path overrides..." );
751 env.env.erase(
"PYTHONHOME" );
752 env.env.erase(
"PYTHONPATH" );
755 std::vector<wxString> args = {
758 "--system-site-packages",
763 [
this, job](
int aRetVal,
const wxString& aOutput,
const wxString& aError )
766 wxString::Format(
"Manager: created venv (python returned %d)", aRetVal ) );
768 if( !aError.IsEmpty() )
769 wxLogTrace(
traceApi, wxString::Format(
"Manager: venv err: %s", aError ) );
773 wxString error = aError;
774 error.Trim().Trim(
false );
776 if( error.IsEmpty() )
777 error = wxString::Format(
_(
"error code %d" ), aRetVal );
780 _(
"Could not create plugin environment" ), error );
783 wxCommandEvent* evt =
new wxCommandEvent( EDA_EVT_PLUGIN_MANAGER_JOB_FINISHED, wxID_ANY );
789 m_jobs.emplace_back( nextJob );
793 wxLogTrace(
traceApi, wxString::Format(
"Manager: setting up environment for %s",
801 wxString debug = wxString::Format( wxS(
"Python binary not found at %s" ), job.
env_path );
802 wxLogTrace(
traceApi, wxString::Format(
"Manager: error: %s", debug ) );
805 _(
"Missing plugin environment" ), debug );
813 env.env[wxS(
"VIRTUAL_ENV" )] = *pythonHome;
817 wxGetEnv( wxS(
"SYSTEMROOT" ), &systemRoot );
818 env.env[wxS(
"SYSTEMROOT" )] = systemRoot;
820 if(
Pgm().GetCommonSettings()->m_Api.python_interpreter
822 || wxGetEnv( wxT(
"KICAD_RUN_FROM_BUILD_DIR" ),
nullptr ) )
825 "Configured Python is the KiCad one; erasing path overrides..." );
826 env.env.erase(
"PYTHONHOME" );
827 env.env.erase(
"PYTHONPATH" );
831 std::vector<wxString> args = {
840 [
this, job](
int aRetVal,
const wxString& aOutput,
const wxString& aError )
842 wxLogTrace(
traceApi, wxString::Format(
"Manager: upgrade pip returned %d",
845 if( !aError.IsEmpty() )
848 wxString::Format(
"Manager: upgrade pip stderr: %s", aError ) );
853 wxString error = aError;
854 error.Trim().Trim(
false );
856 if( error.IsEmpty() )
857 error = wxString::Format(
_(
"error code %d" ), aRetVal );
860 _(
"Could not create plugin environment" ), error );
863 wxCommandEvent* evt =
864 new wxCommandEvent( EDA_EVT_PLUGIN_MANAGER_JOB_FINISHED, wxID_ANY );
870 m_jobs.emplace_back( nextJob );
875 wxLogTrace(
traceApi, wxString::Format(
"Manager: installing dependencies for %s",
880 wxFileName reqs = wxFileName( job.
plugin_path,
"requirements.txt" );
884 wxString debug = wxString::Format( wxS(
"Python binary not found at %s" ), job.
env_path );
885 wxLogTrace(
traceApi, wxString::Format(
"Manager: error: %s", debug ) );
888 _(
"Missing plugin environment" ), debug );
890 else if( !reqs.IsFileReadable() )
893 wxString::Format(
"Manager: error: requirements.txt not found at %s",
895 wxString debug = wxString::Format( wxS(
"Expected at %s" ), reqs.GetFullPath() );
898 _(
"requirements.txt could not be read" ), debug );
902 wxLogTrace(
traceApi,
"Manager: Python exe '%s'", *python );
909 wxGetEnv( wxS(
"SYSTEMROOT" ), &systemRoot );
910 env.env[wxS(
"SYSTEMROOT" )] = systemRoot;
913 env.env.erase(
"PYTHONHOME" );
914 env.env.erase(
"PYTHONPATH" );
918 env.env[wxS(
"VIRTUAL_ENV" )] = *pythonHome;
920 std::vector<wxString> args = {
928 "--require-virtualenv",
936 [
this, job](
int aRetVal,
const wxString& aOutput,
const wxString& aError )
938 if( !aError.IsEmpty() )
939 wxLogTrace(
traceApi, wxString::Format(
"Manager: pip stderr: %s", aError ) );
943 wxString error = aError;
944 error.Trim().Trim(
false );
946 if( error.IsEmpty() )
947 error = wxString::Format(
_(
"error code %d" ), aRetVal );
950 _(
"Could not create plugin environment" ), error );
955 wxLogTrace(
traceApi, wxString::Format(
"Manager: marking %s as ready",
959 wxCommandEvent* availabilityEvt =
961 wxTheApp->QueueEvent( availabilityEvt );
966 wxCommandEvent* evt =
new wxCommandEvent( EDA_EVT_PLUGIN_MANAGER_JOB_FINISHED,
973 wxCommandEvent* evt =
new wxCommandEvent( EDA_EVT_PLUGIN_MANAGER_JOB_FINISHED, wxID_ANY );
982 wxLogTrace(
traceApi, wxString::Format(
"Manager: finished job; %zu left in queue",
static void reportPluginLoadMessage(REPORTER *aReporter, const wxString &aPluginName, const wxString &aMessage)
static void reportPluginActionResult(REPORTER *aReporter, const wxString &aActionName, int aRetVal, const wxString &aError)
wxDEFINE_EVENT(EDA_EVT_PLUGIN_MANAGER_JOB_FINISHED, wxCommandEvent)
static void reportPluginActionMessage(REPORTER *aReporter, const wxString &aActionName, const wxString &aMessage)
const KICOMMON_API wxEventTypeTag< wxCommandEvent > EDA_EVT_PLUGIN_AVAILABILITY_CHANGED
Notifies other parts of KiCad when plugin availability changes.
ACTION_PROCESS(std::function< void(int, const wxString &, const wxString &)> aCallback)
void OnTerminate(int aPid, int aStatus) override
std::function< void(int, const wxString &, const wxString &)> m_callback
std::set< std::unique_ptr< API_PLUGIN >, CompareApiPluginIdentifiers > m_plugins
int InvokeActionSync(const wxString &aIdentifier, std::vector< wxString > aExtraArgs, wxString *aStdout=nullptr, wxString *aStderr=nullptr, std::shared_ptr< REPORTER > aReporter=nullptr)
Invokes an action synchronously, capturing its output.
void ReloadPlugins(std::optional< wxString > aDirectoryToScan=std::nullopt, std::shared_ptr< REPORTER > aReporter=nullptr)
Clears the loaded plugins and actions and re-scans the filesystem to register new ones.
std::map< wxString, wxString > m_environmentCache
Map of plugin identifier to a path for the plugin's virtual environment, if it has one.
std::shared_ptr< REPORTER > m_reloadReporter
void InvokeAction(const wxString &aIdentifier, std::shared_ptr< REPORTER > aReporter=nullptr)
std::unique_ptr< JSON_SCHEMA_VALIDATOR > m_schema_validator
int doInvokeAction(const wxString &aIdentifier, std::vector< wxString > aExtraArgs, bool aSync=false, wxString *aStdout=nullptr, wxString *aStderr=nullptr, std::shared_ptr< REPORTER > aReporter=nullptr)
void processPluginDependencies()
std::vector< const PLUGIN_ACTION * > GetActionsForScope(PLUGIN_ACTION_SCOPE aScope)
std::map< int, wxString > m_menuBindings
Map of menu wx item id to action identifier.
std::map< int, wxString > m_buttonBindings
Map of button wx item id to action identifier.
void RecreatePluginEnvironment(const wxString &aIdentifier)
std::map< wxString, const API_PLUGIN * > m_pluginsCache
wxString pluginName(const wxString &aIdentifier) const
std::optional< const PLUGIN_ACTION * > GetAction(const wxString &aIdentifier)
void processNextJob(wxCommandEvent &aEvent)
std::set< wxString > m_readyPlugins
std::map< wxString, const PLUGIN_ACTION * > m_actionsCache
API_PLUGIN_MANAGER(wxEvtHandler *aParent)
std::set< wxString > m_busyPlugins
A plugin that is invoked by KiCad and runs as an external process; communicating with KiCad via the I...
const PLUGIN_RUNTIME & Runtime() const
const wxString & Identifier() const
wxString BasePath() const
const std::string & Token() const
std::string SocketPath() const
Holds a structured error message.
KI_ERROR & SetDebugText(const wxString &aDebugText)
KI_ERROR & SetDescription(const wxString &aDescription)
KI_ERROR & SetTitle(const wxString &aTitle)
static wxString GetUserPluginsPath()
Gets the user path for plugins.
static wxString GetStockPluginsPath()
Gets the stock (install) plugins path.
static wxString GetDefault3rdPartyPath()
Gets the default path for PCM packages.
static wxString GetStockDataPath(bool aRespectRunFromBuildDir=true)
Gets the stock (install) data path, which is the base path for things like scripting,...
virtual ENV_VAR_MAP & GetLocalEnvVariables() const
KICAD_API_SERVER & GetApiServer()
std::function< void(const wxFileName &)> m_action
PLUGIN_TRAVERSER(std::function< void(const wxFileName &)> aAction)
wxDirTraverseResult OnDir(const wxString &dirPath) override
wxDirTraverseResult OnFile(const wxString &aFilePath) override
static std::optional< wxString > GetPythonEnvironment(const wxString &aNamespace)
long Execute(const std::vector< wxString > &aArgs, const std::function< void(int, const wxString &, const wxString &)> &aCallback, const wxExecuteEnv *aEnv=nullptr, bool aSaveOutput=false)
Launches the Python interpreter with the given arguments.
static std::optional< wxString > GetVirtualPython(const wxString &aNamespace)
Returns a full path to the python binary in a venv, if it exists.
long ExecuteSync(const std::vector< wxString > &aArgs, wxString *aStdout=nullptr, wxString *aStderr=nullptr, const wxExecuteEnv *aEnv=nullptr)
A pure virtual class used to derive REPORTER objects from.
virtual REPORTER & Report(const wxString &aText, SEVERITY aSeverity=RPT_SEVERITY_UNDEFINED)
Report a string with a given severity.
Functions related to environment variables, including help functions.
wxString FindKicadFile(const wxString &shortname)
Search the executable file shortname in KiCad binary path and return full file name if found or short...
const wxChar *const traceApi
Flag to enable debug output related to the IPC API and its plugin system.
std::map< wxString, ENV_VAR_ITEM > ENV_VAR_MAP
KICOMMON_API std::optional< wxString > GetVersionedEnvVarValue(const std::map< wxString, ENV_VAR_ITEM > &aMap, const wxString &aBaseName)
Attempt to retrieve the value of a versioned environment variable, such as KICAD8_TEMPLATE_DIR.
static PGM_BASE * process
PGM_BASE & Pgm()
The global program "get" accessor.
An action performed by a plugin via the IPC API.
const API_PLUGIN & plugin
std::vector< wxString > args
#define FN_NORMALIZE_FLAGS
Default flags to pass to wxFileName::Normalize().