KiCad PCB EDA Suite
Loading...
Searching...
No Matches
api_plugin_manager.cpp
Go to the documentation of this file.
1/*
2 * This program source code file is part of KiCad, a free EDA CAD application.
3 *
4 * Copyright (C) 2024 Jon Evans <[email protected]>
5 * Copyright The KiCad Developers, see AUTHORS.txt for contributors.
6 *
7 * This program is free software: you can redistribute it and/or modify it
8 * under the terms of the GNU General Public License as published by the
9 * Free Software Foundation, either version 3 of the License, or (at your
10 * option) any later version.
11 *
12 * This program is distributed in the hope that it will be useful, but
13 * WITHOUT ANY WARRANTY; without even the implied warranty of
14 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
15 * General Public License for more details.
16 *
17 * You should have received a copy of the GNU General Public License
18 * along with this program. If not, see <https://www.gnu.org/licenses/>.
19 */
20
21#include <fstream>
22
23#include <algorithm>
24
25#include <env_vars.h>
26#include <fmt/format.h>
27#include <wx/dir.h>
28#include <wx/log.h>
29#include <wx/process.h>
30#include <wx/timer.h>
31#include <wx/utils.h>
32
34#include <api/api_server.h>
35#include <api/api_utils.h>
36#include <gestfich.h>
37#include <paths.h>
38#include <wx_filename.h>
39#include <pgm_base.h>
40#include <api/python_manager.h>
41#include <reporter.h>
44
45
48
49
50class ACTION_PROCESS : public wxProcess
51{
52public:
53 ACTION_PROCESS( std::function<void( int, const wxString&, const wxString& )> aCallback ) :
54 wxProcess(),
55 m_callback( std::move( aCallback ) )
56 {}
57
58 void OnTerminate( int aPid, int aStatus ) override
59 {
60 if( m_callback )
61 {
62 wxString output, error;
63
64 if( wxInputStream* processOut = GetInputStream() )
65 {
66 while( processOut->CanRead() )
67 {
68 char buffer[4096];
69 buffer[ processOut->Read( buffer, sizeof( buffer ) - 1 ).LastRead() ] = '\0';
70 output.append( buffer, processOut->LastRead() );
71 }
72 }
73
74 if( wxInputStream* processErr = GetErrorStream() )
75 {
76 while( processErr->CanRead() )
77 {
78 char buffer[4096];
79 buffer[ processErr->Read( buffer, sizeof( buffer ) - 1 ).LastRead() ] = '\0';
80 error.append( buffer, processErr->LastRead() );
81 }
82 }
83
84 m_callback( aStatus, output, error );
85 }
86
87 wxProcess::OnTerminate( aPid, aStatus );
88 }
89
90private:
91 std::function<void( int, const wxString&, const wxString& )> m_callback;
92};
93
94
95static void reportPluginActionMessage( REPORTER* aReporter, const wxString& aActionName,
96 const wxString& aMessage )
97{
98 if( !aReporter || aMessage.IsEmpty() )
99 return;
100
102 error.SetTitle( wxString::Format( _( "Plugin action '%s'" ), aActionName ) );
103 error.SetDescription( aMessage );
104 aReporter->Report( error );
105}
106
107
108static void reportPluginLoadMessage( REPORTER* aReporter, const wxString& aPluginName,
109 const wxString& aMessage )
110{
111 if( !aReporter || aMessage.IsEmpty() )
112 return;
113
114 aReporter->Report( wxString::Format( _( "Plugin '%s': %s" ), aPluginName, aMessage ),
116}
117
118
119static void reportPluginLoadMessage( REPORTER* aReporter, const wxString& aPluginName,
120 const wxString& aDescription, const wxString& aDebugText )
121{
122 if( !aReporter || ( aDescription.IsEmpty() && aDebugText.IsEmpty() ) )
123 return;
124
126 error.SetTitle( wxString::Format( _( "Error loading plugin '%s'" ), aPluginName ) );
127
128 if( !aDescription.IsEmpty() )
129 error.SetDescription( aDescription );
130
131 if( !aDebugText.IsEmpty() )
132 error.SetDebugText( aDebugText );
133
134 aReporter->Report( error );
135}
136
137
138static void reportPluginActionResult( REPORTER* aReporter, const wxString& aActionName,
139 int aRetVal, const wxString& aError )
140{
141 wxString trimmedError = aError;
142 trimmedError.Trim();
143 trimmedError.Trim( false );
144
145 if( aRetVal != 0 )
146 {
148 error.SetTitle( wxString::Format( _( "Plugin action '%s'" ), aActionName ) );
149 error.SetDescription( wxString::Format( _( "exited with code %d" ), aRetVal ) );
150
151 if( !trimmedError.IsEmpty() )
152 error.SetDebugText( trimmedError );
153
154 if( aReporter )
155 aReporter->Report( error );
156 }
157 else if( !trimmedError.IsEmpty() )
158 {
159 reportPluginActionMessage( aReporter, aActionName, trimmedError );
160 }
161}
162
163
164API_PLUGIN_MANAGER::API_PLUGIN_MANAGER( wxEvtHandler* aEvtHandler ) :
165 wxEvtHandler(),
166 m_parent( aEvtHandler ),
167 m_lastPid( 0 ),
168 m_raiseTimer( nullptr )
169{
170 // Read and store pcm schema
171 wxFileName schemaFile( PATHS::GetStockDataPath( true ), wxS( "api.v1.schema.json" ) );
172 schemaFile.Normalize( FN_NORMALIZE_FLAGS | wxPATH_NORM_ENV_VARS );
173 schemaFile.AppendDir( wxS( "schemas" ) );
174
175 m_schema_validator = std::make_unique<JSON_SCHEMA_VALIDATOR>( schemaFile );
176
178}
179
180
181class PLUGIN_TRAVERSER : public wxDirTraverser
182{
183private:
184 std::function<void( const wxFileName& )> m_action;
185
186public:
187 explicit PLUGIN_TRAVERSER( std::function<void( const wxFileName& )> aAction )
188 : m_action( std::move( aAction ) )
189 {
190 }
191
192 wxDirTraverseResult OnFile( const wxString& aFilePath ) override
193 {
194 wxFileName file( aFilePath );
195
196 if( file.GetFullName() == wxS( "plugin.json" ) )
197 m_action( file );
198
199 return wxDIR_CONTINUE;
200 }
201
202 wxDirTraverseResult OnDir( const wxString& dirPath ) override
203 {
204 return wxDIR_CONTINUE;
205 }
206};
207
208
209void API_PLUGIN_MANAGER::ReloadPlugins( std::optional<wxString> aDirectoryToScan,
210 std::shared_ptr<REPORTER> aReporter )
211{
212 m_reloadReporter = std::move( aReporter );
213
214 m_plugins.clear();
215 m_pluginsCache.clear();
216 m_actionsCache.clear();
217 m_environmentCache.clear();
218 m_buttonBindings.clear();
219 m_menuBindings.clear();
220 m_readyPlugins.clear();
221
222 PLUGIN_TRAVERSER loader(
223 [&]( const wxFileName& aFile )
224 {
225 wxLogTrace( traceApi, wxString::Format( "Manager: loading plugin from %s",
226 aFile.GetFullPath() ) );
227
228 auto plugin = std::make_unique<API_PLUGIN>( aFile, *m_schema_validator );
229
230 if( plugin->IsOk() )
231 {
232 const wxString& id = plugin->Identifier();
233
234 if( m_pluginsCache.contains( id ) )
235 {
236 wxLogTrace( traceApi, wxString::Format( "Manager: identifier %s already present, reloading", id ) );
237
238 for( const PLUGIN_ACTION& action : m_pluginsCache[id]->Actions() )
239 m_actionsCache[action.identifier] = &action;
240
241 m_pluginsCache.erase( id );
242 return;
243 }
244
245 m_pluginsCache[id] = plugin.get();
246
247 for( const PLUGIN_ACTION& action : plugin->Actions() )
248 m_actionsCache[action.identifier] = &action;
249
250 m_plugins.insert( std::move( plugin ) );
251 }
252 else
253 {
254 wxLogTrace( traceApi, "Manager: loading failed" );
255
256 reportPluginLoadMessage( m_reloadReporter.get(), aFile.GetName(),
257 plugin->ErrorMessage() );
258 }
259 } );
260
261 if( aDirectoryToScan )
262 {
263 wxDir customDir( *aDirectoryToScan );
264 wxLogTrace( traceApi, wxString::Format( "Manager: scanning custom path (%s) for plugins...",
265 customDir.GetName() ) );
266 customDir.Traverse( loader );
267 }
268 else
269 {
270 wxDir systemPluginsDir( PATHS::GetStockPluginsPath() );
271
272 if( systemPluginsDir.IsOpened() )
273 {
274 wxLogTrace( traceApi, wxString::Format( "Manager: scanning system path (%s) for plugins...",
275 systemPluginsDir.GetName() ) );
276 systemPluginsDir.Traverse( loader );
277 }
278
279 wxString thirdPartyPath;
280 const ENV_VAR_MAP& env = Pgm().GetLocalEnvVariables();
281
282 if( std::optional<wxString> v = ENV_VAR::GetVersionedEnvVarValue( env, wxT( "3RD_PARTY" ) ) )
283 thirdPartyPath = *v;
284 else
285 thirdPartyPath = PATHS::GetDefault3rdPartyPath();
286
287 wxDir thirdParty( thirdPartyPath );
288
289 if( thirdParty.IsOpened() )
290 {
291 wxLogTrace( traceApi, wxString::Format( "Manager: scanning PCM path (%s) for plugins...",
292 thirdParty.GetName() ) );
293 thirdParty.Traverse( loader );
294 }
295
296 wxDir userPluginsDir( PATHS::GetUserPluginsPath() );
297
298 if( userPluginsDir.IsOpened() )
299 {
300 wxLogTrace( traceApi, wxString::Format( "Manager: scanning user path (%s) for plugins...",
301 userPluginsDir.GetName() ) );
302 userPluginsDir.Traverse( loader );
303 }
304 }
305
307
308 if( !Busy() )
309 m_reloadReporter.reset();
310
311 wxCommandEvent* evt = new wxCommandEvent( EDA_EVT_PLUGIN_AVAILABILITY_CHANGED, wxID_ANY );
312 m_parent->QueueEvent( evt );
313}
314
315
316std::map<int, wxString>& API_PLUGIN_MANAGER::ButtonBindings( FRAME_T aFrame )
317{
318 return m_buttonBindings[aFrame];
319}
320
321
322std::map<int, wxString>& API_PLUGIN_MANAGER::MenuBindings( FRAME_T aFrame )
323{
324 return m_menuBindings[aFrame];
325}
326
327
328bool API_PLUGIN_MANAGER::RecreatePluginEnvironment( const wxString& aIdentifier )
329{
330 if( !m_pluginsCache.contains( aIdentifier ) )
331 return false;
332
333 const API_PLUGIN* plugin = m_pluginsCache.at( aIdentifier );
334 wxCHECK( plugin, false );
335
336 if( plugin->Runtime().type != PLUGIN_RUNTIME_TYPE::PYTHON )
337 return false;
338
339 std::optional<wxString> env = PYTHON_MANAGER::GetPythonEnvironment( plugin->Identifier() );
340 wxCHECK( env.has_value(), false );
341
342 wxFileName envConfigPath( *env, wxS( "pyvenv.cfg" ) );
343 envConfigPath.MakeAbsolute();
344
345 if( envConfigPath.DirExists() && envConfigPath.Rmdir( wxPATH_RMDIR_RECURSIVE ) )
346 {
347 wxLogTrace( traceApi,
348 wxString::Format( "Manager: Removed existing Python environment at %s for %s",
349 envConfigPath.GetPath(), plugin->Identifier() ) );
350
351 JOB job;
353 job.identifier = plugin->Identifier();
354 job.plugin_path = plugin->BasePath();
355 job.env_path = envConfigPath.GetPath();
356 m_jobs.emplace_back( job );
357
358 // Caller should send EDA_EVT_PLUGIN_MANAGER_JOB_FINISHED
359 return true;
360 }
361
362 wxLogTrace( traceApi,
363 wxString::Format( "Manager: could not remove Python environment at %s for %s",
364 envConfigPath.GetPath(), plugin->Identifier() ) );
365
366 return false;
367}
368
369
370std::optional<const PLUGIN_ACTION*> API_PLUGIN_MANAGER::GetAction( const wxString& aIdentifier )
371{
372 if( !m_actionsCache.contains( aIdentifier ) )
373 return std::nullopt;
374
375 return m_actionsCache.at( aIdentifier );
376}
377
378
379void API_PLUGIN_MANAGER::recreateCancelledPlugin( const wxString& aIdentifier )
380{
381 m_cancelledPlugins.erase( aIdentifier );
382 m_busyPlugins.erase( aIdentifier );
383
384 if( !RecreatePluginEnvironment( aIdentifier ) )
385 return;
386
387 m_busyPlugins.insert( aIdentifier );
388
389 wxCommandEvent* evt = new wxCommandEvent( EDA_EVT_PLUGIN_MANAGER_JOB_FINISHED, wxID_ANY );
390 QueueEvent( evt );
391}
392
393
395{
396 bool addedAnyJobs = false;
397
398 for( const std::unique_ptr<API_PLUGIN>& plugin : m_plugins )
399 {
400 if( plugin->Runtime().type != PLUGIN_RUNTIME_TYPE::PYTHON )
401 continue;
402
403 std::optional<wxString> env = PYTHON_MANAGER::GetPythonEnvironment( plugin->Identifier() );
404
405 if( !env )
406 continue;
407
408 if( !PYTHON_MANAGER::IsVenvStale( *env, Pgm().GetCommonSettings()->m_Api.python_interpreter ) )
409 continue;
410
411 m_readyPlugins.erase( plugin->Identifier() );
412
413 if( m_busyPlugins.contains( plugin->Identifier() ) )
414 {
415 wxLogTrace( traceApi, wxString::Format( "Manager: cancelling in-flight job for %s after interpreter change",
416 plugin->Identifier() ) );
417
418 if( !cancelPluginJobs( plugin->Identifier() ) )
419 {
420 // Nothing was actually running: no termination callback will fire, so schedule the recreation directly.
421 recreateCancelledPlugin( plugin->Identifier() );
422 addedAnyJobs = true;
423 }
424 }
425 else
426 {
427 wxLogTrace( traceApi, wxString::Format( "Manager: recreating Python env for %s after interpreter change",
428 plugin->Identifier() ) );
429
430 m_busyPlugins.insert( plugin->Identifier() );
431
432 if( RecreatePluginEnvironment( plugin->Identifier() ) )
433 addedAnyJobs = true;
434 else
435 m_busyPlugins.erase( plugin->Identifier() );
436 }
437 }
438
439 if( addedAnyJobs )
440 {
441 wxCommandEvent* evt = new wxCommandEvent( EDA_EVT_PLUGIN_MANAGER_JOB_FINISHED, wxID_ANY );
442 QueueEvent( evt );
443 }
444}
445
446
447int API_PLUGIN_MANAGER::doInvokeAction( const wxString& aIdentifier,
448 std::vector<wxString> aExtraArgs,
449 bool aSync, wxString* aStdout, wxString* aStderr,
450 std::shared_ptr<REPORTER> aReporter )
451{
452 if( !m_actionsCache.contains( aIdentifier ) )
453 {
454 reportPluginActionMessage( aReporter.get(), aIdentifier, _( "action is not registered" ) );
455 return -1;
456 }
457
458 const PLUGIN_ACTION* action = m_actionsCache.at( aIdentifier );
459 const API_PLUGIN& plugin = action->plugin;
460
461 if( !m_readyPlugins.count( plugin.Identifier() ) )
462 {
463 wxLogTrace( traceApi, wxString::Format( "Manager: Plugin %s is not ready",
464 plugin.Identifier() ) );
465 return -1;
466 }
467
468 wxFileName pluginFile( action->entrypoint );
469 pluginFile.MakeAbsolute( plugin.BasePath() );
470 pluginFile.Normalize( wxPATH_NORM_ABSOLUTE | wxPATH_NORM_SHORTCUT | wxPATH_NORM_DOTS
471 | wxPATH_NORM_TILDE, plugin.BasePath() );
472 wxString pluginPath = pluginFile.GetFullPath();
473
474 std::vector<const wchar_t*> args;
475 std::optional<wxString> py;
476
477 switch( plugin.Runtime().type )
478 {
480 {
482
483 if( !py )
484 {
485 wxLogTrace( traceApi, wxString::Format( "Manager: Python interpreter for %s not found",
486 plugin.Identifier() ) );
487 reportPluginActionMessage( aReporter.get(), action->name,
488 _( "missing plugin environment" ) );
489 return -1;
490 }
491
492 if( !pluginFile.IsFileReadable() )
493 {
494 wxLogTrace( traceApi, wxString::Format( "Manager: Python entrypoint %s is not readable",
495 pluginFile.GetFullPath() ) );
496 reportPluginActionMessage( aReporter.get(), action->name,
497 wxString::Format( _( "entrypoint '%s' could not be read" ),
498 pluginFile.GetFullPath() ) );
499 return -1;
500 }
501
502 std::optional<wxString> pythonHome =
504
505 PYTHON_MANAGER manager( *py );
506 wxExecuteEnv env;
507 wxGetEnvMap( &env.env );
508
509 if( Pgm().ApiServerOrNull() )
510 {
511 env.env[wxS( "KICAD_API_SOCKET" )] = Pgm().GetApiServer().SocketPath();
512 env.env[wxS( "KICAD_API_TOKEN" )] = Pgm().GetApiServer().Token();
513 }
514
515 env.cwd = pluginFile.GetPath();
516
517#ifdef _WIN32
518 wxString systemRoot;
519 wxGetEnv( wxS( "SYSTEMROOT" ), &systemRoot );
520 env.env[wxS( "SYSTEMROOT" )] = systemRoot;
521
522 if( Pgm().GetCommonSettings()->m_Api.python_interpreter == FindKicadFile( "pythonw.exe" )
523 || wxGetEnv( wxT( "KICAD_RUN_FROM_BUILD_DIR" ), nullptr ) )
524 {
525 wxLogTrace( traceApi, "Configured Python is the KiCad one; erasing path overrides..." );
526 env.env.erase( "PYTHONHOME" );
527 env.env.erase( "PYTHONPATH" );
528 }
529#endif
530
531 if( pythonHome )
532 env.env[wxS( "VIRTUAL_ENV" )] = *pythonHome;
533
534 std::vector<wxString> pyArgs( action->args );
535 pyArgs.emplace_back( pluginFile.GetFullPath() );
536 pyArgs.insert( pyArgs.end(), aExtraArgs.begin(), aExtraArgs.end() );
537
538 if( aSync )
539 {
540 wxString stdOut;
541 wxString stdErr;
542 wxString* stdoutSink = aStdout ? aStdout : &stdOut;
543 wxString* stderrSink = aStderr ? aStderr : &stdErr;
544 int ret = manager.ExecuteSync( pyArgs, stdoutSink, stderrSink, &env );
545 reportPluginActionResult( aReporter.get(), action->name, ret, *stderrSink );
546 return ret;
547 }
548
549 [[maybe_unused]] long pid = manager.Execute( pyArgs,
550 [aReporter, actionName = action->name]( int aRetVal, const wxString& aOutput,
551 const wxString& aError )
552 {
553 wxLogTrace( traceApi,
554 wxString::Format( "Manager: action exited with code %d", aRetVal ) );
555
556 if( !aError.IsEmpty() )
557 wxLogTrace( traceApi, wxString::Format( "Manager: action stderr: %s", aError ) );
558
559 reportPluginActionResult( aReporter.get(), actionName, aRetVal, aError );
560 },
561 &env, true );
562
563 if( !pid )
564 {
565 reportPluginActionMessage( aReporter.get(), action->name,
566 _( "process could not be created" ) );
567 return -1;
568 }
569
570#ifdef __WXMAC__
571 if( pid )
572 {
573 if( !m_raiseTimer )
574 {
575 m_raiseTimer = new wxTimer( this );
576
577 Bind( wxEVT_TIMER,
578 [&]( wxTimerEvent& )
579 {
580 wxString script = wxString::Format(
581 wxS( "tell application \"System Events\"\n"
582 " set plist to every process whose unix id is %ld\n"
583 " repeat with proc in plist\n"
584 " set the frontmost of proc to true\n"
585 " end repeat\n"
586 "end tell" ), m_lastPid );
587
588 wxString cmd = wxString::Format( "osascript -e '%s'", script );
589 wxLogTrace( traceApi, wxString::Format( "Execute: %s", cmd ) );
590 wxExecute( cmd );
591 },
592 m_raiseTimer->GetId() );
593 }
594
595 m_lastPid = pid;
596 m_raiseTimer->StartOnce( 250 );
597 }
598#endif
599
600 break;
601 }
602
604 {
605 if( !pluginFile.IsFileExecutable() )
606 {
607 wxLogTrace( traceApi, wxString::Format( "Manager: Exec entrypoint %s is not executable",
608 pluginFile.GetFullPath() ) );
609 reportPluginActionMessage( aReporter.get(), action->name,
610 wxString::Format( _( "entrypoint '%s' is not executable" ),
611 pluginFile.GetFullPath() ) );
612 return -1;
613 }
614
615 wxExecuteEnv env;
616 wxGetEnvMap( &env.env );
617
618 if( Pgm().ApiServerOrNull() )
619 {
620 env.env[wxS( "KICAD_API_SOCKET" )] = Pgm().GetApiServer().SocketPath();
621 env.env[wxS( "KICAD_API_TOKEN" )] = Pgm().GetApiServer().Token();
622 }
623
624 env.cwd = pluginFile.GetPath();
625
626 long pidOrRetCode = 0;
627
628 if( aSync )
629 {
630 wxString cmd = pluginPath;
631
632 for( const wxString& arg : action->args )
633 cmd << " " << arg;
634
635 for( const wxString& arg : aExtraArgs )
636 cmd << " " << arg;
637
638 wxArrayString out, err;
639
640 pidOrRetCode = wxExecute( cmd, out, err, wxEXEC_BLOCK, &env );
641
642 if( aStdout )
643 {
644 for( const wxString& line : out )
645 *aStdout << line << "\n";
646 }
647
648 wxString stdErr;
649
650 for( const wxString& line : err )
651 stdErr << line << "\n";
652
653 if( aStderr )
654 *aStderr = stdErr;
655
656 reportPluginActionResult( aReporter.get(), action->name, pidOrRetCode, stdErr );
657 return pidOrRetCode;
658 }
659 else
660 {
662 [aReporter, actionName = action->name]( int aRetVal, const wxString& aOutput,
663 const wxString& aError )
664 {
665 wxLogTrace( traceApi,
666 wxString::Format( "Manager: action exited with code %d", aRetVal ) );
667
668 if( !aError.IsEmpty() )
669 wxLogTrace( traceApi,
670 wxString::Format( "Manager: action stderr: %s", aError ) );
671
672 reportPluginActionResult( aReporter.get(), actionName, aRetVal, aError );
673 } );
674
675 process->Redirect();
676 args.emplace_back( pluginPath.wc_str() );
677
678 for( const wxString& arg : action->args )
679 args.emplace_back( arg.wc_str() );
680
681 for( const wxString& arg : aExtraArgs )
682 args.emplace_back( arg.wc_str() );
683
684 args.emplace_back( nullptr );
685
686 pidOrRetCode = wxExecute( const_cast<wchar_t**>( args.data() ),
687 wxEXEC_ASYNC | wxEXEC_HIDE_CONSOLE, process, &env );
688
689 if( !pidOrRetCode )
690 delete process;
691 }
692
693 if( !pidOrRetCode )
694 {
695 wxLogTrace( traceApi, wxString::Format( "Manager: launching action %s failed",
696 action->identifier ) );
697 reportPluginActionMessage( aReporter.get(), action->name, _( "could not launch plugin" ) );
698 }
699 else
700 {
701 wxLogTrace( traceApi, wxString::Format( "Manager: launching action %s -> pid %ld",
702 action->identifier, pidOrRetCode ) );
703 }
704 break;
705 }
706
707 default:
708 wxLogTrace( traceApi, wxString::Format( "Manager: unhandled runtime for action %s",
709 action->identifier ) );
710 }
711
712 return -1;
713}
714
715
716void API_PLUGIN_MANAGER::InvokeAction( const wxString& aIdentifier,
717 std::shared_ptr<REPORTER> aReporter )
718{
719 doInvokeAction( aIdentifier, {}, false, nullptr, nullptr, std::move( aReporter ) );
720}
721
722
723int API_PLUGIN_MANAGER::InvokeActionSync( const wxString& aIdentifier, std::vector<wxString> aExtraArgs,
724 wxString* aStdout, wxString* aStderr,
725 std::shared_ptr<REPORTER> aReporter )
726{
727 return doInvokeAction( aIdentifier, aExtraArgs, true, aStdout, aStderr,
728 std::move( aReporter ) );
729}
730
731
732std::vector<const PLUGIN_ACTION*> API_PLUGIN_MANAGER::GetActionsForScope( PLUGIN_ACTION_SCOPE aScope )
733{
734 std::vector<const PLUGIN_ACTION*> actions;
735
736 for( auto& [identifier, action] : m_actionsCache )
737 {
738 if( !m_readyPlugins.count( action->plugin.Identifier() ) )
739 continue;
740
741 if( action->scopes.count( aScope ) )
742 actions.emplace_back( action );
743 }
744
745 return actions;
746}
747
748
749wxString API_PLUGIN_MANAGER::pluginName( const wxString& aIdentifier ) const
750{
751 if( m_pluginsCache.contains( aIdentifier ) )
752 return m_pluginsCache.at( aIdentifier )->Name();
753
754 return aIdentifier;
755}
756
757
758bool API_PLUGIN_MANAGER::cancelPluginJobs( const wxString& aIdentifier )
759{
760 std::erase_if( m_jobs,
761 [&aIdentifier]( const JOB& aJob )
762 {
763 return aJob.identifier == aIdentifier;
764 } );
765
766 long pid = m_runningPids[aIdentifier];
767 m_runningPids.erase( aIdentifier );
768
769 if( pid != 0 )
770 wxKill( pid, wxSIGTERM, nullptr, wxKILL_CHILDREN );
771
772 m_cancelledPlugins.insert( aIdentifier );
773
774 return pid != 0;
775}
776
777
779{
780 bool addedAnyJobs = false;
781
782 for( const std::unique_ptr<API_PLUGIN>& plugin : m_plugins )
783 {
784 if( m_busyPlugins.contains( plugin->Identifier() ) )
785 continue;
786
787 wxLogTrace( traceApi, wxString::Format( "Manager: processing dependencies for %s",
788 plugin->Identifier() ) );
789 m_environmentCache[plugin->Identifier()] = wxEmptyString;
790
791 if( plugin->Runtime().type != PLUGIN_RUNTIME_TYPE::PYTHON )
792 {
793 wxLogTrace( traceApi, wxString::Format( "Manager: %s is not a Python plugin, all set",
794 plugin->Identifier() ) );
795 m_readyPlugins.insert( plugin->Identifier() );
796 continue;
797 }
798
799 std::optional<wxString> env = PYTHON_MANAGER::GetPythonEnvironment( plugin->Identifier() );
800
801 if( !env )
802 {
803 wxLogTrace( traceApi, wxString::Format( "Manager: could not create env for %s",
804 plugin->Identifier() ) );
805 continue;
806 }
807
808 m_busyPlugins.insert( plugin->Identifier() );
809
810 wxFileName envConfigPath( *env, wxS( "pyvenv.cfg" ) );
811 envConfigPath.MakeAbsolute();
812
813 if( envConfigPath.IsFileReadable() )
814 {
815 bool broken = !PYTHON_MANAGER::IsVenvUsable( *env );
816 bool stale = PYTHON_MANAGER::IsVenvStale( *env, Pgm().GetCommonSettings()->m_Api.python_interpreter );
817
818 if( broken || stale )
819 {
820 wxLogTrace( traceApi, wxString::Format( "Manager: Python env for %s at %s needs recreation (%s)",
821 plugin->Identifier(), envConfigPath.GetPath(),
822 broken ? "broken" : "interpreter changed" ) );
823
824 if( RecreatePluginEnvironment( plugin->Identifier() ) )
825 {
826 addedAnyJobs = true;
827 continue;
828 }
829
830 wxLogTrace( traceApi, wxString::Format( "Manager: could not remove env for %s; "
831 "scheduling creation anyway",
832 plugin->Identifier() ) );
833 }
834 else
835 {
836 wxLogTrace( traceApi, wxString::Format( "Manager: Python env for %s exists at %s", plugin->Identifier(),
837 envConfigPath.GetPath() ) );
838
839 JOB job;
841 job.identifier = plugin->Identifier();
842 job.plugin_path = plugin->BasePath();
843 job.env_path = envConfigPath.GetPath();
844 m_jobs.emplace_back( job );
845 addedAnyJobs = true;
846 continue;
847 }
848 }
849
850 wxLogTrace( traceApi, wxString::Format( "Manager: will create Python env for %s at %s",
851 plugin->Identifier(), envConfigPath.GetPath() ) );
852 JOB job;
854 job.identifier = plugin->Identifier();
855 job.plugin_path = plugin->BasePath();
856 job.env_path = envConfigPath.GetPath();
857 m_jobs.emplace_back( job );
858 addedAnyJobs = true;
859 }
860
861 if( addedAnyJobs )
862 {
863 wxCommandEvent* evt = new wxCommandEvent( EDA_EVT_PLUGIN_MANAGER_JOB_FINISHED, wxID_ANY );
864 QueueEvent( evt );
865 }
866}
867
868
869void API_PLUGIN_MANAGER::processNextJob( wxCommandEvent& aEvent )
870{
871 if( m_jobs.empty() )
872 {
873 wxLogTrace( traceApi, "Manager: no more jobs to process" );
874 return;
875 }
876
877 wxLogTrace( traceApi, wxString::Format( "Manager: begin processing; %zu jobs left in queue",
878 m_jobs.size() ) );
879
880 JOB& job = m_jobs.front();
881
882 if( job.type == JOB_TYPE::CREATE_ENV )
883 {
884 wxLogTrace( traceApi, "Manager: Using Python interpreter at %s",
885 Pgm().GetCommonSettings()->m_Api.python_interpreter );
886 wxLogTrace( traceApi, wxString::Format( "Manager: creating Python env at %s",
887 job.env_path ) );
888 PYTHON_MANAGER manager( Pgm().GetCommonSettings()->m_Api.python_interpreter );
889 wxExecuteEnv env;
890
891#ifdef _WIN32
892 wxString systemRoot;
893 wxGetEnv( wxS( "SYSTEMROOT" ), &systemRoot );
894 env.env[wxS( "SYSTEMROOT" )] = systemRoot;
895
896 if( Pgm().GetCommonSettings()->m_Api.python_interpreter == FindKicadFile( "pythonw.exe" )
897 || wxGetEnv( wxT( "KICAD_RUN_FROM_BUILD_DIR" ), nullptr ) )
898 {
899 wxLogTrace( traceApi, "Configured Python is the KiCad one; erasing path overrides..." );
900 env.env.erase( "PYTHONHOME" );
901 env.env.erase( "PYTHONPATH" );
902 }
903#endif
904 std::vector<wxString> args = {
905 "-m",
906 "venv",
907 "--system-site-packages",
908 job.env_path
909 };
910
911 long pid = manager.Execute( args,
912 [this, job]( int aRetVal, const wxString& aOutput, const wxString& aError )
913 {
914 m_runningPids.erase( job.identifier );
915
916 wxLogTrace( traceApi,
917 wxString::Format( "Manager: created venv (python returned %d)", aRetVal ) );
918
919 if( !aError.IsEmpty() )
920 wxLogTrace( traceApi, wxString::Format( "Manager: venv err: %s", aError ) );
921
922 if( m_cancelledPlugins.contains( job.identifier ) )
923 {
925 return;
926 }
927
928 if( aRetVal != 0 )
929 {
930 wxString error = aError;
931 error.Trim().Trim( false );
932
933 if( error.IsEmpty() )
934 error = wxString::Format( _( "error code %d" ), aRetVal );
935
937 _( "Could not create plugin environment" ), error );
938 }
939
940 wxCommandEvent* evt = new wxCommandEvent( EDA_EVT_PLUGIN_MANAGER_JOB_FINISHED, wxID_ANY );
941 QueueEvent( evt );
942 }, &env );
943
944 m_runningPids[job.identifier] = pid;
945
946 JOB nextJob( job );
947 nextJob.type = JOB_TYPE::SETUP_ENV;
948 m_jobs.emplace_back( nextJob );
949 }
950 else if( job.type == JOB_TYPE::SETUP_ENV )
951 {
952 wxLogTrace( traceApi, wxString::Format( "Manager: setting up environment for %s",
953 job.plugin_path ) );
954
955 std::optional<wxString> pythonHome = PYTHON_MANAGER::GetPythonEnvironment( job.identifier );
956 std::optional<wxString> python = PYTHON_MANAGER::GetVirtualPython( job.identifier );
957
958 if( m_cancelledPlugins.contains( job.identifier ) )
959 {
961 }
962 else if( !python )
963 {
964 wxString debug = wxString::Format( wxS( "Python binary not found at %s" ), job.env_path );
965 wxLogTrace( traceApi, wxString::Format( "Manager: error: %s", debug ) );
966
968 _( "Missing plugin environment" ), debug );
969
970 m_busyPlugins.erase( job.identifier );
971 wxCommandEvent* evt = new wxCommandEvent( EDA_EVT_PLUGIN_MANAGER_JOB_FINISHED, wxID_ANY );
972 QueueEvent( evt );
973 }
974 else
975 {
976 PYTHON_MANAGER manager( *python );
977 wxExecuteEnv env;
978
979 if( pythonHome )
980 env.env[wxS( "VIRTUAL_ENV" )] = *pythonHome;
981
982#ifdef _WIN32
983 wxString systemRoot;
984 wxGetEnv( wxS( "SYSTEMROOT" ), &systemRoot );
985 env.env[wxS( "SYSTEMROOT" )] = systemRoot;
986
987 if( Pgm().GetCommonSettings()->m_Api.python_interpreter
988 == FindKicadFile( "pythonw.exe" )
989 || wxGetEnv( wxT( "KICAD_RUN_FROM_BUILD_DIR" ), nullptr ) )
990 {
991 wxLogTrace( traceApi,
992 "Configured Python is the KiCad one; erasing path overrides..." );
993 env.env.erase( "PYTHONHOME" );
994 env.env.erase( "PYTHONPATH" );
995 }
996#endif
997
998 std::vector<wxString> args = {
999 "-m",
1000 "pip",
1001 "install",
1002 "--upgrade",
1003 "pip"
1004 };
1005
1006 long pid = manager.Execute( args,
1007 [this, job]( int aRetVal, const wxString& aOutput, const wxString& aError )
1008 {
1009 m_runningPids.erase( job.identifier );
1010
1011 wxLogTrace( traceApi, wxString::Format( "Manager: upgrade pip returned %d",
1012 aRetVal ) );
1013
1014 if( !aError.IsEmpty() )
1015 {
1016 wxLogTrace( traceApi,
1017 wxString::Format( "Manager: upgrade pip stderr: %s", aError ) );
1018 }
1019
1020 if( m_cancelledPlugins.contains( job.identifier ) )
1021 {
1023 return;
1024 }
1025
1026 if( aRetVal != 0 )
1027 {
1028 wxString error = aError;
1029 error.Trim().Trim( false );
1030
1031 if( error.IsEmpty() )
1032 error = wxString::Format( _( "error code %d" ), aRetVal );
1033
1035 _( "Could not create plugin environment" ), error );
1036 }
1037
1038 wxCommandEvent* evt =
1039 new wxCommandEvent( EDA_EVT_PLUGIN_MANAGER_JOB_FINISHED, wxID_ANY );
1040 QueueEvent( evt );
1041 }, &env );
1042
1043 m_runningPids[job.identifier] = pid;
1044
1045 JOB nextJob( job );
1047 m_jobs.emplace_back( nextJob );
1048 }
1049 }
1050 else if( job.type == JOB_TYPE::INSTALL_REQUIREMENTS )
1051 {
1052 wxLogTrace( traceApi, wxString::Format( "Manager: installing dependencies for %s",
1053 job.plugin_path ) );
1054
1055 std::optional<wxString> pythonHome = PYTHON_MANAGER::GetPythonEnvironment( job.identifier );
1056 std::optional<wxString> python = PYTHON_MANAGER::GetVirtualPython( job.identifier );
1057 wxFileName reqs = wxFileName( job.plugin_path, "requirements.txt" );
1058
1059 if( m_cancelledPlugins.contains( job.identifier ) )
1060 {
1062 }
1063 else if( !python )
1064 {
1065 wxString debug = wxString::Format( wxS( "Python binary not found at %s" ), job.env_path );
1066 wxLogTrace( traceApi, wxString::Format( "Manager: error: %s", debug ) );
1067
1069 _( "Missing plugin environment" ), debug );
1070
1071 m_busyPlugins.erase( job.identifier );
1072 }
1073 else if( !reqs.IsFileReadable() )
1074 {
1075 wxLogTrace( traceApi,
1076 wxString::Format( "Manager: error: requirements.txt not found at %s",
1077 job.plugin_path ) );
1078 wxString debug = wxString::Format( wxS( "Expected at %s" ), reqs.GetFullPath() );
1079
1081 _( "requirements.txt could not be read" ), debug );
1082
1083 // No requirements to install; the plugin is complete.
1084 m_readyPlugins.insert( job.identifier );
1085 m_busyPlugins.erase( job.identifier );
1086
1087 wxCommandEvent* availabilityEvt = new wxCommandEvent( EDA_EVT_PLUGIN_AVAILABILITY_CHANGED, wxID_ANY );
1088 wxTheApp->QueueEvent( availabilityEvt );
1089 }
1090 else
1091 {
1092 wxLogTrace( traceApi, "Manager: Python exe '%s'", *python );
1093
1094 PYTHON_MANAGER manager( *python );
1095 wxExecuteEnv env;
1096
1097#ifdef _WIN32
1098 wxString systemRoot;
1099 wxGetEnv( wxS( "SYSTEMROOT" ), &systemRoot );
1100 env.env[wxS( "SYSTEMROOT" )] = systemRoot;
1101
1102 // If we are using the KiCad-shipped Python interpreter we have to do hacks
1103 env.env.erase( "PYTHONHOME" );
1104 env.env.erase( "PYTHONPATH" );
1105#endif
1106
1107 if( pythonHome )
1108 env.env[wxS( "VIRTUAL_ENV" )] = *pythonHome;
1109
1110 std::vector<wxString> args = {
1111 "-m",
1112 "pip",
1113 "install",
1114 "--no-input",
1115 "--isolated",
1116 "--only-binary",
1117 ":all:",
1118 "--require-virtualenv",
1119 "--exists-action",
1120 "i",
1121 "-r",
1122 reqs.GetFullPath()
1123 };
1124
1125 long pid = manager.Execute( args,
1126 [this, job]( int aRetVal, const wxString& aOutput, const wxString& aError )
1127 {
1128 m_runningPids.erase( job.identifier );
1129
1130 if( m_cancelledPlugins.contains( job.identifier ) )
1131 {
1133 return;
1134 }
1135
1136 if( !aError.IsEmpty() )
1137 wxLogTrace( traceApi, wxString::Format( "Manager: pip stderr: %s", aError ) );
1138
1139 if( aRetVal != 0 )
1140 {
1141 wxString error = aError;
1142 error.Trim().Trim( false );
1143
1144 if( error.IsEmpty() )
1145 error = wxString::Format( _( "error code %d" ), aRetVal );
1146
1148 _( "Could not create plugin environment" ), error );
1149 }
1150
1151 if( aRetVal == 0 )
1152 {
1153 wxLogTrace( traceApi, wxString::Format( "Manager: marking %s as ready",
1154 job.identifier ) );
1155 m_readyPlugins.insert( job.identifier );
1156
1157 wxCommandEvent* availabilityEvt =
1158 new wxCommandEvent( EDA_EVT_PLUGIN_AVAILABILITY_CHANGED, wxID_ANY );
1159 wxTheApp->QueueEvent( availabilityEvt );
1160 }
1161
1162 m_busyPlugins.erase( job.identifier );
1163
1164 wxCommandEvent* evt = new wxCommandEvent( EDA_EVT_PLUGIN_MANAGER_JOB_FINISHED,
1165 wxID_ANY );
1166
1167 QueueEvent( evt );
1168 }, &env );
1169
1170 m_runningPids[job.identifier] = pid;
1171 }
1172
1173 wxCommandEvent* evt = new wxCommandEvent( EDA_EVT_PLUGIN_MANAGER_JOB_FINISHED, wxID_ANY );
1174 QueueEvent( evt );
1175 }
1176
1177 m_jobs.pop_front();
1178
1179 if( !Busy() )
1180 m_reloadReporter.reset();
1181
1182 wxLogTrace( traceApi, wxString::Format( "Manager: finished job; %zu left in queue",
1183 m_jobs.size() ) );
1184}
1185
1186
1188{
1189 return !m_jobs.empty() || !m_busyPlugins.empty();
1190}
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.
const KICOMMON_API wxEventTypeTag< wxCommandEvent > EDA_EVT_PLUGIN_MANAGER_JOB_FINISHED
Event used for marking async plugin setup tasks as done.
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.
bool RecreatePluginEnvironment(const wxString &aIdentifier)
Removes the given plugin's virtual environment (if any) and schedules a new one to be created with th...
bool cancelPluginJobs(const wxString &aIdentifier)
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 recreateCancelledPlugin(const wxString &aIdentifier)
void InvokeAction(const wxString &aIdentifier, std::shared_ptr< REPORTER > aReporter=nullptr)
std::set< wxString > m_cancelledPlugins
Plugins whose setup jobs were cancelled and need their environment recreated.
std::unique_ptr< JSON_SCHEMA_VALIDATOR > m_schema_validator
std::deque< JOB > m_jobs
int doInvokeAction(const wxString &aIdentifier, std::vector< wxString > aExtraArgs, bool aSync=false, wxString *aStdout=nullptr, wxString *aStderr=nullptr, std::shared_ptr< REPORTER > aReporter=nullptr)
std::vector< const PLUGIN_ACTION * > GetActionsForScope(PLUGIN_ACTION_SCOPE aScope)
std::map< FRAME_T, std::map< int, wxString > > m_menuBindings
Map of menu wx item id to action identifier, per frame.
std::map< int, wxString > & ButtonBindings(FRAME_T aFrame)
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::map< FRAME_T, std::map< int, wxString > > m_buttonBindings
Map of button wx item id to action identifier, per frame.
std::set< wxString > m_readyPlugins
std::map< wxString, long > m_runningPids
Map of plugin identifier to the pid of its currently-running setup process, if any.
std::map< wxString, const PLUGIN_ACTION * > m_actionsCache
API_PLUGIN_MANAGER(wxEvtHandler *aParent)
std::set< wxString > m_busyPlugins
std::map< int, wxString > & MenuBindings(FRAME_T aFrame)
A plugin that is invoked by KiCad and runs as an external process; communicating with KiCad via the I...
Definition api_plugin.h:98
const PLUGIN_RUNTIME & Runtime() const
const wxString & Identifier() const
wxString BasePath() const
const std::string & Token() const
Definition api_server.h:83
std::string SocketPath() const
Holds a structured error message.
Definition ki_error.h:38
KI_ERROR & SetDebugText(const wxString &aDebugText)
Definition ki_error.h:70
KI_ERROR & SetDescription(const wxString &aDescription)
Definition ki_error.h:64
KI_ERROR & SetTitle(const wxString &aTitle)
Definition ki_error.h:58
static wxString GetUserPluginsPath()
Gets the user path for plugins.
Definition paths.cpp:49
static wxString GetStockPluginsPath()
Gets the stock (install) plugins path.
Definition paths.cpp:377
static wxString GetDefault3rdPartyPath()
Gets the default path for PCM packages.
Definition paths.cpp:126
static wxString GetStockDataPath(bool aRespectRunFromBuildDir=true)
Gets the stock (install) data path, which is the base path for things like scripting,...
Definition paths.cpp:233
virtual ENV_VAR_MAP & GetLocalEnvVariables() const
Definition pgm_base.cpp:792
KICAD_API_SERVER & GetApiServer()
Definition pgm_base.h:141
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 bool IsVenvUsable(const wxString &aEnvPath)
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)
static bool IsVenvStale(const wxString &aEnvPath, const wxString &aConfiguredInterpreter)
Returns true if the venv at aEnvPath was created by an interpreter other than aConfiguredInterpreter.
A pure virtual class used to derive REPORTER objects from.
Definition reporter.h:73
virtual REPORTER & Report(const wxString &aText, SEVERITY aSeverity=RPT_SEVERITY_UNDEFINED)
Report a string with a given severity.
Definition reporter.h:102
#define _(s)
Functions related to environment variables, including help functions.
FRAME_T
The set of EDA_BASE_FRAME derivatives, typically stored in EDA_BASE_FRAME::m_Ident.
Definition frame_type.h:29
wxString FindKicadFile(const wxString &shortname)
Search the executable file shortname in KiCad binary path and return full file name if found or short...
Definition gestfich.cpp:67
const wxChar *const traceApi
Flag to enable debug output related to the IPC API and its plugin system.
Definition api_utils.cpp:33
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.
Definition env_vars.cpp:103
STL namespace.
static PGM_BASE * process
PGM_BASE & Pgm()
The global program "get" accessor.
see class PGM_BASE
PLUGIN_ACTION_SCOPE
@ RPT_SEVERITY_ERROR
An action performed by a plugin via the IPC API.
Definition api_plugin.h:74
const API_PLUGIN & plugin
Definition api_plugin.h:89
wxString name
Definition api_plugin.h:80
wxString identifier
Definition api_plugin.h:79
wxString entrypoint
Definition api_plugin.h:83
std::vector< wxString > args
Definition api_plugin.h:85
PLUGIN_RUNTIME_TYPE type
Definition api_plugin.h:64
#define FN_NORMALIZE_FLAGS
Default flags to pass to wxFileName::Normalize().
Definition wx_filename.h:35