24#include <fmt/format.h>
27#include <wx/process.h>
50 ACTION_PROCESS( std::function<
void(
int,
const wxString&,
const wxString& )> aCallback ) :
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 int aRetVal,
const wxString& aError )
117 wxString trimmedError = aError;
119 trimmedError.Trim(
false );
124 wxString::Format(
_(
"exited with code %d" ), aRetVal ) );
127 if( !trimmedError.IsEmpty() )
141 schemaFile.AppendDir( wxS(
"schemas" ) );
160 wxDirTraverseResult
OnFile(
const wxString& aFilePath )
override
162 wxFileName file( aFilePath );
164 if( file.GetFullName() == wxS(
"plugin.json" ) )
167 return wxDIR_CONTINUE;
170 wxDirTraverseResult
OnDir(
const wxString& dirPath )
override
172 return wxDIR_CONTINUE;
178 std::shared_ptr<REPORTER> aReporter )
191 [&](
const wxFileName& aFile )
193 wxLogTrace(
traceApi, wxString::Format(
"Manager: loading plugin from %s",
194 aFile.GetFullPath() ) );
200 const wxString&
id = plugin->Identifier();
204 wxLogTrace(
traceApi, wxString::Format(
"Manager: identifier %s already present, reloading",
id ) );
222 wxLogTrace(
traceApi,
"Manager: loading failed" );
225 plugin->ErrorMessage() );
229 if( aDirectoryToScan )
231 wxDir customDir( *aDirectoryToScan );
232 wxLogTrace(
traceApi, wxString::Format(
"Manager: scanning custom path (%s) for plugins...",
233 customDir.GetName() ) );
234 customDir.Traverse( loader );
240 if( systemPluginsDir.IsOpened() )
242 wxLogTrace(
traceApi, wxString::Format(
"Manager: scanning system path (%s) for plugins...",
243 systemPluginsDir.GetName() ) );
244 systemPluginsDir.Traverse( loader );
247 wxString thirdPartyPath;
255 wxDir thirdParty( thirdPartyPath );
257 if( thirdParty.IsOpened() )
259 wxLogTrace(
traceApi, wxString::Format(
"Manager: scanning PCM path (%s) for plugins...",
260 thirdParty.GetName() ) );
261 thirdParty.Traverse( loader );
266 if( userPluginsDir.IsOpened() )
268 wxLogTrace(
traceApi, wxString::Format(
"Manager: scanning user path (%s) for plugins...",
269 userPluginsDir.GetName() ) );
270 userPluginsDir.Traverse( loader );
296 wxCHECK( env.has_value(), );
298 wxFileName envConfigPath( *env, wxS(
"pyvenv.cfg" ) );
299 envConfigPath.MakeAbsolute();
301 if( envConfigPath.DirExists() && envConfigPath.Rmdir( wxPATH_RMDIR_RECURSIVE ) )
304 wxString::Format(
"Manager: Removed existing Python environment at %s for %s",
305 envConfigPath.GetPath(), plugin->
Identifier() ) );
311 job.
env_path = envConfigPath.GetPath();
312 m_jobs.emplace_back( job );
314 wxCommandEvent* evt =
new wxCommandEvent( EDA_EVT_PLUGIN_MANAGER_JOB_FINISHED, wxID_ANY );
330 bool aSync, wxString* aStdout, wxString* aStderr,
331 std::shared_ptr<REPORTER> aReporter )
344 wxLogTrace(
traceApi, wxString::Format(
"Manager: Plugin %s is not ready",
350 pluginFile.MakeAbsolute( plugin.
BasePath() );
351 pluginFile.Normalize( wxPATH_NORM_ABSOLUTE | wxPATH_NORM_SHORTCUT | wxPATH_NORM_DOTS
352 | wxPATH_NORM_TILDE, plugin.
BasePath() );
353 wxString pluginPath = pluginFile.GetFullPath();
355 std::vector<const wchar_t*> args;
356 std::optional<wxString> py;
366 wxLogTrace(
traceApi, wxString::Format(
"Manager: Python interpreter for %s not found",
369 _(
"missing plugin environment" ) );
373 if( !pluginFile.IsFileReadable() )
375 wxLogTrace(
traceApi, wxString::Format(
"Manager: Python entrypoint %s is not readable",
376 pluginFile.GetFullPath() ) );
378 wxString::Format(
_(
"entrypoint '%s' could not be read" ),
379 pluginFile.GetFullPath() ) );
383 std::optional<wxString> pythonHome =
388 wxGetEnvMap( &env.env );
390 if(
Pgm().ApiServerOrNull() )
392 env.env[wxS(
"KICAD_API_SOCKET" )] =
Pgm().GetApiServer().SocketPath();
393 env.env[wxS(
"KICAD_API_TOKEN" )] =
Pgm().GetApiServer().Token();
396 env.cwd = pluginFile.GetPath();
400 wxGetEnv( wxS(
"SYSTEMROOT" ), &systemRoot );
401 env.env[wxS(
"SYSTEMROOT" )] = systemRoot;
403 if(
Pgm().GetCommonSettings()->m_Api.python_interpreter ==
FindKicadFile(
"pythonw.exe" )
404 || wxGetEnv( wxT(
"KICAD_RUN_FROM_BUILD_DIR" ),
nullptr ) )
406 wxLogTrace(
traceApi,
"Configured Python is the KiCad one; erasing path overrides..." );
407 env.env.erase(
"PYTHONHOME" );
408 env.env.erase(
"PYTHONPATH" );
413 env.env[wxS(
"VIRTUAL_ENV" )] = *pythonHome;
415 std::vector<wxString> pyArgs( aExtraArgs );
416 pyArgs.insert( pyArgs.begin(), pluginFile.GetFullPath() );
422 wxString* stdoutSink = aStdout ? aStdout : &stdOut;
423 wxString* stderrSink = aStderr ? aStderr : &stdErr;
424 int ret = manager.
ExecuteSync( pyArgs, stdoutSink, stderrSink, &env );
429 [[maybe_unused]]
long pid = manager.
Execute( pyArgs,
430 [aReporter, action](
int aRetVal,
const wxString& aOutput,
431 const wxString& aError )
434 wxString::Format(
"Manager: action exited with code %d", aRetVal ) );
436 if( !aError.IsEmpty() )
437 wxLogTrace(
traceApi, wxString::Format(
"Manager: action stderr: %s", aError ) );
446 _(
"process could not be created" ) );
460 wxString script = wxString::Format(
461 wxS(
"tell application \"System Events\"\n"
462 " set plist to every process whose unix id is %ld\n"
463 " repeat with proc in plist\n"
464 " set the frontmost of proc to true\n"
468 wxString cmd = wxString::Format(
"osascript -e '%s'", script );
469 wxLogTrace(
traceApi, wxString::Format(
"Execute: %s", cmd ) );
485 if( !pluginFile.IsFileExecutable() )
487 wxLogTrace(
traceApi, wxString::Format(
"Manager: Exec entrypoint %s is not executable",
488 pluginFile.GetFullPath() ) );
490 wxString::Format(
_(
"entrypoint '%s' is not executable" ),
491 pluginFile.GetFullPath() ) );
496 wxGetEnvMap( &env.env );
498 if(
Pgm().ApiServerOrNull() )
500 env.env[wxS(
"KICAD_API_SOCKET" )] =
Pgm().GetApiServer().SocketPath();
501 env.env[wxS(
"KICAD_API_TOKEN" )] =
Pgm().GetApiServer().Token();
504 env.cwd = pluginFile.GetPath();
506 long pidOrRetCode = 0;
510 wxString cmd = pluginPath;
512 for(
const wxString& arg : action->
args )
515 wxArrayString out, err;
517 pidOrRetCode = wxExecute( cmd, out, err, wxEXEC_BLOCK, &env );
521 for(
const wxString& line : out )
522 *aStdout << line <<
"\n";
527 for(
const wxString& line : err )
528 stdErr << line <<
"\n";
539 [aReporter, action](
int aRetVal,
const wxString& aOutput,
540 const wxString& aError )
543 wxString::Format(
"Manager: action exited with code %d", aRetVal ) );
545 if( !aError.IsEmpty() )
547 wxString::Format(
"Manager: action stderr: %s", aError ) );
553 args.emplace_back( pluginPath.wc_str() );
555 for(
const wxString& arg : action->
args )
556 args.emplace_back( arg.wc_str() );
558 args.emplace_back(
nullptr );
560 pidOrRetCode = wxExecute(
const_cast<wchar_t**
>( args.data() ),
561 wxEXEC_ASYNC | wxEXEC_HIDE_CONSOLE,
process, &env );
569 wxLogTrace(
traceApi, wxString::Format(
"Manager: launching action %s failed",
575 wxLogTrace(
traceApi, wxString::Format(
"Manager: launching action %s -> pid %ld",
582 wxLogTrace(
traceApi, wxString::Format(
"Manager: unhandled runtime for action %s",
591 std::shared_ptr<REPORTER> aReporter )
593 doInvokeAction( aIdentifier, {},
false,
nullptr,
nullptr, std::move( aReporter ) );
598 wxString* aStdout, wxString* aStderr,
599 std::shared_ptr<REPORTER> aReporter )
601 return doInvokeAction( aIdentifier, aExtraArgs,
true, aStdout, aStderr,
602 std::move( aReporter ) );
608 std::vector<const PLUGIN_ACTION*> actions;
615 if( action->scopes.count( aScope ) )
616 actions.emplace_back( action );
625 bool addedAnyJobs =
false;
627 for(
const std::unique_ptr<API_PLUGIN>& plugin :
m_plugins )
632 wxLogTrace(
traceApi, wxString::Format(
"Manager: processing dependencies for %s",
633 plugin->Identifier() ) );
638 wxLogTrace(
traceApi, wxString::Format(
"Manager: %s is not a Python plugin, all set",
639 plugin->Identifier() ) );
648 wxLogTrace(
traceApi, wxString::Format(
"Manager: could not create env for %s",
649 plugin->Identifier() ) );
655 wxFileName envConfigPath( *env, wxS(
"pyvenv.cfg" ) );
656 envConfigPath.MakeAbsolute();
658 if( envConfigPath.IsFileReadable() )
660 wxLogTrace(
traceApi, wxString::Format(
"Manager: Python env for %s exists at %s",
661 plugin->Identifier(),
662 envConfigPath.GetPath() ) );
667 job.
env_path = envConfigPath.GetPath();
668 m_jobs.emplace_back( job );
673 wxLogTrace(
traceApi, wxString::Format(
"Manager: will create Python env for %s at %s",
674 plugin->Identifier(), envConfigPath.GetPath() ) );
679 job.
env_path = envConfigPath.GetPath();
680 m_jobs.emplace_back( job );
686 wxCommandEvent* evt =
new wxCommandEvent( EDA_EVT_PLUGIN_MANAGER_JOB_FINISHED, wxID_ANY );
696 wxLogTrace(
traceApi,
"Manager: no more jobs to process" );
700 wxLogTrace(
traceApi, wxString::Format(
"Manager: begin processing; %zu jobs left in queue",
707 wxLogTrace(
traceApi,
"Manager: Using Python interpreter at %s",
708 Pgm().GetCommonSettings()->m_Api.python_interpreter );
709 wxLogTrace(
traceApi, wxString::Format(
"Manager: creating Python env at %s",
716 wxGetEnv( wxS(
"SYSTEMROOT" ), &systemRoot );
717 env.env[wxS(
"SYSTEMROOT" )] = systemRoot;
719 if(
Pgm().GetCommonSettings()->m_Api.python_interpreter ==
FindKicadFile(
"pythonw.exe" )
720 || wxGetEnv( wxT(
"KICAD_RUN_FROM_BUILD_DIR" ),
nullptr ) )
722 wxLogTrace(
traceApi,
"Configured Python is the KiCad one; erasing path overrides..." );
723 env.env.erase(
"PYTHONHOME" );
724 env.env.erase(
"PYTHONPATH" );
727 std::vector<wxString> args = {
730 "--system-site-packages",
735 [
this, job](
int aRetVal,
const wxString& aOutput,
const wxString& aError )
738 wxString::Format(
"Manager: created venv (python returned %d)", aRetVal ) );
740 if( !aError.IsEmpty() )
741 wxLogTrace(
traceApi, wxString::Format(
"Manager: venv err: %s", aError ) );
745 wxString error = aError;
746 error.Trim().Trim(
false );
748 if( error.IsEmpty() )
749 error = wxString::Format(
_(
"error code %d" ), aRetVal );
751 error = wxString::Format(
_(
"could not create plugin environment: %s" ), error );
756 wxCommandEvent* evt =
757 new wxCommandEvent( EDA_EVT_PLUGIN_MANAGER_JOB_FINISHED, wxID_ANY );
763 m_jobs.emplace_back( nextJob );
767 wxLogTrace(
traceApi, wxString::Format(
"Manager: setting up environment for %s",
775 wxLogTrace(
traceApi, wxString::Format(
"Manager: error: python not found at %s",
779 _(
"missing plugin environment" ) );
787 env.env[wxS(
"VIRTUAL_ENV" )] = *pythonHome;
791 wxGetEnv( wxS(
"SYSTEMROOT" ), &systemRoot );
792 env.env[wxS(
"SYSTEMROOT" )] = systemRoot;
794 if(
Pgm().GetCommonSettings()->m_Api.python_interpreter
796 || wxGetEnv( wxT(
"KICAD_RUN_FROM_BUILD_DIR" ),
nullptr ) )
799 "Configured Python is the KiCad one; erasing path overrides..." );
800 env.env.erase(
"PYTHONHOME" );
801 env.env.erase(
"PYTHONPATH" );
805 std::vector<wxString> args = {
814 [
this, job](
int aRetVal,
const wxString& aOutput,
const wxString& aError )
816 wxLogTrace(
traceApi, wxString::Format(
"Manager: upgrade pip returned %d",
819 if( !aError.IsEmpty() )
822 wxString::Format(
"Manager: upgrade pip stderr: %s", aError ) );
827 wxString error = aError;
828 error.Trim().Trim(
false );
830 if( error.IsEmpty() )
831 error = wxString::Format(
_(
"error code %d" ), aRetVal );
833 error = wxString::Format(
_(
"could not create plugin environment: %s" ), error );
838 wxCommandEvent* evt =
839 new wxCommandEvent( EDA_EVT_PLUGIN_MANAGER_JOB_FINISHED, wxID_ANY );
845 m_jobs.emplace_back( nextJob );
850 wxLogTrace(
traceApi, wxString::Format(
"Manager: installing dependencies for %s",
855 wxFileName reqs = wxFileName( job.
plugin_path,
"requirements.txt" );
859 wxLogTrace(
traceApi, wxString::Format(
"Manager: error: python not found at %s",
863 _(
"missing plugin environment" ) );
865 else if( !reqs.IsFileReadable() )
868 wxString::Format(
"Manager: error: requirements.txt not found at %s",
872 _(
"requirements.txt could not be read" ) );
876 wxLogTrace(
traceApi,
"Manager: Python exe '%s'", *python );
883 wxGetEnv( wxS(
"SYSTEMROOT" ), &systemRoot );
884 env.env[wxS(
"SYSTEMROOT" )] = systemRoot;
887 env.env.erase(
"PYTHONHOME" );
888 env.env.erase(
"PYTHONPATH" );
892 env.env[wxS(
"VIRTUAL_ENV" )] = *pythonHome;
894 std::vector<wxString> args = {
902 "--require-virtualenv",
910 [
this, job](
int aRetVal,
const wxString& aOutput,
const wxString& aError )
912 if( !aError.IsEmpty() )
913 wxLogTrace(
traceApi, wxString::Format(
"Manager: pip stderr: %s", aError ) );
917 wxString error = aError;
918 error.Trim().Trim(
false );
920 if( error.IsEmpty() )
921 error = wxString::Format(
_(
"error code %d" ), aRetVal );
923 error = wxString::Format(
_(
"could not create plugin environment: %s" ), error );
930 wxLogTrace(
traceApi, wxString::Format(
"Manager: marking %s as ready",
934 wxCommandEvent* availabilityEvt =
936 wxTheApp->QueueEvent( availabilityEvt );
941 wxCommandEvent* evt =
new wxCommandEvent( EDA_EVT_PLUGIN_MANAGER_JOB_FINISHED,
948 wxCommandEvent* evt =
new wxCommandEvent( EDA_EVT_PLUGIN_MANAGER_JOB_FINISHED, wxID_ANY );
957 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
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
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
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().