You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

509 lines
18 KiB

2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
  1. /*
  2. * This program source code file is part of KiCad, a free EDA CAD application.
  3. *
  4. * Copyright (C) 2024 Jon Evans <jon@craftyjon.com>
  5. * Copyright (C) 2024 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 along
  18. * with this program. If not, see <http://www.gnu.org/licenses/>.
  19. */
  20. #include <env_vars.h>
  21. #include <fmt/format.h>
  22. #include <wx/dir.h>
  23. #include <wx/log.h>
  24. #include <wx/utils.h>
  25. #include <api/api_plugin_manager.h>
  26. #include <api/api_server.h>
  27. #include <api/api_utils.h>
  28. #include <paths.h>
  29. #include <pgm_base.h>
  30. #include <python_manager.h>
  31. #include <settings/settings_manager.h>
  32. #include <settings/common_settings.h>
  33. wxDEFINE_EVENT( EDA_EVT_PLUGIN_MANAGER_JOB_FINISHED, wxCommandEvent );
  34. wxDEFINE_EVENT( EDA_EVT_PLUGIN_AVAILABILITY_CHANGED, wxCommandEvent );
  35. API_PLUGIN_MANAGER::API_PLUGIN_MANAGER( wxEvtHandler* aEvtHandler ) :
  36. wxEvtHandler(),
  37. m_parent( aEvtHandler )
  38. {
  39. Bind( EDA_EVT_PLUGIN_MANAGER_JOB_FINISHED, &API_PLUGIN_MANAGER::processNextJob, this );
  40. }
  41. class PLUGIN_TRAVERSER : public wxDirTraverser
  42. {
  43. private:
  44. std::function<void( const wxFileName& )> m_action;
  45. public:
  46. explicit PLUGIN_TRAVERSER( std::function<void( const wxFileName& )> aAction )
  47. : m_action( std::move( aAction ) )
  48. {
  49. }
  50. wxDirTraverseResult OnFile( const wxString& aFilePath ) override
  51. {
  52. wxFileName file( aFilePath );
  53. if( file.GetFullName() == wxS( "plugin.json" ) )
  54. m_action( file );
  55. return wxDIR_CONTINUE;
  56. }
  57. wxDirTraverseResult OnDir( const wxString& dirPath ) override
  58. {
  59. return wxDIR_CONTINUE;
  60. }
  61. };
  62. void API_PLUGIN_MANAGER::ReloadPlugins()
  63. {
  64. m_plugins.clear();
  65. m_pluginsCache.clear();
  66. m_actionsCache.clear();
  67. m_environmentCache.clear();
  68. m_buttonBindings.clear();
  69. m_menuBindings.clear();
  70. m_readyPlugins.clear();
  71. PLUGIN_TRAVERSER loader(
  72. [&]( const wxFileName& aFile )
  73. {
  74. wxLogTrace( traceApi, wxString::Format( "Manager: loading plugin from %s",
  75. aFile.GetFullPath() ) );
  76. auto plugin = std::make_unique<API_PLUGIN>( aFile );
  77. if( plugin->IsOk() )
  78. {
  79. if( m_pluginsCache.count( plugin->Identifier() ) )
  80. {
  81. wxLogTrace( traceApi,
  82. wxString::Format( "Manager: identifier %s already present!",
  83. plugin->Identifier() ) );
  84. return;
  85. }
  86. else
  87. {
  88. m_pluginsCache[plugin->Identifier()] = plugin.get();
  89. }
  90. for( const PLUGIN_ACTION& action : plugin->Actions() )
  91. m_actionsCache[action.identifier] = &action;
  92. m_plugins.insert( std::move( plugin ) );
  93. }
  94. else
  95. {
  96. wxLogTrace( traceApi, "Manager: loading failed" );
  97. }
  98. } );
  99. wxDir systemPluginsDir( PATHS::GetStockPluginsPath() );
  100. if( systemPluginsDir.IsOpened() )
  101. {
  102. wxLogTrace( traceApi, wxString::Format( "Manager: scanning system path (%s) for plugins...",
  103. systemPluginsDir.GetName() ) );
  104. systemPluginsDir.Traverse( loader );
  105. }
  106. wxString thirdPartyPath;
  107. const ENV_VAR_MAP& env = Pgm().GetLocalEnvVariables();
  108. if( std::optional<wxString> v = ENV_VAR::GetVersionedEnvVarValue( env, wxT( "3RD_PARTY" ) ) )
  109. thirdPartyPath = *v;
  110. else
  111. thirdPartyPath = PATHS::GetDefault3rdPartyPath();
  112. wxDir thirdParty( thirdPartyPath );
  113. if( thirdParty.IsOpened() )
  114. {
  115. wxLogTrace( traceApi, wxString::Format( "Manager: scanning PCM path (%s) for plugins...",
  116. thirdParty.GetName() ) );
  117. thirdParty.Traverse( loader );
  118. }
  119. wxDir userPluginsDir( PATHS::GetUserPluginsPath() );
  120. if( userPluginsDir.IsOpened() )
  121. {
  122. wxLogTrace( traceApi, wxString::Format( "Manager: scanning user path (%s) for plugins...",
  123. userPluginsDir.GetName() ) );
  124. userPluginsDir.Traverse( loader );
  125. }
  126. processPluginDependencies();
  127. wxCommandEvent* evt = new wxCommandEvent( EDA_EVT_PLUGIN_AVAILABILITY_CHANGED, wxID_ANY );
  128. m_parent->QueueEvent( evt );
  129. }
  130. std::optional<const PLUGIN_ACTION*> API_PLUGIN_MANAGER::GetAction( const wxString& aIdentifier )
  131. {
  132. if( !m_actionsCache.count( aIdentifier ) )
  133. return std::nullopt;
  134. return m_actionsCache.at( aIdentifier );
  135. }
  136. void API_PLUGIN_MANAGER::InvokeAction( const wxString& aIdentifier )
  137. {
  138. if( !m_actionsCache.count( aIdentifier ) )
  139. return;
  140. const PLUGIN_ACTION* action = m_actionsCache.at( aIdentifier );
  141. const API_PLUGIN& plugin = action->plugin;
  142. if( !m_readyPlugins.count( plugin.Identifier() ) )
  143. {
  144. wxLogTrace( traceApi, wxString::Format( "Manager: Plugin %s is not ready",
  145. plugin.Identifier() ) );
  146. return;
  147. }
  148. wxFileName pluginFile( plugin.BasePath(), action->entrypoint );
  149. pluginFile.Normalize( wxPATH_NORM_ABSOLUTE | wxPATH_NORM_SHORTCUT | wxPATH_NORM_DOTS
  150. | wxPATH_NORM_TILDE, plugin.BasePath() );
  151. wxString pluginPath = pluginFile.GetFullPath();
  152. std::vector<const wchar_t*> args;
  153. std::optional<wxString> py;
  154. switch( plugin.Runtime().type )
  155. {
  156. case PLUGIN_RUNTIME_TYPE::PYTHON:
  157. {
  158. py = PYTHON_MANAGER::GetVirtualPython( plugin.Identifier() );
  159. if( !py )
  160. {
  161. wxLogTrace( traceApi, wxString::Format( "Manager: Python interpreter for %s not found",
  162. plugin.Identifier() ) );
  163. return;
  164. }
  165. args.push_back( py->wc_str() );
  166. if( !pluginFile.IsFileReadable() )
  167. {
  168. wxLogTrace( traceApi, wxString::Format( "Manager: Python entrypoint %s is not readable",
  169. pluginFile.GetFullPath() ) );
  170. return;
  171. }
  172. break;
  173. }
  174. case PLUGIN_RUNTIME_TYPE::EXEC:
  175. {
  176. if( !pluginFile.IsFileExecutable() )
  177. {
  178. wxLogTrace( traceApi, wxString::Format( "Manager: Exec entrypoint %s is not executable",
  179. pluginFile.GetFullPath() ) );
  180. return;
  181. }
  182. break;
  183. };
  184. default:
  185. wxLogTrace( traceApi, wxString::Format( "Manager: unhandled runtime for action %s",
  186. action->identifier ) );
  187. return;
  188. }
  189. args.emplace_back( pluginPath.wc_str() );
  190. for( const wxString& arg : action->args )
  191. args.emplace_back( arg.wc_str() );
  192. args.emplace_back( nullptr );
  193. wxExecuteEnv env;
  194. wxGetEnvMap( &env.env );
  195. env.env[ wxS( "KICAD_API_SOCKET" ) ] = Pgm().GetApiServer().SocketPath();
  196. env.env[ wxS( "KICAD_API_TOKEN" ) ] = Pgm().GetApiServer().Token();
  197. env.cwd = pluginFile.GetPath();
  198. long p = wxExecute( const_cast<wchar_t**>( args.data() ), wxEXEC_ASYNC, nullptr, &env );
  199. if( !p )
  200. {
  201. wxLogTrace( traceApi, wxString::Format( "Manager: launching action %s failed",
  202. action->identifier ) );
  203. }
  204. else
  205. {
  206. wxLogTrace( traceApi, wxString::Format( "Manager: launching action %s -> pid %ld",
  207. action->identifier, p ) );
  208. }
  209. }
  210. std::vector<const PLUGIN_ACTION*> API_PLUGIN_MANAGER::GetActionsForScope( PLUGIN_ACTION_SCOPE aScope )
  211. {
  212. std::vector<const PLUGIN_ACTION*> actions;
  213. for( auto& [identifier, action] : m_actionsCache )
  214. {
  215. if( !m_readyPlugins.count( action->plugin.Identifier() ) )
  216. continue;
  217. if( action->scopes.count( aScope ) )
  218. actions.emplace_back( action );
  219. }
  220. return actions;
  221. }
  222. void API_PLUGIN_MANAGER::processPluginDependencies()
  223. {
  224. bool addedAnyJobs = false;
  225. for( const std::unique_ptr<API_PLUGIN>& plugin : m_plugins )
  226. {
  227. if( m_busyPlugins.contains( plugin->Identifier() ) )
  228. continue;
  229. wxLogTrace( traceApi, wxString::Format( "Manager: processing dependencies for %s",
  230. plugin->Identifier() ) );
  231. m_environmentCache[plugin->Identifier()] = wxEmptyString;
  232. if( plugin->Runtime().type != PLUGIN_RUNTIME_TYPE::PYTHON )
  233. {
  234. wxLogTrace( traceApi, wxString::Format( "Manager: %s is not a Python plugin, all set",
  235. plugin->Identifier() ) );
  236. m_readyPlugins.insert( plugin->Identifier() );
  237. continue;
  238. }
  239. std::optional<wxString> env = PYTHON_MANAGER::GetPythonEnvironment( plugin->Identifier() );
  240. if( !env )
  241. {
  242. wxLogTrace( traceApi, wxString::Format( "Manager: could not create env for %s",
  243. plugin->Identifier() ) );
  244. continue;
  245. }
  246. m_busyPlugins.insert( plugin->Identifier() );
  247. wxFileName envConfigPath( *env, wxS( "pyvenv.cfg" ) );
  248. envConfigPath.MakeAbsolute();
  249. if( envConfigPath.IsFileReadable() )
  250. {
  251. wxLogTrace( traceApi, wxString::Format( "Manager: Python env for %s exists at %s",
  252. plugin->Identifier(),
  253. envConfigPath.GetPath() ) );
  254. JOB job;
  255. job.type = JOB_TYPE::INSTALL_REQUIREMENTS;
  256. job.identifier = plugin->Identifier();
  257. job.plugin_path = plugin->BasePath();
  258. job.env_path = envConfigPath.GetPath();
  259. m_jobs.emplace_back( job );
  260. addedAnyJobs = true;
  261. continue;
  262. }
  263. wxLogTrace( traceApi, wxString::Format( "Manager: will create Python env for %s at %s",
  264. plugin->Identifier(), envConfigPath.GetPath() ) );
  265. JOB job;
  266. job.type = JOB_TYPE::CREATE_ENV;
  267. job.identifier = plugin->Identifier();
  268. job.plugin_path = plugin->BasePath();
  269. job.env_path = envConfigPath.GetPath();
  270. m_jobs.emplace_back( job );
  271. addedAnyJobs = true;
  272. }
  273. if( addedAnyJobs )
  274. {
  275. wxCommandEvent* evt = new wxCommandEvent( EDA_EVT_PLUGIN_MANAGER_JOB_FINISHED, wxID_ANY );
  276. QueueEvent( evt );
  277. }
  278. }
  279. void API_PLUGIN_MANAGER::processNextJob( wxCommandEvent& aEvent )
  280. {
  281. if( m_jobs.empty() )
  282. {
  283. wxLogTrace( traceApi, "Manager: no more jobs to process" );
  284. return;
  285. }
  286. wxLogTrace( traceApi, wxString::Format( "Manager: begin processing; %zu jobs left in queue",
  287. m_jobs.size() ) );
  288. JOB& job = m_jobs.front();
  289. if( job.type == JOB_TYPE::CREATE_ENV )
  290. {
  291. wxLogTrace( traceApi, "Manager: Using Python interpreter at %s",
  292. Pgm().GetCommonSettings()->m_Api.python_interpreter );
  293. wxLogTrace( traceApi, wxString::Format( "Manager: creating Python env at %s",
  294. job.env_path ) );
  295. PYTHON_MANAGER manager( Pgm().GetCommonSettings()->m_Api.python_interpreter );
  296. manager.Execute(
  297. wxString::Format( wxS( "-m venv --system-site-packages '%s'" ),
  298. job.env_path ),
  299. [this]( int aRetVal, const wxString& aOutput, const wxString& aError )
  300. {
  301. wxLogTrace( traceApi,
  302. wxString::Format( "Manager: venv (%d): %s", aRetVal, aOutput ) );
  303. if( !aError.IsEmpty() )
  304. wxLogTrace( traceApi, wxString::Format( "Manager: venv err: %s", aError ) );
  305. wxCommandEvent* evt =
  306. new wxCommandEvent( EDA_EVT_PLUGIN_MANAGER_JOB_FINISHED, wxID_ANY );
  307. QueueEvent( evt );
  308. } );
  309. JOB nextJob( job );
  310. nextJob.type = JOB_TYPE::SETUP_ENV;
  311. m_jobs.emplace_back( nextJob );
  312. }
  313. else if( job.type == JOB_TYPE::SETUP_ENV )
  314. {
  315. wxLogTrace( traceApi, wxString::Format( "Manager: setting up environment for %s",
  316. job.plugin_path ) );
  317. std::optional<wxString> pythonHome = PYTHON_MANAGER::GetPythonEnvironment( job.identifier );
  318. std::optional<wxString> python = PYTHON_MANAGER::GetVirtualPython( job.identifier );
  319. if( !python )
  320. {
  321. wxLogTrace( traceApi, wxString::Format( "Manager: error: python not found at %s",
  322. job.env_path ) );
  323. }
  324. else
  325. {
  326. PYTHON_MANAGER manager( *python );
  327. wxExecuteEnv env;
  328. if( pythonHome )
  329. env.env[wxS( "VIRTUAL_ENV" )] = *pythonHome;
  330. wxString cmd = wxS( "-m pip install --upgrade pip" );
  331. wxLogTrace( traceApi, "Manager: calling python %s", cmd );
  332. manager.Execute( cmd,
  333. [this]( int aRetVal, const wxString& aOutput, const wxString& aError )
  334. {
  335. wxLogTrace( traceApi, wxString::Format( "Manager: upgrade pip (%d): %s",
  336. aRetVal, aOutput ) );
  337. if( !aError.IsEmpty() )
  338. {
  339. wxLogTrace( traceApi,
  340. wxString::Format( "Manager: upgrade pip stderr: %s", aError ) );
  341. }
  342. wxCommandEvent* evt =
  343. new wxCommandEvent( EDA_EVT_PLUGIN_MANAGER_JOB_FINISHED, wxID_ANY );
  344. QueueEvent( evt );
  345. }, &env );
  346. JOB nextJob( job );
  347. nextJob.type = JOB_TYPE::INSTALL_REQUIREMENTS;
  348. m_jobs.emplace_back( nextJob );
  349. }
  350. }
  351. else if( job.type == JOB_TYPE::INSTALL_REQUIREMENTS )
  352. {
  353. wxLogTrace( traceApi, wxString::Format( "Manager: installing dependencies for %s",
  354. job.plugin_path ) );
  355. std::optional<wxString> pythonHome = PYTHON_MANAGER::GetPythonEnvironment( job.identifier );
  356. std::optional<wxString> python = PYTHON_MANAGER::GetVirtualPython( job.identifier );
  357. wxFileName reqs = wxFileName( job.plugin_path, "requirements.txt" );
  358. if( !python )
  359. {
  360. wxLogTrace( traceApi, wxString::Format( "Manager: error: python not found at %s",
  361. job.env_path ) );
  362. }
  363. else if( !reqs.IsFileReadable() )
  364. {
  365. wxLogTrace( traceApi,
  366. wxString::Format( "Manager: error: requirements.txt not found at %s",
  367. job.plugin_path ) );
  368. }
  369. else
  370. {
  371. wxLogTrace( traceApi, "Manager: Python exe '%s'", *python );
  372. PYTHON_MANAGER manager( *python );
  373. wxExecuteEnv env;
  374. if( pythonHome )
  375. env.env[wxS( "VIRTUAL_ENV" )] = *pythonHome;
  376. wxString cmd = wxString::Format(
  377. wxS( "-m pip install --no-input --isolated --prefer-binary --require-virtualenv "
  378. "--exists-action i -r '%s'" ),
  379. reqs.GetFullPath() );
  380. wxLogTrace( traceApi, "Manager: calling python %s", cmd );
  381. manager.Execute( cmd,
  382. [this, job]( int aRetVal, const wxString& aOutput, const wxString& aError )
  383. {
  384. if( !aOutput.IsEmpty() )
  385. wxLogTrace( traceApi, wxString::Format( "Manager: pip: %s", aOutput ) );
  386. if( !aError.IsEmpty() )
  387. wxLogTrace( traceApi, wxString::Format( "Manager: pip stderr: %s", aError ) );
  388. if( aRetVal == 0 )
  389. {
  390. wxLogTrace( traceApi, wxString::Format( "Manager: marking %s as ready",
  391. job.identifier ) );
  392. m_readyPlugins.insert( job.identifier );
  393. m_busyPlugins.erase( job.identifier );
  394. wxCommandEvent* availabilityEvt =
  395. new wxCommandEvent( EDA_EVT_PLUGIN_AVAILABILITY_CHANGED, wxID_ANY );
  396. wxTheApp->QueueEvent( availabilityEvt );
  397. }
  398. wxCommandEvent* evt = new wxCommandEvent( EDA_EVT_PLUGIN_MANAGER_JOB_FINISHED,
  399. wxID_ANY );
  400. QueueEvent( evt );
  401. }, &env );
  402. }
  403. wxCommandEvent* evt = new wxCommandEvent( EDA_EVT_PLUGIN_MANAGER_JOB_FINISHED, wxID_ANY );
  404. QueueEvent( evt );
  405. }
  406. m_jobs.pop_front();
  407. wxLogTrace( traceApi, wxString::Format( "Manager: finished job; %zu left in queue",
  408. m_jobs.size() ) );
  409. }