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 <env_vars.h>
24#include <fmt/format.h>
25#include <wx/dir.h>
26#include <wx/log.h>
27#include <wx/process.h>
28#include <wx/timer.h>
29#include <wx/utils.h>
30
32#include <api/api_server.h>
33#include <api/api_utils.h>
34#include <gestfich.h>
35#include <paths.h>
36#include <pgm_base.h>
37#include <api/python_manager.h>
38#include <reporter.h>
41
42
43wxDEFINE_EVENT( EDA_EVT_PLUGIN_MANAGER_JOB_FINISHED, wxCommandEvent );
45
46
47class ACTION_PROCESS : public wxProcess
48{
49public:
50 ACTION_PROCESS( std::function<void( int, const wxString&, const wxString& )> aCallback ) :
51 wxProcess(),
52 m_callback( std::move( aCallback ) )
53 {}
54
55 void OnTerminate( int aPid, int aStatus ) override
56 {
57 if( m_callback )
58 {
59 wxString output, error;
60
61 if( wxInputStream* processOut = GetInputStream() )
62 {
63 while( processOut->CanRead() )
64 {
65 char buffer[4096];
66 buffer[ processOut->Read( buffer, sizeof( buffer ) - 1 ).LastRead() ] = '\0';
67 output.append( buffer, processOut->LastRead() );
68 }
69 }
70
71 if( wxInputStream* processErr = GetErrorStream() )
72 {
73 while( processErr->CanRead() )
74 {
75 char buffer[4096];
76 buffer[ processErr->Read( buffer, sizeof( buffer ) - 1 ).LastRead() ] = '\0';
77 error.append( buffer, processErr->LastRead() );
78 }
79 }
80
81 m_callback( aStatus, output, error );
82 }
83
84 wxProcess::OnTerminate( aPid, aStatus );
85 }
86
87private:
88 std::function<void( int, const wxString&, const wxString& )> m_callback;
89};
90
91
92static void reportPluginActionMessage( REPORTER* aReporter, const wxString& aActionName,
93 const wxString& aMessage )
94{
95 if( !aReporter || aMessage.IsEmpty() )
96 return;
97
98 aReporter->Report( wxString::Format( _( "Plugin action '%s': %s" ), aActionName, aMessage ),
100}
101
102
103static void reportPluginLoadMessage( REPORTER* aReporter, const wxString& aPluginName,
104 const wxString& aMessage )
105{
106 if( !aReporter || aMessage.IsEmpty() )
107 return;
108
109 aReporter->Report( wxString::Format( _( "Plugin '%s': %s" ), aPluginName, aMessage ),
111}
112
113
114static void reportPluginLoadMessage( REPORTER* aReporter, const wxString& aPluginName,
115 const wxString& aDescription, const wxString& aDebugText )
116{
117 if( !aReporter || ( aDescription.IsEmpty() && aDebugText.IsEmpty() ) )
118 return;
119
121 error.SetTitle( wxString::Format( _( "Error loading plugin '%s'" ), aPluginName ) );
122
123 if( !aDescription.IsEmpty() )
124 error.SetDescription( aDescription );
125
126 if( !aDebugText.IsEmpty() )
127 error.SetDebugText( aDebugText );
128
129 aReporter->Report( error );
130}
131
132
133static void reportPluginActionResult( REPORTER* aReporter, const wxString& aActionName,
134 int aRetVal, const wxString& aError )
135{
136 wxString trimmedError = aError;
137 trimmedError.Trim();
138 trimmedError.Trim( false );
139
140 if( aRetVal != 0 )
141 {
142 reportPluginActionMessage( aReporter, aActionName,
143 wxString::Format( _( "exited with code %d" ), aRetVal ) );
144 }
145
146 if( !trimmedError.IsEmpty() )
147 reportPluginActionMessage( aReporter, aActionName, trimmedError );
148}
149
150
151API_PLUGIN_MANAGER::API_PLUGIN_MANAGER( wxEvtHandler* aEvtHandler ) :
152 wxEvtHandler(),
153 m_parent( aEvtHandler ),
154 m_lastPid( 0 ),
155 m_raiseTimer( nullptr )
156{
157 // Read and store pcm schema
158 wxFileName schemaFile( PATHS::GetStockDataPath( true ), wxS( "api.v1.schema.json" ) );
159 schemaFile.Normalize( FN_NORMALIZE_FLAGS | wxPATH_NORM_ENV_VARS );
160 schemaFile.AppendDir( wxS( "schemas" ) );
161
162 m_schema_validator = std::make_unique<JSON_SCHEMA_VALIDATOR>( schemaFile );
163
164 Bind( EDA_EVT_PLUGIN_MANAGER_JOB_FINISHED, &API_PLUGIN_MANAGER::processNextJob, this );
165}
166
167
168class PLUGIN_TRAVERSER : public wxDirTraverser
169{
170private:
171 std::function<void( const wxFileName& )> m_action;
172
173public:
174 explicit PLUGIN_TRAVERSER( std::function<void( const wxFileName& )> aAction )
175 : m_action( std::move( aAction ) )
176 {
177 }
178
179 wxDirTraverseResult OnFile( const wxString& aFilePath ) override
180 {
181 wxFileName file( aFilePath );
182
183 if( file.GetFullName() == wxS( "plugin.json" ) )
184 m_action( file );
185
186 return wxDIR_CONTINUE;
187 }
188
189 wxDirTraverseResult OnDir( const wxString& dirPath ) override
190 {
191 return wxDIR_CONTINUE;
192 }
193};
194
195
196void API_PLUGIN_MANAGER::ReloadPlugins( std::optional<wxString> aDirectoryToScan,
197 std::shared_ptr<REPORTER> aReporter )
198{
199 m_reloadReporter = std::move( aReporter );
200
201 m_plugins.clear();
202 m_pluginsCache.clear();
203 m_actionsCache.clear();
204 m_environmentCache.clear();
205 m_buttonBindings.clear();
206 m_menuBindings.clear();
207 m_readyPlugins.clear();
208
209 PLUGIN_TRAVERSER loader(
210 [&]( const wxFileName& aFile )
211 {
212 wxLogTrace( traceApi, wxString::Format( "Manager: loading plugin from %s",
213 aFile.GetFullPath() ) );
214
215 auto plugin = std::make_unique<API_PLUGIN>( aFile, *m_schema_validator );
216
217 if( plugin->IsOk() )
218 {
219 const wxString& id = plugin->Identifier();
220
221 if( m_pluginsCache.contains( id ) )
222 {
223 wxLogTrace( traceApi, wxString::Format( "Manager: identifier %s already present, reloading", id ) );
224
225 for( const PLUGIN_ACTION& action : m_pluginsCache[id]->Actions() )
226 m_actionsCache[action.identifier] = &action;
227
228 m_pluginsCache.erase( id );
229 return;
230 }
231
232 m_pluginsCache[id] = plugin.get();
233
234 for( const PLUGIN_ACTION& action : plugin->Actions() )
235 m_actionsCache[action.identifier] = &action;
236
237 m_plugins.insert( std::move( plugin ) );
238 }
239 else
240 {
241 wxLogTrace( traceApi, "Manager: loading failed" );
242
243 reportPluginLoadMessage( m_reloadReporter.get(), aFile.GetName(),
244 plugin->ErrorMessage() );
245 }
246 } );
247
248 if( aDirectoryToScan )
249 {
250 wxDir customDir( *aDirectoryToScan );
251 wxLogTrace( traceApi, wxString::Format( "Manager: scanning custom path (%s) for plugins...",
252 customDir.GetName() ) );
253 customDir.Traverse( loader );
254 }
255 else
256 {
257 wxDir systemPluginsDir( PATHS::GetStockPluginsPath() );
258
259 if( systemPluginsDir.IsOpened() )
260 {
261 wxLogTrace( traceApi, wxString::Format( "Manager: scanning system path (%s) for plugins...",
262 systemPluginsDir.GetName() ) );
263 systemPluginsDir.Traverse( loader );
264 }
265
266 wxString thirdPartyPath;
267 const ENV_VAR_MAP& env = Pgm().GetLocalEnvVariables();
268
269 if( std::optional<wxString> v = ENV_VAR::GetVersionedEnvVarValue( env, wxT( "3RD_PARTY" ) ) )
270 thirdPartyPath = *v;
271 else
272 thirdPartyPath = PATHS::GetDefault3rdPartyPath();
273
274 wxDir thirdParty( thirdPartyPath );
275
276 if( thirdParty.IsOpened() )
277 {
278 wxLogTrace( traceApi, wxString::Format( "Manager: scanning PCM path (%s) for plugins...",
279 thirdParty.GetName() ) );
280 thirdParty.Traverse( loader );
281 }
282
283 wxDir userPluginsDir( PATHS::GetUserPluginsPath() );
284
285 if( userPluginsDir.IsOpened() )
286 {
287 wxLogTrace( traceApi, wxString::Format( "Manager: scanning user path (%s) for plugins...",
288 userPluginsDir.GetName() ) );
289 userPluginsDir.Traverse( loader );
290 }
291 }
292
294
295 if( !Busy() )
296 m_reloadReporter.reset();
297
298 wxCommandEvent* evt = new wxCommandEvent( EDA_EVT_PLUGIN_AVAILABILITY_CHANGED, wxID_ANY );
299 m_parent->QueueEvent( evt );
300}
301
302
303void API_PLUGIN_MANAGER::RecreatePluginEnvironment( const wxString& aIdentifier )
304{
305 if( !m_pluginsCache.contains( aIdentifier ) )
306 return;
307
308 const API_PLUGIN* plugin = m_pluginsCache.at( aIdentifier );
309 wxCHECK( plugin, /* void */ );
310
311 if( plugin->Runtime().type != PLUGIN_RUNTIME_TYPE::PYTHON )
312 return;
313
314 std::optional<wxString> env = PYTHON_MANAGER::GetPythonEnvironment( plugin->Identifier() );
315 wxCHECK( env.has_value(), /* void */ );
316
317 wxFileName envConfigPath( *env, wxS( "pyvenv.cfg" ) );
318 envConfigPath.MakeAbsolute();
319
320 if( envConfigPath.DirExists() && envConfigPath.Rmdir( wxPATH_RMDIR_RECURSIVE ) )
321 {
322 wxLogTrace( traceApi,
323 wxString::Format( "Manager: Removed existing Python environment at %s for %s",
324 envConfigPath.GetPath(), plugin->Identifier() ) );
325
326 JOB job;
328 job.identifier = plugin->Identifier();
329 job.plugin_path = plugin->BasePath();
330 job.env_path = envConfigPath.GetPath();
331 m_jobs.emplace_back( job );
332
333 wxCommandEvent* evt = new wxCommandEvent( EDA_EVT_PLUGIN_MANAGER_JOB_FINISHED, wxID_ANY );
334 QueueEvent( evt );
335 }
336}
337
338
339std::optional<const PLUGIN_ACTION*> API_PLUGIN_MANAGER::GetAction( const wxString& aIdentifier )
340{
341 if( !m_actionsCache.contains( aIdentifier ) )
342 return std::nullopt;
343
344 return m_actionsCache.at( aIdentifier );
345}
346
347
348int API_PLUGIN_MANAGER::doInvokeAction( const wxString& aIdentifier, std::vector<wxString> aExtraArgs,
349 bool aSync, wxString* aStdout, wxString* aStderr,
350 std::shared_ptr<REPORTER> aReporter )
351{
352 if( !m_actionsCache.contains( aIdentifier ) )
353 {
354 reportPluginActionMessage( aReporter.get(), aIdentifier, _( "action is not registered" ) );
355 return -1;
356 }
357
358 const PLUGIN_ACTION* action = m_actionsCache.at( aIdentifier );
359 const API_PLUGIN& plugin = action->plugin;
360
361 if( !m_readyPlugins.count( plugin.Identifier() ) )
362 {
363 wxLogTrace( traceApi, wxString::Format( "Manager: Plugin %s is not ready",
364 plugin.Identifier() ) );
365 return -1;
366 }
367
368 wxFileName pluginFile( action->entrypoint );
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();
373
374 std::vector<const wchar_t*> args;
375 std::optional<wxString> py;
376
377 switch( plugin.Runtime().type )
378 {
380 {
382
383 if( !py )
384 {
385 wxLogTrace( traceApi, wxString::Format( "Manager: Python interpreter for %s not found",
386 plugin.Identifier() ) );
387 reportPluginActionMessage( aReporter.get(), action->name,
388 _( "missing plugin environment" ) );
389 return -1;
390 }
391
392 if( !pluginFile.IsFileReadable() )
393 {
394 wxLogTrace( traceApi, wxString::Format( "Manager: Python entrypoint %s is not readable",
395 pluginFile.GetFullPath() ) );
396 reportPluginActionMessage( aReporter.get(), action->name,
397 wxString::Format( _( "entrypoint '%s' could not be read" ),
398 pluginFile.GetFullPath() ) );
399 return -1;
400 }
401
402 std::optional<wxString> pythonHome =
404
405 PYTHON_MANAGER manager( *py );
406 wxExecuteEnv env;
407 wxGetEnvMap( &env.env );
408
409 if( Pgm().ApiServerOrNull() )
410 {
411 env.env[wxS( "KICAD_API_SOCKET" )] = Pgm().GetApiServer().SocketPath();
412 env.env[wxS( "KICAD_API_TOKEN" )] = Pgm().GetApiServer().Token();
413 }
414
415 env.cwd = pluginFile.GetPath();
416
417#ifdef _WIN32
418 wxString systemRoot;
419 wxGetEnv( wxS( "SYSTEMROOT" ), &systemRoot );
420 env.env[wxS( "SYSTEMROOT" )] = systemRoot;
421
422 if( Pgm().GetCommonSettings()->m_Api.python_interpreter == FindKicadFile( "pythonw.exe" )
423 || wxGetEnv( wxT( "KICAD_RUN_FROM_BUILD_DIR" ), nullptr ) )
424 {
425 wxLogTrace( traceApi, "Configured Python is the KiCad one; erasing path overrides..." );
426 env.env.erase( "PYTHONHOME" );
427 env.env.erase( "PYTHONPATH" );
428 }
429#endif
430
431 if( pythonHome )
432 env.env[wxS( "VIRTUAL_ENV" )] = *pythonHome;
433
434 std::vector<wxString> pyArgs( aExtraArgs );
435 pyArgs.insert( pyArgs.begin(), pluginFile.GetFullPath() );
436
437 if( aSync )
438 {
439 wxString stdOut;
440 wxString stdErr;
441 wxString* stdoutSink = aStdout ? aStdout : &stdOut;
442 wxString* stderrSink = aStderr ? aStderr : &stdErr;
443 int ret = manager.ExecuteSync( pyArgs, stdoutSink, stderrSink, &env );
444 reportPluginActionResult( aReporter.get(), action->name, ret, *stderrSink );
445 return ret;
446 }
447
448 [[maybe_unused]] long pid = manager.Execute( pyArgs,
449 [aReporter, action]( int aRetVal, const wxString& aOutput,
450 const wxString& aError )
451 {
452 wxLogTrace( traceApi,
453 wxString::Format( "Manager: action exited with code %d", aRetVal ) );
454
455 if( !aError.IsEmpty() )
456 wxLogTrace( traceApi, wxString::Format( "Manager: action stderr: %s", aError ) );
457
458 reportPluginActionResult( aReporter.get(), action->name, aRetVal, aError );
459 },
460 &env, true );
461
462 if( !pid )
463 {
464 reportPluginActionMessage( aReporter.get(), action->name,
465 _( "process could not be created" ) );
466 return -1;
467 }
468
469#ifdef __WXMAC__
470 if( pid )
471 {
472 if( !m_raiseTimer )
473 {
474 m_raiseTimer = new wxTimer( this );
475
476 Bind( wxEVT_TIMER,
477 [&]( wxTimerEvent& )
478 {
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"
484 " end repeat\n"
485 "end tell" ), m_lastPid );
486
487 wxString cmd = wxString::Format( "osascript -e '%s'", script );
488 wxLogTrace( traceApi, wxString::Format( "Execute: %s", cmd ) );
489 wxExecute( cmd );
490 },
491 m_raiseTimer->GetId() );
492 }
493
494 m_lastPid = pid;
495 m_raiseTimer->StartOnce( 250 );
496 }
497#endif
498
499 break;
500 }
501
503 {
504 if( !pluginFile.IsFileExecutable() )
505 {
506 wxLogTrace( traceApi, wxString::Format( "Manager: Exec entrypoint %s is not executable",
507 pluginFile.GetFullPath() ) );
508 reportPluginActionMessage( aReporter.get(), action->name,
509 wxString::Format( _( "entrypoint '%s' is not executable" ),
510 pluginFile.GetFullPath() ) );
511 return -1;
512 }
513
514 wxExecuteEnv env;
515 wxGetEnvMap( &env.env );
516
517 if( Pgm().ApiServerOrNull() )
518 {
519 env.env[wxS( "KICAD_API_SOCKET" )] = Pgm().GetApiServer().SocketPath();
520 env.env[wxS( "KICAD_API_TOKEN" )] = Pgm().GetApiServer().Token();
521 }
522
523 env.cwd = pluginFile.GetPath();
524
525 long pidOrRetCode = 0;
526
527 if( aSync )
528 {
529 wxString cmd = pluginPath;
530
531 for( const wxString& arg : action->args )
532 cmd << " " << arg;
533
534 wxArrayString out, err;
535
536 pidOrRetCode = wxExecute( cmd, out, err, wxEXEC_BLOCK, &env );
537
538 if( aStdout )
539 {
540 for( const wxString& line : out )
541 *aStdout << line << "\n";
542 }
543
544 wxString stdErr;
545
546 for( const wxString& line : err )
547 stdErr << line << "\n";
548
549 if( aStderr )
550 *aStderr = stdErr;
551
552 reportPluginActionResult( aReporter.get(), action->name, pidOrRetCode, stdErr );
553 return pidOrRetCode;
554 }
555 else
556 {
558 [aReporter, action]( int aRetVal, const wxString& aOutput,
559 const wxString& aError )
560 {
561 wxLogTrace( traceApi,
562 wxString::Format( "Manager: action exited with code %d", aRetVal ) );
563
564 if( !aError.IsEmpty() )
565 wxLogTrace( traceApi,
566 wxString::Format( "Manager: action stderr: %s", aError ) );
567
568 reportPluginActionResult( aReporter.get(), action->name, aRetVal, aError );
569 } );
570
571 process->Redirect();
572 args.emplace_back( pluginPath.wc_str() );
573
574 for( const wxString& arg : action->args )
575 args.emplace_back( arg.wc_str() );
576
577 args.emplace_back( nullptr );
578
579 pidOrRetCode = wxExecute( const_cast<wchar_t**>( args.data() ),
580 wxEXEC_ASYNC | wxEXEC_HIDE_CONSOLE, process, &env );
581
582 if( !pidOrRetCode )
583 delete process;
584 }
585
586 if( !pidOrRetCode )
587 {
588 wxLogTrace( traceApi, wxString::Format( "Manager: launching action %s failed",
589 action->identifier ) );
590 reportPluginActionMessage( aReporter.get(), action->name, _( "could not launch plugin" ) );
591 }
592 else
593 {
594 wxLogTrace( traceApi, wxString::Format( "Manager: launching action %s -> pid %ld",
595 action->identifier, pidOrRetCode ) );
596 }
597 break;
598 }
599
600 default:
601 wxLogTrace( traceApi, wxString::Format( "Manager: unhandled runtime for action %s",
602 action->identifier ) );
603 }
604
605 return -1;
606}
607
608
609void API_PLUGIN_MANAGER::InvokeAction( const wxString& aIdentifier,
610 std::shared_ptr<REPORTER> aReporter )
611{
612 doInvokeAction( aIdentifier, {}, false, nullptr, nullptr, std::move( aReporter ) );
613}
614
615
616int API_PLUGIN_MANAGER::InvokeActionSync( const wxString& aIdentifier, std::vector<wxString> aExtraArgs,
617 wxString* aStdout, wxString* aStderr,
618 std::shared_ptr<REPORTER> aReporter )
619{
620 return doInvokeAction( aIdentifier, aExtraArgs, true, aStdout, aStderr,
621 std::move( aReporter ) );
622}
623
624
625std::vector<const PLUGIN_ACTION*> API_PLUGIN_MANAGER::GetActionsForScope( PLUGIN_ACTION_SCOPE aScope )
626{
627 std::vector<const PLUGIN_ACTION*> actions;
628
629 for( auto& [identifier, action] : m_actionsCache )
630 {
631 if( !m_readyPlugins.count( action->plugin.Identifier() ) )
632 continue;
633
634 if( action->scopes.count( aScope ) )
635 actions.emplace_back( action );
636 }
637
638 return actions;
639}
640
641
642wxString API_PLUGIN_MANAGER::pluginName( const wxString& aIdentifier ) const
643{
644 if( m_pluginsCache.contains( aIdentifier ) )
645 return m_pluginsCache.at( aIdentifier )->Name();
646
647 return aIdentifier;
648}
649
650
652{
653 bool addedAnyJobs = false;
654
655 for( const std::unique_ptr<API_PLUGIN>& plugin : m_plugins )
656 {
657 if( m_busyPlugins.contains( plugin->Identifier() ) )
658 continue;
659
660 wxLogTrace( traceApi, wxString::Format( "Manager: processing dependencies for %s",
661 plugin->Identifier() ) );
662 m_environmentCache[plugin->Identifier()] = wxEmptyString;
663
664 if( plugin->Runtime().type != PLUGIN_RUNTIME_TYPE::PYTHON )
665 {
666 wxLogTrace( traceApi, wxString::Format( "Manager: %s is not a Python plugin, all set",
667 plugin->Identifier() ) );
668 m_readyPlugins.insert( plugin->Identifier() );
669 continue;
670 }
671
672 std::optional<wxString> env = PYTHON_MANAGER::GetPythonEnvironment( plugin->Identifier() );
673
674 if( !env )
675 {
676 wxLogTrace( traceApi, wxString::Format( "Manager: could not create env for %s",
677 plugin->Identifier() ) );
678 continue;
679 }
680
681 m_busyPlugins.insert( plugin->Identifier() );
682
683 wxFileName envConfigPath( *env, wxS( "pyvenv.cfg" ) );
684 envConfigPath.MakeAbsolute();
685
686 if( envConfigPath.IsFileReadable() )
687 {
688 wxLogTrace( traceApi, wxString::Format( "Manager: Python env for %s exists at %s",
689 plugin->Identifier(),
690 envConfigPath.GetPath() ) );
691 JOB job;
693 job.identifier = plugin->Identifier();
694 job.plugin_path = plugin->BasePath();
695 job.env_path = envConfigPath.GetPath();
696 m_jobs.emplace_back( job );
697 addedAnyJobs = true;
698 continue;
699 }
700
701 wxLogTrace( traceApi, wxString::Format( "Manager: will create Python env for %s at %s",
702 plugin->Identifier(), envConfigPath.GetPath() ) );
703 JOB job;
705 job.identifier = plugin->Identifier();
706 job.plugin_path = plugin->BasePath();
707 job.env_path = envConfigPath.GetPath();
708 m_jobs.emplace_back( job );
709 addedAnyJobs = true;
710 }
711
712 if( addedAnyJobs )
713 {
714 wxCommandEvent* evt = new wxCommandEvent( EDA_EVT_PLUGIN_MANAGER_JOB_FINISHED, wxID_ANY );
715 QueueEvent( evt );
716 }
717}
718
719
720void API_PLUGIN_MANAGER::processNextJob( wxCommandEvent& aEvent )
721{
722 if( m_jobs.empty() )
723 {
724 wxLogTrace( traceApi, "Manager: no more jobs to process" );
725 return;
726 }
727
728 wxLogTrace( traceApi, wxString::Format( "Manager: begin processing; %zu jobs left in queue",
729 m_jobs.size() ) );
730
731 JOB& job = m_jobs.front();
732
733 if( job.type == JOB_TYPE::CREATE_ENV )
734 {
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",
738 job.env_path ) );
739 PYTHON_MANAGER manager( Pgm().GetCommonSettings()->m_Api.python_interpreter );
740 wxExecuteEnv env;
741
742#ifdef _WIN32
743 wxString systemRoot;
744 wxGetEnv( wxS( "SYSTEMROOT" ), &systemRoot );
745 env.env[wxS( "SYSTEMROOT" )] = systemRoot;
746
747 if( Pgm().GetCommonSettings()->m_Api.python_interpreter == FindKicadFile( "pythonw.exe" )
748 || wxGetEnv( wxT( "KICAD_RUN_FROM_BUILD_DIR" ), nullptr ) )
749 {
750 wxLogTrace( traceApi, "Configured Python is the KiCad one; erasing path overrides..." );
751 env.env.erase( "PYTHONHOME" );
752 env.env.erase( "PYTHONPATH" );
753 }
754#endif
755 std::vector<wxString> args = {
756 "-m",
757 "venv",
758 "--system-site-packages",
759 job.env_path
760 };
761
762 manager.Execute( args,
763 [this, job]( int aRetVal, const wxString& aOutput, const wxString& aError )
764 {
765 wxLogTrace( traceApi,
766 wxString::Format( "Manager: created venv (python returned %d)", aRetVal ) );
767
768 if( !aError.IsEmpty() )
769 wxLogTrace( traceApi, wxString::Format( "Manager: venv err: %s", aError ) );
770
771 if( aRetVal != 0 )
772 {
773 wxString error = aError;
774 error.Trim().Trim( false );
775
776 if( error.IsEmpty() )
777 error = wxString::Format( _( "error code %d" ), aRetVal );
778
780 _( "Could not create plugin environment" ), error );
781 }
782
783 wxCommandEvent* evt = new wxCommandEvent( EDA_EVT_PLUGIN_MANAGER_JOB_FINISHED, wxID_ANY );
784 QueueEvent( evt );
785 }, &env );
786
787 JOB nextJob( job );
788 nextJob.type = JOB_TYPE::SETUP_ENV;
789 m_jobs.emplace_back( nextJob );
790 }
791 else if( job.type == JOB_TYPE::SETUP_ENV )
792 {
793 wxLogTrace( traceApi, wxString::Format( "Manager: setting up environment for %s",
794 job.plugin_path ) );
795
796 std::optional<wxString> pythonHome = PYTHON_MANAGER::GetPythonEnvironment( job.identifier );
797 std::optional<wxString> python = PYTHON_MANAGER::GetVirtualPython( job.identifier );
798
799 if( !python )
800 {
801 wxString debug = wxString::Format( wxS( "Python binary not found at %s" ), job.env_path );
802 wxLogTrace( traceApi, wxString::Format( "Manager: error: %s", debug ) );
803
805 _( "Missing plugin environment" ), debug );
806 }
807 else
808 {
809 PYTHON_MANAGER manager( *python );
810 wxExecuteEnv env;
811
812 if( pythonHome )
813 env.env[wxS( "VIRTUAL_ENV" )] = *pythonHome;
814
815#ifdef _WIN32
816 wxString systemRoot;
817 wxGetEnv( wxS( "SYSTEMROOT" ), &systemRoot );
818 env.env[wxS( "SYSTEMROOT" )] = systemRoot;
819
820 if( Pgm().GetCommonSettings()->m_Api.python_interpreter
821 == FindKicadFile( "pythonw.exe" )
822 || wxGetEnv( wxT( "KICAD_RUN_FROM_BUILD_DIR" ), nullptr ) )
823 {
824 wxLogTrace( traceApi,
825 "Configured Python is the KiCad one; erasing path overrides..." );
826 env.env.erase( "PYTHONHOME" );
827 env.env.erase( "PYTHONPATH" );
828 }
829#endif
830
831 std::vector<wxString> args = {
832 "-m",
833 "pip",
834 "install",
835 "--upgrade",
836 "pip"
837 };
838
839 manager.Execute( args,
840 [this, job]( int aRetVal, const wxString& aOutput, const wxString& aError )
841 {
842 wxLogTrace( traceApi, wxString::Format( "Manager: upgrade pip returned %d",
843 aRetVal ) );
844
845 if( !aError.IsEmpty() )
846 {
847 wxLogTrace( traceApi,
848 wxString::Format( "Manager: upgrade pip stderr: %s", aError ) );
849 }
850
851 if( aRetVal != 0 )
852 {
853 wxString error = aError;
854 error.Trim().Trim( false );
855
856 if( error.IsEmpty() )
857 error = wxString::Format( _( "error code %d" ), aRetVal );
858
860 _( "Could not create plugin environment" ), error );
861 }
862
863 wxCommandEvent* evt =
864 new wxCommandEvent( EDA_EVT_PLUGIN_MANAGER_JOB_FINISHED, wxID_ANY );
865 QueueEvent( evt );
866 }, &env );
867
868 JOB nextJob( job );
870 m_jobs.emplace_back( nextJob );
871 }
872 }
873 else if( job.type == JOB_TYPE::INSTALL_REQUIREMENTS )
874 {
875 wxLogTrace( traceApi, wxString::Format( "Manager: installing dependencies for %s",
876 job.plugin_path ) );
877
878 std::optional<wxString> pythonHome = PYTHON_MANAGER::GetPythonEnvironment( job.identifier );
879 std::optional<wxString> python = PYTHON_MANAGER::GetVirtualPython( job.identifier );
880 wxFileName reqs = wxFileName( job.plugin_path, "requirements.txt" );
881
882 if( !python )
883 {
884 wxString debug = wxString::Format( wxS( "Python binary not found at %s" ), job.env_path );
885 wxLogTrace( traceApi, wxString::Format( "Manager: error: %s", debug ) );
886
888 _( "Missing plugin environment" ), debug );
889 }
890 else if( !reqs.IsFileReadable() )
891 {
892 wxLogTrace( traceApi,
893 wxString::Format( "Manager: error: requirements.txt not found at %s",
894 job.plugin_path ) );
895 wxString debug = wxString::Format( wxS( "Expected at %s" ), reqs.GetFullPath() );
896
898 _( "requirements.txt could not be read" ), debug );
899 }
900 else
901 {
902 wxLogTrace( traceApi, "Manager: Python exe '%s'", *python );
903
904 PYTHON_MANAGER manager( *python );
905 wxExecuteEnv env;
906
907#ifdef _WIN32
908 wxString systemRoot;
909 wxGetEnv( wxS( "SYSTEMROOT" ), &systemRoot );
910 env.env[wxS( "SYSTEMROOT" )] = systemRoot;
911
912 // If we are using the KiCad-shipped Python interpreter we have to do hacks
913 env.env.erase( "PYTHONHOME" );
914 env.env.erase( "PYTHONPATH" );
915#endif
916
917 if( pythonHome )
918 env.env[wxS( "VIRTUAL_ENV" )] = *pythonHome;
919
920 std::vector<wxString> args = {
921 "-m",
922 "pip",
923 "install",
924 "--no-input",
925 "--isolated",
926 "--only-binary",
927 ":all:",
928 "--require-virtualenv",
929 "--exists-action",
930 "i",
931 "-r",
932 reqs.GetFullPath()
933 };
934
935 manager.Execute( args,
936 [this, job]( int aRetVal, const wxString& aOutput, const wxString& aError )
937 {
938 if( !aError.IsEmpty() )
939 wxLogTrace( traceApi, wxString::Format( "Manager: pip stderr: %s", aError ) );
940
941 if( aRetVal != 0 )
942 {
943 wxString error = aError;
944 error.Trim().Trim( false );
945
946 if( error.IsEmpty() )
947 error = wxString::Format( _( "error code %d" ), aRetVal );
948
950 _( "Could not create plugin environment" ), error );
951 }
952
953 if( aRetVal == 0 )
954 {
955 wxLogTrace( traceApi, wxString::Format( "Manager: marking %s as ready",
956 job.identifier ) );
957 m_readyPlugins.insert( job.identifier );
958
959 wxCommandEvent* availabilityEvt =
960 new wxCommandEvent( EDA_EVT_PLUGIN_AVAILABILITY_CHANGED, wxID_ANY );
961 wxTheApp->QueueEvent( availabilityEvt );
962 }
963
964 m_busyPlugins.erase( job.identifier );
965
966 wxCommandEvent* evt = new wxCommandEvent( EDA_EVT_PLUGIN_MANAGER_JOB_FINISHED,
967 wxID_ANY );
968
969 QueueEvent( evt );
970 }, &env );
971 }
972
973 wxCommandEvent* evt = new wxCommandEvent( EDA_EVT_PLUGIN_MANAGER_JOB_FINISHED, wxID_ANY );
974 QueueEvent( evt );
975 }
976
977 m_jobs.pop_front();
978
979 if( !Busy() )
980 m_reloadReporter.reset();
981
982 wxLogTrace( traceApi, wxString::Format( "Manager: finished job; %zu left in queue",
983 m_jobs.size() ) );
984}
985
986
988{
989 return !m_jobs.empty() || !m_busyPlugins.empty();
990}
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
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< 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...
Definition api_plugin.h:96
const PLUGIN_RUNTIME & Runtime() const
const wxString & Identifier() const
wxString BasePath() const
const std::string & Token() const
Definition api_server.h:81
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 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.
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.
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:62
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:72
const API_PLUGIN & plugin
Definition api_plugin.h:87
wxString name
Definition api_plugin.h:78
wxString identifier
Definition api_plugin.h:77
wxString entrypoint
Definition api_plugin.h:81
std::vector< wxString > args
Definition api_plugin.h:83
PLUGIN_RUNTIME_TYPE type
Definition api_plugin.h:62
#define FN_NORMALIZE_FLAGS
Default flags to pass to wxFileName::Normalize().
Definition wx_filename.h:35