Add a new plugin system for the new API
This commit is contained in:
parent
f613cd1cb4
commit
a3b6ab48a4
|
@ -145,6 +145,7 @@ set( KICOMMON_SRCS
|
|||
env_vars.cpp
|
||||
exceptions.cpp
|
||||
gestfich.cpp
|
||||
json_conversions.cpp
|
||||
kiid.cpp
|
||||
kiway.cpp
|
||||
kiway_express.cpp
|
||||
|
@ -178,6 +179,16 @@ set( KICOMMON_SRCS
|
|||
io/kicad/kicad_io_utils.cpp # needed by richio
|
||||
)
|
||||
|
||||
if( KICAD_IPC_API )
|
||||
set( KICOMMON_SRCS
|
||||
${KICOMMON_SRCS}
|
||||
api/api_plugin.cpp
|
||||
api/api_plugin_manager.cpp
|
||||
|
||||
../scripting/python_manager.cpp
|
||||
)
|
||||
endif()
|
||||
|
||||
add_library( kicommon SHARED
|
||||
${KICOMMON_SRCS}
|
||||
)
|
||||
|
@ -254,6 +265,7 @@ target_include_directories( kicommon
|
|||
PUBLIC
|
||||
.
|
||||
${CMAKE_BINARY_DIR}
|
||||
$<TARGET_PROPERTY:magic_enum,INTERFACE_INCLUDE_DIRECTORIES>
|
||||
$<TARGET_PROPERTY:pegtl,INTERFACE_INCLUDE_DIRECTORIES>
|
||||
$<TARGET_PROPERTY:kiapi,INTERFACE_INCLUDE_DIRECTORIES>
|
||||
)
|
||||
|
|
|
@ -0,0 +1,331 @@
|
|||
/*
|
||||
* This program source code file is part of KiCad, a free EDA CAD application.
|
||||
*
|
||||
* Copyright (C) 2024 Jon Evans <jon@craftyjon.com>
|
||||
* Copyright (C) 2024 KiCad Developers, see AUTHORS.txt for contributors.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU General Public License as published by the
|
||||
* Free Software Foundation, either version 3 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
* General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include <magic_enum.hpp>
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <wx/log.h>
|
||||
#include <wx/regex.h>
|
||||
#include <wx/stdstream.h>
|
||||
#include <wx/wfstream.h>
|
||||
|
||||
#include <api/api_plugin.h>
|
||||
#include <api/api_plugin_manager.h>
|
||||
#include <json_conversions.h>
|
||||
|
||||
|
||||
bool PLUGIN_RUNTIME::FromJson( const nlohmann::json& aJson )
|
||||
{
|
||||
// TODO move to tl::expected and give user feedback about parse errors
|
||||
|
||||
try
|
||||
{
|
||||
type = magic_enum::enum_cast<PLUGIN_RUNTIME_TYPE>( aJson.at( "type" ).get<std::string>(),
|
||||
magic_enum::case_insensitive )
|
||||
.value_or( PLUGIN_RUNTIME_TYPE::INVALID );
|
||||
|
||||
|
||||
}
|
||||
catch( ... )
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return type != PLUGIN_RUNTIME_TYPE::INVALID;
|
||||
}
|
||||
|
||||
|
||||
struct API_PLUGIN_CONFIG
|
||||
{
|
||||
API_PLUGIN_CONFIG( API_PLUGIN& aParent, const wxFileName& aConfigFile );
|
||||
|
||||
bool valid;
|
||||
wxString identifier;
|
||||
wxString name;
|
||||
wxString description;
|
||||
PLUGIN_RUNTIME runtime;
|
||||
std::vector<PLUGIN_ACTION> actions;
|
||||
|
||||
API_PLUGIN& parent;
|
||||
};
|
||||
|
||||
|
||||
API_PLUGIN_CONFIG::API_PLUGIN_CONFIG( API_PLUGIN& aParent, const wxFileName& aConfigFile ) :
|
||||
parent( aParent )
|
||||
{
|
||||
valid = false;
|
||||
|
||||
if( !aConfigFile.IsFileReadable() )
|
||||
return;
|
||||
|
||||
wxLogTrace( traceApi, "Plugin: parsing config file" );
|
||||
|
||||
wxFFileInputStream fp( aConfigFile.GetFullPath(), wxT( "rt" ) );
|
||||
wxStdInputStream fstream( fp );
|
||||
|
||||
nlohmann::json js;
|
||||
|
||||
try
|
||||
{
|
||||
js = nlohmann::json::parse( fstream, nullptr,
|
||||
/* allow_exceptions = */ true,
|
||||
/* ignore_comments = */ true );
|
||||
}
|
||||
catch( ... )
|
||||
{
|
||||
wxLogTrace( traceApi, "Plugin: exception during parse" );
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO add schema and validate
|
||||
|
||||
// All of these are required; any exceptions here leave us with valid == false
|
||||
try
|
||||
{
|
||||
identifier = js.at( "identifier" ).get<wxString>();
|
||||
name = js.at( "name" ).get<wxString>();
|
||||
description = js.at( "description" ).get<wxString>();
|
||||
|
||||
if( !runtime.FromJson( js.at( "runtime" ) ) )
|
||||
{
|
||||
wxLogTrace( traceApi, "Plugin: error parsing runtime section" );
|
||||
return;
|
||||
}
|
||||
}
|
||||
catch( ... )
|
||||
{
|
||||
wxLogTrace( traceApi, "Plugin: exception while parsing required keys" );
|
||||
return;
|
||||
}
|
||||
|
||||
// At minimum, we need a reverse-DNS style identifier with two dots and a 2+ character TLD
|
||||
wxRegEx identifierRegex( wxS( "[\\w\\d]{2,}\\.[\\w\\d]+\\.[\\w\\d]+" ) );
|
||||
|
||||
if( !identifierRegex.Matches( identifier ) )
|
||||
{
|
||||
wxLogTrace( traceApi, wxString::Format( "Plugin: identifier %s does not meet requirements",
|
||||
identifier ) );
|
||||
return;
|
||||
}
|
||||
|
||||
wxLogTrace( traceApi, wxString::Format( "Plugin: %s (%s)", identifier, name ) );
|
||||
|
||||
try
|
||||
{
|
||||
const nlohmann::json& actionsJs = js.at( "actions" );
|
||||
|
||||
if( actionsJs.is_array() )
|
||||
{
|
||||
for( const nlohmann::json& actionJs : actionsJs )
|
||||
{
|
||||
if( std::optional<PLUGIN_ACTION> a = parent.createActionFromJson( actionJs ) )
|
||||
{
|
||||
a->identifier = wxString::Format( "%s.%s", identifier, a->identifier );
|
||||
wxLogTrace( traceApi, wxString::Format( "Plugin: loaded action %s",
|
||||
a->identifier ) );
|
||||
actions.emplace_back( *a );
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch( ... )
|
||||
{
|
||||
wxLogTrace( traceApi, "Plugin: exception while parsing actions" );
|
||||
}
|
||||
|
||||
valid = true;
|
||||
}
|
||||
|
||||
|
||||
API_PLUGIN::API_PLUGIN( const wxFileName& aConfigFile ) :
|
||||
m_configFile( aConfigFile ),
|
||||
m_config( std::make_unique<API_PLUGIN_CONFIG>( *this, aConfigFile ) )
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
API_PLUGIN::~API_PLUGIN()
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
bool API_PLUGIN::IsOk() const
|
||||
{
|
||||
return m_config->valid;
|
||||
}
|
||||
|
||||
|
||||
const wxString& API_PLUGIN::Identifier() const
|
||||
{
|
||||
return m_config->identifier;
|
||||
}
|
||||
|
||||
|
||||
const wxString& API_PLUGIN::Name() const
|
||||
{
|
||||
return m_config->name;
|
||||
}
|
||||
|
||||
|
||||
const wxString& API_PLUGIN::Description() const
|
||||
{
|
||||
return m_config->description;
|
||||
}
|
||||
|
||||
|
||||
const PLUGIN_RUNTIME& API_PLUGIN::Runtime() const
|
||||
{
|
||||
return m_config->runtime;
|
||||
}
|
||||
|
||||
|
||||
const std::vector<PLUGIN_ACTION>& API_PLUGIN::Actions() const
|
||||
{
|
||||
return m_config->actions;
|
||||
}
|
||||
|
||||
|
||||
wxString API_PLUGIN::BasePath() const
|
||||
{
|
||||
return m_configFile.GetPath();
|
||||
}
|
||||
|
||||
|
||||
std::optional<PLUGIN_ACTION> API_PLUGIN::createActionFromJson( const nlohmann::json& aJson )
|
||||
{
|
||||
// TODO move to tl::expected and give user feedback about parse errors
|
||||
PLUGIN_ACTION action( *this );
|
||||
|
||||
try
|
||||
{
|
||||
action.identifier = aJson.at( "identifier" ).get<wxString>();
|
||||
wxLogTrace( traceApi, wxString::Format( "Plugin: load action %s", action.identifier ) );
|
||||
action.name = aJson.at( "name" ).get<wxString>();
|
||||
action.description = aJson.at( "description" ).get<wxString>();
|
||||
action.entrypoint = aJson.at( "entrypoint" ).get<wxString>();
|
||||
action.show_button = aJson.contains( "show-button" ) && aJson.at( "show-button" ).get<bool>();
|
||||
}
|
||||
catch( ... )
|
||||
{
|
||||
wxLogTrace( traceApi, "Plugin: exception while parsing action required keys" );
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
wxFileName f( action.entrypoint );
|
||||
|
||||
if( !f.IsRelative() )
|
||||
{
|
||||
wxLogTrace( traceApi, wxString::Format( "Plugin: action contains abs path %s; skipping",
|
||||
action.entrypoint ) );
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
f.Normalize( wxPATH_NORM_ABSOLUTE, m_configFile.GetPath() );
|
||||
|
||||
if( !f.IsFileReadable() )
|
||||
{
|
||||
wxLogTrace( traceApi, wxString::Format( "WARNING: action entrypoint %s is not readable",
|
||||
f.GetFullPath() ) );
|
||||
}
|
||||
|
||||
if( aJson.contains( "args" ) && aJson.at( "args" ).is_array() )
|
||||
{
|
||||
for( const nlohmann::json& argJs : aJson.at( "args" ) )
|
||||
{
|
||||
try
|
||||
{
|
||||
action.args.emplace_back( argJs.get<wxString>() );
|
||||
}
|
||||
catch( ... )
|
||||
{
|
||||
wxLogTrace( traceApi, "Plugin: exception while parsing action args" );
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if( aJson.contains( "scopes" ) && aJson.at( "scopes" ).is_array() )
|
||||
{
|
||||
for( const nlohmann::json& scopeJs : aJson.at( "scopes" ) )
|
||||
{
|
||||
try
|
||||
{
|
||||
action.scopes.insert( magic_enum::enum_cast<PLUGIN_ACTION_SCOPE>(
|
||||
scopeJs.get<std::string>(), magic_enum::case_insensitive )
|
||||
.value_or( PLUGIN_ACTION_SCOPE::INVALID ) );
|
||||
}
|
||||
catch( ... )
|
||||
{
|
||||
wxLogTrace( traceApi, "Plugin: exception while parsing action scopes" );
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
auto handleBitmap =
|
||||
[&]( const std::string& aKey, wxBitmapBundle& aDest )
|
||||
{
|
||||
if( aJson.contains( aKey ) && aJson.at( aKey ).is_array() )
|
||||
{
|
||||
wxVector<wxBitmap> bitmaps;
|
||||
|
||||
for( const nlohmann::json& iconJs : aJson.at( aKey ) )
|
||||
{
|
||||
wxFileName iconFile;
|
||||
|
||||
try
|
||||
{
|
||||
iconFile = iconJs.get<wxString>();
|
||||
}
|
||||
catch( ... )
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
iconFile.Normalize( wxPATH_NORM_ABSOLUTE, m_configFile.GetPath() );
|
||||
|
||||
wxLogTrace( traceApi,
|
||||
wxString::Format( "Plugin: action %s: loading icon %s",
|
||||
action.identifier, iconFile.GetFullPath() ) );
|
||||
|
||||
|
||||
if( !iconFile.IsFileReadable() )
|
||||
{
|
||||
wxLogTrace( traceApi, "Plugin: icon file could not be read" );
|
||||
continue;
|
||||
}
|
||||
|
||||
wxBitmap bmp;
|
||||
bmp.LoadFile( iconFile.GetFullPath() );
|
||||
|
||||
if( bmp.IsOk() )
|
||||
bitmaps.push_back( bmp );
|
||||
else
|
||||
wxLogTrace( traceApi, "Plugin: icon file not a valid bitmap" );
|
||||
}
|
||||
|
||||
aDest = wxBitmapBundle::FromBitmaps( bitmaps );
|
||||
}
|
||||
};
|
||||
|
||||
handleBitmap( "icons-light", action.icon_light );
|
||||
handleBitmap( "icons-dark", action.icon_dark );
|
||||
|
||||
return action;
|
||||
}
|
|
@ -0,0 +1,391 @@
|
|||
/*
|
||||
* This program source code file is part of KiCad, a free EDA CAD application.
|
||||
*
|
||||
* Copyright (C) 2024 Jon Evans <jon@craftyjon.com>
|
||||
* Copyright (C) 2024 KiCad Developers, see AUTHORS.txt for contributors.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU General Public License as published by the
|
||||
* Free Software Foundation, either version 3 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
* General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include <fmt/format.h>
|
||||
#include <wx/dir.h>
|
||||
#include <wx/log.h>
|
||||
#include <wx/utils.h>
|
||||
|
||||
#include <api/api_plugin_manager.h>
|
||||
#include <api/api_server.h>
|
||||
#include <paths.h>
|
||||
#include <pgm_base.h>
|
||||
#include <python_manager.h>
|
||||
#include <settings/settings_manager.h>
|
||||
#include <settings/common_settings.h>
|
||||
|
||||
const wxChar* const traceApi = wxT( "KICAD_API" );
|
||||
|
||||
|
||||
wxDEFINE_EVENT( EDA_EVT_PLUGIN_MANAGER_JOB_FINISHED, wxCommandEvent );
|
||||
wxDEFINE_EVENT( EDA_EVT_PLUGIN_AVAILABILITY_CHANGED, wxCommandEvent );
|
||||
|
||||
|
||||
API_PLUGIN_MANAGER::API_PLUGIN_MANAGER( wxEvtHandler* aEvtHandler ) :
|
||||
wxEvtHandler(),
|
||||
m_parent( aEvtHandler )
|
||||
{
|
||||
Bind( EDA_EVT_PLUGIN_MANAGER_JOB_FINISHED, &API_PLUGIN_MANAGER::processNextJob, this );
|
||||
}
|
||||
|
||||
|
||||
class PLUGIN_TRAVERSER : public wxDirTraverser
|
||||
{
|
||||
private:
|
||||
std::function<void( const wxFileName& )> m_action;
|
||||
|
||||
public:
|
||||
explicit PLUGIN_TRAVERSER( std::function<void( const wxFileName& )> aAction )
|
||||
: m_action( std::move( aAction ) )
|
||||
{
|
||||
}
|
||||
|
||||
wxDirTraverseResult OnFile( const wxString& aFilePath ) override
|
||||
{
|
||||
wxFileName file( aFilePath );
|
||||
|
||||
if( file.GetFullName() == wxS( "plugin.json" ) )
|
||||
m_action( file );
|
||||
|
||||
return wxDIR_CONTINUE;
|
||||
}
|
||||
|
||||
wxDirTraverseResult OnDir( const wxString& dirPath ) override
|
||||
{
|
||||
return wxDIR_CONTINUE;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
void API_PLUGIN_MANAGER::ReloadPlugins()
|
||||
{
|
||||
m_plugins.clear();
|
||||
m_pluginsCache.clear();
|
||||
m_actionsCache.clear();
|
||||
m_environmentCache.clear();
|
||||
m_buttonBindings.clear();
|
||||
m_menuBindings.clear();
|
||||
m_readyPlugins.clear();
|
||||
|
||||
// TODO support system-provided plugins
|
||||
wxDir userPluginsDir( PATHS::GetUserPluginsPath() );
|
||||
|
||||
PLUGIN_TRAVERSER loader(
|
||||
[&]( const wxFileName& aFile )
|
||||
{
|
||||
wxLogTrace( traceApi, wxString::Format( "Manager: loading plugin from %s",
|
||||
aFile.GetFullPath() ) );
|
||||
|
||||
auto plugin = std::make_unique<API_PLUGIN>( aFile );
|
||||
|
||||
if( plugin->IsOk() )
|
||||
{
|
||||
if( m_pluginsCache.count( plugin->Identifier() ) )
|
||||
{
|
||||
wxLogTrace( traceApi,
|
||||
wxString::Format( "Manager: identifier %s already present!",
|
||||
plugin->Identifier() ) );
|
||||
return;
|
||||
}
|
||||
else
|
||||
{
|
||||
m_pluginsCache[plugin->Identifier()] = plugin.get();
|
||||
}
|
||||
|
||||
for( const PLUGIN_ACTION& action : plugin->Actions() )
|
||||
m_actionsCache[action.identifier] = &action;
|
||||
|
||||
m_plugins.insert( std::move( plugin ) );
|
||||
}
|
||||
else
|
||||
{
|
||||
wxLogTrace( traceApi, "Manager: loading failed" );
|
||||
}
|
||||
} );
|
||||
|
||||
if( userPluginsDir.IsOpened() )
|
||||
{
|
||||
wxLogTrace( traceApi, wxString::Format( "Manager: scanning user path (%s) for plugins...",
|
||||
userPluginsDir.GetName() ) );
|
||||
userPluginsDir.Traverse( loader );
|
||||
processPluginDependencies();
|
||||
}
|
||||
|
||||
wxCommandEvent* evt = new wxCommandEvent( EDA_EVT_PLUGIN_AVAILABILITY_CHANGED, wxID_ANY );
|
||||
m_parent->QueueEvent( evt );
|
||||
}
|
||||
|
||||
|
||||
void API_PLUGIN_MANAGER::InvokeAction( const wxString& aIdentifier )
|
||||
{
|
||||
if( !m_actionsCache.count( aIdentifier ) )
|
||||
return;
|
||||
|
||||
const PLUGIN_ACTION* action = m_actionsCache.at( aIdentifier );
|
||||
const API_PLUGIN& plugin = action->plugin;
|
||||
|
||||
if( !m_readyPlugins.count( plugin.Identifier() ) )
|
||||
{
|
||||
wxLogTrace( traceApi, wxString::Format( "Manager: Plugin %s is not ready",
|
||||
plugin.Identifier() ) );
|
||||
return;
|
||||
}
|
||||
|
||||
wxFileName pluginFile( plugin.BasePath(), action->entrypoint );
|
||||
pluginFile.Normalize( wxPATH_NORM_ABSOLUTE | wxPATH_NORM_SHORTCUT | wxPATH_NORM_DOTS
|
||||
| wxPATH_NORM_TILDE, plugin.BasePath() );
|
||||
wxString pluginPath = pluginFile.GetFullPath();
|
||||
|
||||
std::vector<const wchar_t*> args;
|
||||
std::optional<wxString> py;
|
||||
|
||||
switch( plugin.Runtime().type )
|
||||
{
|
||||
case PLUGIN_RUNTIME_TYPE::PYTHON:
|
||||
{
|
||||
py = PYTHON_MANAGER::GetVirtualPython( plugin.Identifier() );
|
||||
|
||||
if( !py )
|
||||
{
|
||||
wxLogTrace( traceApi, wxString::Format( "Manager: Python interpreter for %s not found",
|
||||
plugin.Identifier() ) );
|
||||
return;
|
||||
}
|
||||
|
||||
args.push_back( py->wc_str() );
|
||||
|
||||
if( !pluginFile.IsFileReadable() )
|
||||
{
|
||||
wxLogTrace( traceApi, wxString::Format( "Manager: Python entrypoint %s is not readable",
|
||||
pluginFile.GetFullPath() ) );
|
||||
return;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case PLUGIN_RUNTIME_TYPE::EXEC:
|
||||
{
|
||||
if( !pluginFile.IsFileExecutable() )
|
||||
{
|
||||
wxLogTrace( traceApi, wxString::Format( "Manager: Exec entrypoint %s is not executable",
|
||||
pluginFile.GetFullPath() ) );
|
||||
return;
|
||||
}
|
||||
|
||||
break;
|
||||
};
|
||||
|
||||
default:
|
||||
wxLogTrace( traceApi, wxString::Format( "Manager: unhandled runtime for action %s",
|
||||
action->identifier ) );
|
||||
return;
|
||||
}
|
||||
|
||||
args.emplace_back( pluginPath.wc_str() );
|
||||
|
||||
for( const wxString& arg : action->args )
|
||||
args.emplace_back( arg.wc_str() );
|
||||
|
||||
args.emplace_back( nullptr );
|
||||
|
||||
wxExecuteEnv env;
|
||||
wxGetEnvMap( &env.env );
|
||||
env.env[ wxS( "KICAD_API_SOCKET" ) ] = Pgm().GetApiServer().SocketPath();
|
||||
env.env[ wxS( "KICAD_API_TOKEN" ) ] = Pgm().GetApiServer().Token();
|
||||
env.cwd = pluginFile.GetPath();
|
||||
|
||||
long p = wxExecute( const_cast<wchar_t**>( args.data() ), wxEXEC_ASYNC, nullptr, &env );
|
||||
|
||||
if( !p )
|
||||
{
|
||||
wxLogTrace( traceApi, wxString::Format( "Manager: launching action %s failed",
|
||||
action->identifier ) );
|
||||
}
|
||||
else
|
||||
{
|
||||
wxLogTrace( traceApi, wxString::Format( "Manager: launching action %s -> pid %ld",
|
||||
action->identifier, p ) );
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
std::vector<const PLUGIN_ACTION*> API_PLUGIN_MANAGER::GetActionsForScope( PLUGIN_ACTION_SCOPE aScope )
|
||||
{
|
||||
std::vector<const PLUGIN_ACTION*> actions;
|
||||
|
||||
for( auto& [identifier, action] : m_actionsCache )
|
||||
{
|
||||
if( !m_readyPlugins.count( action->plugin.Identifier() ) )
|
||||
continue;
|
||||
|
||||
if( action->scopes.count( aScope ) )
|
||||
actions.emplace_back( action );
|
||||
}
|
||||
|
||||
return actions;
|
||||
}
|
||||
|
||||
|
||||
void API_PLUGIN_MANAGER::processPluginDependencies()
|
||||
{
|
||||
for( const std::unique_ptr<API_PLUGIN>& plugin : m_plugins )
|
||||
{
|
||||
m_environmentCache[plugin->Identifier()] = wxEmptyString;
|
||||
|
||||
if( plugin->Runtime().type != PLUGIN_RUNTIME_TYPE::PYTHON )
|
||||
{
|
||||
m_readyPlugins.insert( plugin->Identifier() );
|
||||
continue;
|
||||
}
|
||||
|
||||
std::optional<wxString> env = PYTHON_MANAGER::GetPythonEnvironment( plugin->Identifier() );
|
||||
|
||||
if( !env )
|
||||
{
|
||||
wxLogTrace( traceApi, wxString::Format( "Manager: could not create env for %s",
|
||||
plugin->Identifier() ) );
|
||||
continue;
|
||||
}
|
||||
|
||||
wxFileName envConfigPath( *env, wxS( "pyvenv.cfg" ) );
|
||||
|
||||
if( envConfigPath.IsFileReadable() )
|
||||
{
|
||||
wxLogTrace( traceApi, wxString::Format( "Manager: Python env for %s exists at %s",
|
||||
plugin->Identifier(), *env ) );
|
||||
JOB job;
|
||||
job.type = JOB_TYPE::INSTALL_REQUIREMENTS;
|
||||
job.identifier = plugin->Identifier();
|
||||
job.plugin_path = plugin->BasePath();
|
||||
job.env_path = *env;
|
||||
m_jobs.emplace_back( job );
|
||||
continue;
|
||||
}
|
||||
|
||||
wxLogTrace( traceApi, wxString::Format( "Manager: will create Python env for %s at %s",
|
||||
plugin->Identifier(), *env ) );
|
||||
JOB job;
|
||||
job.type = JOB_TYPE::CREATE_ENV;
|
||||
job.identifier = plugin->Identifier();
|
||||
job.plugin_path = plugin->BasePath();
|
||||
job.env_path = *env;
|
||||
m_jobs.emplace_back( job );
|
||||
}
|
||||
|
||||
wxCommandEvent evt;
|
||||
processNextJob( evt );
|
||||
}
|
||||
|
||||
|
||||
void API_PLUGIN_MANAGER::processNextJob( wxCommandEvent& aEvent )
|
||||
{
|
||||
if( m_jobs.empty() )
|
||||
{
|
||||
wxLogTrace( traceApi, "Manager: cleared job queue" );
|
||||
return;
|
||||
}
|
||||
|
||||
JOB& job = m_jobs.front();
|
||||
|
||||
if( job.type == JOB_TYPE::CREATE_ENV )
|
||||
{
|
||||
wxLogTrace( traceApi, wxString::Format( "Manager: creating Python env at %s",
|
||||
job.env_path ) );
|
||||
PYTHON_MANAGER manager( Pgm().GetCommonSettings()->m_Python.interpreter_path );
|
||||
|
||||
manager.Execute( wxString::Format( wxS( "-m venv %s"), job.env_path ),
|
||||
[=]( int aRetVal, const wxString& aOutput, const wxString& aError )
|
||||
{
|
||||
wxLogTrace( traceApi, wxString::Format( "Manager: venv (%d): %s",
|
||||
aRetVal, aOutput ) );
|
||||
|
||||
if( !aError.IsEmpty() )
|
||||
wxLogTrace( traceApi, wxString::Format( "Manager: venv err: %s", aError ) );
|
||||
|
||||
wxCommandEvent* evt = new wxCommandEvent( EDA_EVT_PLUGIN_MANAGER_JOB_FINISHED,
|
||||
wxID_ANY );
|
||||
QueueEvent( evt );
|
||||
} );
|
||||
}
|
||||
else if( job.type == JOB_TYPE::INSTALL_REQUIREMENTS )
|
||||
{
|
||||
wxLogTrace( traceApi, wxString::Format( "Manager: installing dependencies for %s",
|
||||
job.plugin_path ) );
|
||||
|
||||
std::optional<wxString> python = PYTHON_MANAGER::GetVirtualPython( job.identifier );
|
||||
wxFileName reqs = wxFileName( job.plugin_path, "requirements.txt" );
|
||||
|
||||
if( !python )
|
||||
{
|
||||
wxLogTrace( traceApi, wxString::Format( "Manager: error: python not found at %s",
|
||||
job.env_path ) );
|
||||
wxCommandEvent* evt = new wxCommandEvent( EDA_EVT_PLUGIN_MANAGER_JOB_FINISHED,
|
||||
wxID_ANY );
|
||||
QueueEvent( evt );
|
||||
}
|
||||
else if( !reqs.IsFileReadable() )
|
||||
{
|
||||
wxLogTrace( traceApi,
|
||||
wxString::Format( "Manager: error: requirements.txt not found at %s",
|
||||
job.plugin_path ) );
|
||||
wxCommandEvent* evt = new wxCommandEvent( EDA_EVT_PLUGIN_MANAGER_JOB_FINISHED,
|
||||
wxID_ANY );
|
||||
QueueEvent( evt );
|
||||
}
|
||||
else
|
||||
{
|
||||
PYTHON_MANAGER manager( *python );
|
||||
|
||||
wxString cmd = wxString::Format(
|
||||
wxS( "-m pip install --no-input --isolated --require-virtualenv "
|
||||
"--exists-action i -r '%s'" ),
|
||||
reqs.GetFullPath() );
|
||||
|
||||
manager.Execute( cmd,
|
||||
[=]( int aRetVal, const wxString& aOutput, const wxString& aError )
|
||||
{
|
||||
wxLogTrace( traceApi, wxString::Format( "Manager: pip (%d): %s",
|
||||
aRetVal, aOutput ) );
|
||||
|
||||
if( !aError.IsEmpty() )
|
||||
wxLogTrace( traceApi, wxString::Format( "Manager: pip err: %s", aError ) );
|
||||
|
||||
if( aRetVal == 0 )
|
||||
{
|
||||
wxLogTrace( traceApi, wxString::Format( "Manager: marking %s as ready",
|
||||
job.identifier ) );
|
||||
m_readyPlugins.insert( job.identifier );
|
||||
wxCommandEvent* availabilityEvt =
|
||||
new wxCommandEvent( EDA_EVT_PLUGIN_AVAILABILITY_CHANGED, wxID_ANY );
|
||||
wxTheApp->QueueEvent( availabilityEvt );
|
||||
}
|
||||
|
||||
wxCommandEvent* evt = new wxCommandEvent( EDA_EVT_PLUGIN_MANAGER_JOB_FINISHED,
|
||||
wxID_ANY );
|
||||
|
||||
QueueEvent( evt );
|
||||
} );
|
||||
}
|
||||
}
|
||||
|
||||
m_jobs.pop_front();
|
||||
wxLogTrace( traceApi, wxString::Format( "Manager: %zu jobs left in queue", m_jobs.size() ) );
|
||||
}
|
|
@ -49,6 +49,7 @@ KICAD_API_SERVER::KICAD_API_SERVER() :
|
|||
{
|
||||
m_server = std::make_unique<KINNG_REQUEST_SERVER>();
|
||||
m_server->SetCallback( [&]( std::string* aRequest ) { onApiRequest( aRequest ); } );
|
||||
m_socketPath = m_server->SocketPath();
|
||||
|
||||
m_commonHandler = std::make_unique<API_HANDLER_COMMON>();
|
||||
RegisterHandler( m_commonHandler.get() );
|
||||
|
|
|
@ -42,9 +42,8 @@ void PANEL_PYTHON_SETTINGS::ResetPanel()
|
|||
bool PANEL_PYTHON_SETTINGS::TransferDataToWindow()
|
||||
{
|
||||
SETTINGS_MANAGER& mgr = Pgm().GetSettingsManager();
|
||||
COMMON_SETTINGS* settings = mgr.GetCommonSettings();
|
||||
|
||||
m_pickerPythonInterpreter->SetFileName( settings->m_Python.interpreter_path );
|
||||
m_pickerPythonInterpreter->SetFileName( mgr.GetCommonSettings()->m_Python.interpreter_path );
|
||||
validateInterpreter();
|
||||
|
||||
return true;
|
||||
|
@ -54,10 +53,10 @@ bool PANEL_PYTHON_SETTINGS::TransferDataToWindow()
|
|||
bool PANEL_PYTHON_SETTINGS::TransferDataFromWindow()
|
||||
{
|
||||
SETTINGS_MANAGER& mgr = Pgm().GetSettingsManager();
|
||||
COMMON_SETTINGS* settings = mgr.GetCommonSettings();
|
||||
wxString interpreter = m_pickerPythonInterpreter->GetTextCtrlValue();
|
||||
|
||||
if( m_interpreterValid )
|
||||
settings->m_Python.interpreter_path = m_pickerPythonInterpreter->GetTextCtrlValue();
|
||||
if( m_interpreterValid || interpreter.IsEmpty() )
|
||||
mgr.GetCommonSettings()->m_Python.interpreter_path = interpreter;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
@ -71,6 +70,20 @@ void PANEL_PYTHON_SETTINGS::OnPythonInterpreterChanged( wxFileDirPickerEvent& ev
|
|||
|
||||
void PANEL_PYTHON_SETTINGS::OnBtnDetectAutomaticallyClicked( wxCommandEvent& aEvent )
|
||||
{
|
||||
#ifdef __WXMSW__
|
||||
// TODO(JE) where
|
||||
#else
|
||||
wxArrayString output;
|
||||
|
||||
if( 0 == wxExecute( wxS( "which -a python" ), output, wxEXEC_SYNC ) )
|
||||
{
|
||||
if( !output.IsEmpty() )
|
||||
{
|
||||
m_pickerPythonInterpreter->SetPath( output[0] );
|
||||
validateInterpreter();
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
|
||||
|
@ -90,7 +103,7 @@ void PANEL_PYTHON_SETTINGS::validateInterpreter()
|
|||
PYTHON_MANAGER manager( pythonExe.GetFullPath() );
|
||||
|
||||
manager.Execute( wxS( "--version" ),
|
||||
[&]( int aRetCode, const wxString& aStdOut )
|
||||
[&]( int aRetCode, const wxString& aStdOut, const wxString& aStdErr )
|
||||
{
|
||||
wxString msg;
|
||||
|
||||
|
|
|
@ -0,0 +1,33 @@
|
|||
/*
|
||||
* This program source code file is part of KiCad, a free EDA CAD application.
|
||||
*
|
||||
* Copyright (C) 2024 KiCad Developers, see AUTHORS.txt for contributors.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU General Public License as published by the
|
||||
* Free Software Foundation, either version 3 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
* General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <json_conversions.h>
|
||||
|
||||
// Specializations to allow directly reading/writing wxStrings from JSON
|
||||
void to_json( nlohmann::json& aJson, const wxString& aString )
|
||||
{
|
||||
aJson = aString.ToUTF8();
|
||||
}
|
||||
|
||||
|
||||
void from_json( const nlohmann::json& aJson, wxString& aString )
|
||||
{
|
||||
aString = wxString( aJson.get<std::string>().c_str(), wxConvUTF8 );
|
||||
}
|
|
@ -62,17 +62,6 @@ wxString PATHS::GetUserPluginsPath()
|
|||
}
|
||||
|
||||
|
||||
wxString PATHS::GetUserPlugins3DPath()
|
||||
{
|
||||
wxFileName tmp;
|
||||
|
||||
tmp.AssignDir( PATHS::GetUserPluginsPath() );
|
||||
tmp.AppendDir( wxT( "3d" ) );
|
||||
|
||||
return tmp.GetPath();
|
||||
}
|
||||
|
||||
|
||||
wxString PATHS::GetUserScriptingPath()
|
||||
{
|
||||
wxFileName tmp;
|
||||
|
|
|
@ -75,6 +75,7 @@
|
|||
#endif
|
||||
|
||||
#ifdef KICAD_IPC_API
|
||||
#include <api/api_plugin_manager.h>
|
||||
#include <api/api_server.h>
|
||||
#endif
|
||||
|
||||
|
@ -556,6 +557,7 @@ bool PGM_BASE::InitPgm( bool aHeadless, bool aSkipPyInit, bool aIsUnitTest )
|
|||
m_settings_manager = std::make_unique<SETTINGS_MANAGER>( aHeadless );
|
||||
m_background_jobs_monitor = std::make_unique<BACKGROUND_JOBS_MONITOR>();
|
||||
m_notifications_manager = std::make_unique<NOTIFICATIONS_MANAGER>();
|
||||
m_plugin_manager = std::make_unique<API_PLUGIN_MANAGER>( &App() );
|
||||
|
||||
// Our unit test mocks break if we continue
|
||||
// A bug caused InitPgm to terminate early in unit tests and the mocks are...simplistic
|
||||
|
@ -603,6 +605,8 @@ bool PGM_BASE::InitPgm( bool aHeadless, bool aSkipPyInit, bool aIsUnitTest )
|
|||
// Need to create a project early for now (it can have an empty path for the moment)
|
||||
GetSettingsManager().LoadProject( "" );
|
||||
|
||||
m_plugin_manager->ReloadPlugins();
|
||||
|
||||
// This sets the maximum tooltip display duration to 10s (up from 5) but only affects
|
||||
// Windows as other platforms display tooltips while the mouse is not moving
|
||||
if( !aHeadless )
|
||||
|
|
|
@ -904,19 +904,6 @@ template<> void JSON_SETTINGS::Set<wxString>( const std::string& aPath, wxString
|
|||
}
|
||||
|
||||
|
||||
// Specializations to allow directly reading/writing wxStrings from JSON
|
||||
void to_json( nlohmann::json& aJson, const wxString& aString )
|
||||
{
|
||||
aJson = aString.ToUTF8();
|
||||
}
|
||||
|
||||
|
||||
void from_json( const nlohmann::json& aJson, wxString& aString )
|
||||
{
|
||||
aString = wxString( aJson.get<std::string>().c_str(), wxConvUTF8 );
|
||||
}
|
||||
|
||||
|
||||
template<typename ResultType>
|
||||
ResultType JSON_SETTINGS::fetchOrDefault( const nlohmann::json& aJson, const std::string& aKey,
|
||||
ResultType aDefault )
|
||||
|
|
|
@ -0,0 +1,142 @@
|
|||
/*
|
||||
* This program source code file is part of KiCad, a free EDA CAD application.
|
||||
*
|
||||
* Copyright (C) 2024 Jon Evans <jon@craftyjon.com>
|
||||
* Copyright (C) 2024 KiCad Developers, see AUTHORS.txt for contributors.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU General Public License as published by the
|
||||
* Free Software Foundation, either version 3 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
* General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#ifndef KICAD_API_PLUGIN_H
|
||||
#define KICAD_API_PLUGIN_H
|
||||
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <set>
|
||||
#include <nlohmann/json_fwd.hpp>
|
||||
#include <wx/bmpbndl.h>
|
||||
#include <wx/filename.h>
|
||||
#include <wx/string.h>
|
||||
|
||||
#include <kicommon.h>
|
||||
|
||||
|
||||
struct API_PLUGIN_CONFIG;
|
||||
class API_PLUGIN;
|
||||
|
||||
|
||||
struct PLUGIN_DEPENDENCY
|
||||
{
|
||||
wxString package_name;
|
||||
wxString version;
|
||||
};
|
||||
|
||||
|
||||
enum class PLUGIN_RUNTIME_TYPE
|
||||
{
|
||||
INVALID,
|
||||
PYTHON,
|
||||
EXEC
|
||||
};
|
||||
|
||||
|
||||
enum class PLUGIN_ACTION_SCOPE
|
||||
{
|
||||
INVALID,
|
||||
PCB,
|
||||
SCHEMATIC,
|
||||
FOOTPRINT,
|
||||
SYMBOL,
|
||||
KICAD
|
||||
};
|
||||
|
||||
|
||||
struct PLUGIN_RUNTIME
|
||||
{
|
||||
bool FromJson( const nlohmann::json& aJson );
|
||||
|
||||
PLUGIN_RUNTIME_TYPE type;
|
||||
wxString min_version;
|
||||
std::vector<PLUGIN_DEPENDENCY> dependencies;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* An action performed by a plugin via the IPC API
|
||||
* (not to be confused with ACTION_PLUGIN, the old SWIG plugin system, which will be removed
|
||||
* in the future)
|
||||
*/
|
||||
struct PLUGIN_ACTION
|
||||
{
|
||||
PLUGIN_ACTION( const API_PLUGIN& aPlugin ) :
|
||||
plugin( aPlugin )
|
||||
{}
|
||||
|
||||
wxString identifier;
|
||||
wxString name;
|
||||
wxString description;
|
||||
bool show_button;
|
||||
wxString entrypoint;
|
||||
std::set<PLUGIN_ACTION_SCOPE> scopes;
|
||||
std::vector<wxString> args;
|
||||
wxBitmapBundle icon_light;
|
||||
wxBitmapBundle icon_dark;
|
||||
|
||||
const API_PLUGIN& plugin;
|
||||
};
|
||||
|
||||
/**
|
||||
* A plugin that is invoked by KiCad and runs as an external process; communicating with KiCad
|
||||
* via the IPC API. The plugin metadata is read from a JSON file containing things like which
|
||||
* actions the plugin is capable of and how to invoke each one.
|
||||
*/
|
||||
class KICOMMON_API API_PLUGIN
|
||||
{
|
||||
public:
|
||||
API_PLUGIN( const wxFileName& aConfigFile );
|
||||
|
||||
~API_PLUGIN();
|
||||
|
||||
bool IsOk() const;
|
||||
|
||||
const wxString& Identifier() const;
|
||||
const wxString& Name() const;
|
||||
const wxString& Description() const;
|
||||
const PLUGIN_RUNTIME& Runtime() const;
|
||||
wxString BasePath() const;
|
||||
|
||||
const std::vector<PLUGIN_ACTION>& Actions() const;
|
||||
|
||||
private:
|
||||
friend struct API_PLUGIN_CONFIG;
|
||||
|
||||
std::optional<PLUGIN_ACTION> createActionFromJson( const nlohmann::json& aJson );
|
||||
|
||||
wxFileName m_configFile;
|
||||
|
||||
std::unique_ptr<API_PLUGIN_CONFIG> m_config;
|
||||
};
|
||||
|
||||
/**
|
||||
* Comparison functor for ensuring API_PLUGINs have unique identifiers
|
||||
*/
|
||||
struct CompareApiPluginIdentifiers
|
||||
{
|
||||
bool operator()( const std::unique_ptr<API_PLUGIN>& item1, const std::unique_ptr<API_PLUGIN>& item2 ) const
|
||||
{
|
||||
return item1->Identifier() < item2->Identifier();
|
||||
}
|
||||
};
|
||||
|
||||
#endif //KICAD_API_PLUGIN_H
|
|
@ -0,0 +1,102 @@
|
|||
/*
|
||||
* This program source code file is part of KiCad, a free EDA CAD application.
|
||||
*
|
||||
* Copyright (C) 2024 Jon Evans <jon@craftyjon.com>
|
||||
* Copyright (C) 2024 KiCad Developers, see AUTHORS.txt for contributors.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU General Public License as published by the
|
||||
* Free Software Foundation, either version 3 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
* General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include <deque>
|
||||
#include <memory>
|
||||
#include <set>
|
||||
|
||||
#include <wx/event.h>
|
||||
|
||||
#include <api/api_plugin.h>
|
||||
#include <kicommon.h>
|
||||
|
||||
/**
|
||||
* Flag to enable debug output related to the API plugin system
|
||||
*
|
||||
* Use "KICAD_API" to enable.
|
||||
*
|
||||
* @ingroup traceApi
|
||||
*/
|
||||
extern const KICOMMON_API wxChar* const traceApi;
|
||||
|
||||
/// Internal event used for handling async tasks
|
||||
wxDECLARE_EVENT( EDA_EVT_PLUGIN_MANAGER_JOB_FINISHED, wxCommandEvent );
|
||||
|
||||
/// Notifies other parts of KiCad when plugin availability changes
|
||||
extern const KICOMMON_API wxEventTypeTag<wxCommandEvent> EDA_EVT_PLUGIN_AVAILABILITY_CHANGED;
|
||||
/**
|
||||
* Responsible for loading plugin definitions for API-based plugins (ones that do not run inside
|
||||
* KiCad itself, but instead are launched as external processes by KiCad)
|
||||
*/
|
||||
class KICOMMON_API API_PLUGIN_MANAGER : public wxEvtHandler
|
||||
{
|
||||
public:
|
||||
API_PLUGIN_MANAGER( wxEvtHandler* aParent );
|
||||
|
||||
void ReloadPlugins();
|
||||
|
||||
void InvokeAction( const wxString& aIdentifier );
|
||||
|
||||
std::vector<const PLUGIN_ACTION*> GetActionsForScope( PLUGIN_ACTION_SCOPE aScope );
|
||||
|
||||
std::map<int, wxString>& ButtonBindings() { return m_buttonBindings; }
|
||||
|
||||
std::map<int, wxString>& MenuBindings() { return m_menuBindings; }
|
||||
|
||||
private:
|
||||
void processPluginDependencies();
|
||||
|
||||
void processNextJob( wxCommandEvent& aEvent );
|
||||
|
||||
wxEvtHandler* m_parent;
|
||||
|
||||
std::set<std::unique_ptr<API_PLUGIN>, CompareApiPluginIdentifiers> m_plugins;
|
||||
|
||||
std::map<wxString, const API_PLUGIN*> m_pluginsCache;
|
||||
|
||||
std::map<wxString, const PLUGIN_ACTION*> m_actionsCache;
|
||||
|
||||
/// Map of plugin identifier to a path for the plugin's virtual environment, if it has one
|
||||
std::map<wxString, wxString> m_environmentCache;
|
||||
|
||||
/// Map of button wx item id to action identifier
|
||||
std::map<int, wxString> m_buttonBindings;
|
||||
|
||||
/// Map of menu wx item id to action identifier
|
||||
std::map<int, wxString> m_menuBindings;
|
||||
|
||||
std::set<wxString> m_readyPlugins;
|
||||
|
||||
enum class JOB_TYPE
|
||||
{
|
||||
CREATE_ENV,
|
||||
INSTALL_REQUIREMENTS
|
||||
};
|
||||
|
||||
struct JOB
|
||||
{
|
||||
JOB_TYPE type;
|
||||
wxString identifier;
|
||||
wxString plugin_path;
|
||||
wxString env_path;
|
||||
};
|
||||
|
||||
std::deque<JOB> m_jobs;
|
||||
};
|
|
@ -60,6 +60,10 @@ public:
|
|||
|
||||
void SetReadyToReply( bool aReady = true ) { m_readyToReply = aReady; }
|
||||
|
||||
const std::string& SocketPath() const { return m_socketPath; }
|
||||
|
||||
const std::string& Token() const { return m_token; }
|
||||
|
||||
private:
|
||||
|
||||
/**
|
||||
|
@ -83,6 +87,8 @@ private:
|
|||
|
||||
std::set<API_HANDLER*> m_handlers;
|
||||
|
||||
std::string m_socketPath;
|
||||
|
||||
std::string m_token;
|
||||
|
||||
bool m_readyToReply;
|
||||
|
|
|
@ -0,0 +1,33 @@
|
|||
/*
|
||||
* This program source code file is part of KiCad, a free EDA CAD application.
|
||||
*
|
||||
* Copyright (C) 2024 KiCad Developers, see AUTHORS.txt for contributors.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU General Public License as published by the
|
||||
* Free Software Foundation, either version 3 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
* General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#ifndef KICAD_JSON_CONVERSIONS_H
|
||||
#define KICAD_JSON_CONVERSIONS_H
|
||||
|
||||
#include <kicommon.h>
|
||||
#include <nlohmann/json_fwd.hpp>
|
||||
#include <wx/string.h>
|
||||
|
||||
// Specializations to allow directly reading/writing wxStrings from JSON
|
||||
|
||||
KICOMMON_API void to_json( nlohmann::json& aJson, const wxString& aString );
|
||||
|
||||
KICOMMON_API void from_json( const nlohmann::json& aJson, wxString& aString );
|
||||
|
||||
#endif //KICAD_JSON_CONVERSIONS_H
|
|
@ -52,11 +52,6 @@ public:
|
|||
*/
|
||||
static wxString GetUserPluginsPath();
|
||||
|
||||
/**
|
||||
* Gets the user path for 3d viewer plugin
|
||||
*/
|
||||
static wxString GetUserPlugins3DPath();
|
||||
|
||||
/**
|
||||
* Gets the default path we point users to create projects
|
||||
*/
|
||||
|
|
|
@ -53,6 +53,7 @@ class SETTINGS_MANAGER;
|
|||
class SCRIPTING;
|
||||
|
||||
#ifdef KICAD_IPC_API
|
||||
class API_PLUGIN_MANAGER;
|
||||
class KICAD_API_SERVER;
|
||||
#endif
|
||||
|
||||
|
@ -147,6 +148,8 @@ public:
|
|||
virtual NOTIFICATIONS_MANAGER& GetNotificationsManager() const { return *m_notifications_manager; }
|
||||
|
||||
#ifdef KICAD_IPC_API
|
||||
virtual API_PLUGIN_MANAGER& GetPluginManager() const { return *m_plugin_manager; }
|
||||
|
||||
KICAD_API_SERVER& GetApiServer() { return *m_api_server; }
|
||||
#endif
|
||||
|
||||
|
@ -412,6 +415,7 @@ protected:
|
|||
std::unique_ptr<wxSingleInstanceChecker> m_pgm_checker;
|
||||
|
||||
#ifdef KICAD_IPC_API
|
||||
std::unique_ptr<API_PLUGIN_MANAGER> m_plugin_manager;
|
||||
std::unique_ptr<KICAD_API_SERVER> m_api_server;
|
||||
#endif
|
||||
|
||||
|
|
|
@ -23,6 +23,7 @@
|
|||
#include <nlohmann/json.hpp>
|
||||
#include <wx/string.h>
|
||||
#include <vector>
|
||||
#include <json_conversions.h>
|
||||
|
||||
/**
|
||||
* Contains the json serialization structs for DRC and ERC reports
|
||||
|
@ -119,4 +120,4 @@ NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE( ERC_REPORT, $schema, source, date, kicad_ver
|
|||
|
||||
} // namespace RC_JSON
|
||||
|
||||
#endif
|
||||
#endif
|
||||
|
|
|
@ -29,6 +29,7 @@
|
|||
#include <functional>
|
||||
#include <optional>
|
||||
#include <settings/json_settings_internals.h>
|
||||
#include <json_conversions.h>
|
||||
|
||||
#include <kicommon.h>
|
||||
|
||||
|
|
|
@ -43,6 +43,8 @@ public:
|
|||
|
||||
void Reply( const std::string& aReply );
|
||||
|
||||
const std::string& SocketPath() const { return m_socketUrl; }
|
||||
|
||||
private:
|
||||
void listenThread();
|
||||
|
||||
|
|
|
@ -38,7 +38,8 @@ static const wxString s_defaultCommitMessage = wxS( "Modification from API" );
|
|||
|
||||
API_HANDLER_PCB::API_HANDLER_PCB( PCB_EDIT_FRAME* aFrame ) :
|
||||
API_HANDLER(),
|
||||
m_frame( aFrame )
|
||||
m_frame( aFrame ),
|
||||
m_transactionInProgress( false )
|
||||
{
|
||||
registerHandler<RunAction, RunActionResponse>( &API_HANDLER_PCB::handleRunAction );
|
||||
registerHandler<GetOpenDocuments, GetOpenDocumentsResponse>(
|
||||
|
|
|
@ -26,6 +26,7 @@
|
|||
#include <pgm_base.h>
|
||||
#include <pcb_edit_frame.h>
|
||||
#include <3d_viewer/eda_3d_viewer_frame.h>
|
||||
#include <api/api_plugin_manager.h>
|
||||
#include <fp_lib_table.h>
|
||||
#include <bitmaps.h>
|
||||
#include <confirm.h>
|
||||
|
@ -265,6 +266,14 @@ PCB_EDIT_FRAME::PCB_EDIT_FRAME( KIWAY* aKiway, wxWindow* aParent ) :
|
|||
ReCreateVToolbar();
|
||||
ReCreateOptToolbar();
|
||||
|
||||
wxTheApp->Bind( EDA_EVT_PLUGIN_AVAILABILITY_CHANGED,
|
||||
[&]( wxCommandEvent& aEvt )
|
||||
{
|
||||
wxLogTrace( traceApi, "PCB frame: EDA_EVT_PLUGIN_AVAILABILITY_CHANGED" );
|
||||
ReCreateHToolbar();
|
||||
aEvt.Skip();
|
||||
} );
|
||||
|
||||
|
||||
m_propertiesPanel = new PCB_PROPERTIES_PANEL( this, this );
|
||||
|
||||
|
@ -2053,6 +2062,24 @@ void PCB_EDIT_FRAME::PythonSyncProjectName()
|
|||
}
|
||||
|
||||
|
||||
void PCB_EDIT_FRAME::OnApiPluginMenu( wxCommandEvent& aEvent )
|
||||
{
|
||||
API_PLUGIN_MANAGER& mgr = Pgm().GetPluginManager();
|
||||
|
||||
if( mgr.MenuBindings().count( aEvent.GetId() ) )
|
||||
mgr.InvokeAction( mgr.MenuBindings().at( aEvent.GetId() ) );
|
||||
}
|
||||
|
||||
|
||||
void PCB_EDIT_FRAME::OnApiPluginButton( wxCommandEvent& aEvent )
|
||||
{
|
||||
API_PLUGIN_MANAGER& mgr = Pgm().GetPluginManager();
|
||||
|
||||
if( mgr.ButtonBindings().count( aEvent.GetId() ) )
|
||||
mgr.InvokeAction( mgr.ButtonBindings().at( aEvent.GetId() ) );
|
||||
}
|
||||
|
||||
|
||||
void PCB_EDIT_FRAME::ShowFootprintPropertiesDialog( FOOTPRINT* aFootprint )
|
||||
{
|
||||
if( aFootprint == nullptr )
|
||||
|
|
|
@ -760,6 +760,11 @@ protected:
|
|||
*/
|
||||
void AddActionPluginTools();
|
||||
|
||||
/**
|
||||
* Append actions from API plugins to the main toolbar
|
||||
*/
|
||||
void AddApiPluginTools();
|
||||
|
||||
/**
|
||||
* Execute action plugin's Run() method and updates undo buffer.
|
||||
*
|
||||
|
@ -781,6 +786,9 @@ protected:
|
|||
*/
|
||||
void OnActionPluginButton( wxCommandEvent& aEvent );
|
||||
|
||||
void OnApiPluginMenu( wxCommandEvent& aEvent );
|
||||
void OnApiPluginButton( wxCommandEvent& aEvent );
|
||||
|
||||
/**
|
||||
* Update the state of the GUI after a new board is loaded or created.
|
||||
*/
|
||||
|
|
|
@ -24,9 +24,11 @@
|
|||
#include "pcb_scripting_tool.h"
|
||||
|
||||
#include <action_plugin.h>
|
||||
#include <api/api_plugin_manager.h>
|
||||
#include <kiface_ids.h>
|
||||
#include <kiway.h>
|
||||
#include <macros.h>
|
||||
#include <pgm_base.h>
|
||||
#include <python_scripting.h>
|
||||
#include <tools/pcb_actions.h>
|
||||
|
||||
|
@ -106,6 +108,9 @@ int SCRIPTING_TOOL::reloadPlugins( const TOOL_EVENT& aEvent )
|
|||
return -1;
|
||||
}
|
||||
|
||||
// TODO move this elsewhere when SWIG plugins are removed
|
||||
Pgm().GetPluginManager().ReloadPlugins();
|
||||
|
||||
if( !m_isFootprintEditor )
|
||||
{
|
||||
// Action plugins can be modified, therefore the plugins menu must be updated:
|
||||
|
|
|
@ -27,10 +27,12 @@
|
|||
#include <memory>
|
||||
|
||||
#include <advanced_config.h>
|
||||
#include <api/api_plugin_manager.h>
|
||||
#include <bitmaps.h>
|
||||
#include <board.h>
|
||||
#include <board_design_settings.h>
|
||||
#include <kiface_base.h>
|
||||
#include <kiplatform/ui.h>
|
||||
#include <macros.h>
|
||||
#include <pcb_edit_frame.h>
|
||||
#include <pcb_layer_box_selector.h>
|
||||
|
@ -280,12 +282,23 @@ void PCB_EDIT_FRAME::ReCreateHToolbar()
|
|||
m_mainToolBar->AddScaledSeparator( this );
|
||||
m_mainToolBar->Add( PCB_ACTIONS::showEeschema );
|
||||
|
||||
// Access to the scripting console
|
||||
if( SCRIPTING::IsWxAvailable() )
|
||||
// Add SWIG and API plugins
|
||||
bool scriptingAvailable = SCRIPTING::IsWxAvailable();
|
||||
bool haveApiPlugins =
|
||||
!Pgm().GetPluginManager().GetActionsForScope( PLUGIN_ACTION_SCOPE::PCB ).empty();
|
||||
|
||||
if( scriptingAvailable || haveApiPlugins )
|
||||
{
|
||||
m_mainToolBar->AddScaledSeparator( this );
|
||||
m_mainToolBar->Add( PCB_ACTIONS::showPythonConsole, ACTION_TOOLBAR::TOGGLE );
|
||||
AddActionPluginTools();
|
||||
|
||||
if( scriptingAvailable )
|
||||
{
|
||||
m_mainToolBar->Add( PCB_ACTIONS::showPythonConsole, ACTION_TOOLBAR::TOGGLE );
|
||||
AddActionPluginTools();
|
||||
}
|
||||
|
||||
if( haveApiPlugins )
|
||||
AddApiPluginTools();
|
||||
}
|
||||
|
||||
// after adding the buttons to the toolbar, must call Realize() to reflect the changes
|
||||
|
@ -293,6 +306,35 @@ void PCB_EDIT_FRAME::ReCreateHToolbar()
|
|||
}
|
||||
|
||||
|
||||
void PCB_EDIT_FRAME::AddApiPluginTools()
|
||||
{
|
||||
// TODO: Add user control over visibility and order
|
||||
API_PLUGIN_MANAGER& mgr = Pgm().GetPluginManager();
|
||||
|
||||
mgr.ButtonBindings().clear();
|
||||
|
||||
std::vector<const PLUGIN_ACTION*> actions = mgr.GetActionsForScope( PLUGIN_ACTION_SCOPE::PCB );
|
||||
|
||||
for( auto& action : actions )
|
||||
{
|
||||
if( !action->show_button )
|
||||
continue;
|
||||
|
||||
const wxBitmapBundle& icon = KIPLATFORM::UI::IsDarkTheme() && action->icon_dark.IsOk()
|
||||
? action->icon_dark
|
||||
: action->icon_light;
|
||||
|
||||
wxAuiToolBarItem* button = m_mainToolBar->AddTool( wxID_ANY, wxEmptyString, icon,
|
||||
action->name );
|
||||
|
||||
Connect( button->GetId(), wxEVT_COMMAND_MENU_SELECTED,
|
||||
wxCommandEventHandler( PCB_EDIT_FRAME::OnApiPluginButton ) );
|
||||
|
||||
mgr.ButtonBindings().insert( { button->GetId(), action->identifier } );
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void PCB_EDIT_FRAME::ReCreateOptToolbar()
|
||||
{
|
||||
// Note:
|
||||
|
|
|
@ -22,12 +22,14 @@
|
|||
|
||||
#include <utility>
|
||||
|
||||
#include "python_manager.h"
|
||||
#include <paths.h>
|
||||
#include <python_manager.h>
|
||||
|
||||
|
||||
class PYTHON_PROCESS : public wxProcess
|
||||
{
|
||||
public:
|
||||
PYTHON_PROCESS( std::function<void(int, const wxString&)> aCallback ) :
|
||||
PYTHON_PROCESS( std::function<void(int, const wxString&, const wxString&)> aCallback ) :
|
||||
wxProcess(),
|
||||
m_callback( std::move( aCallback ) )
|
||||
{}
|
||||
|
@ -36,7 +38,7 @@ public:
|
|||
{
|
||||
if( m_callback )
|
||||
{
|
||||
wxString output;
|
||||
wxString output, error;
|
||||
wxInputStream* processOut = GetInputStream();
|
||||
size_t bytesRead = 0;
|
||||
|
||||
|
@ -48,26 +50,88 @@ public:
|
|||
bytesRead += processOut->LastRead();
|
||||
}
|
||||
|
||||
m_callback( aStatus, output );
|
||||
processOut = GetErrorStream();
|
||||
bytesRead = 0;
|
||||
|
||||
while( processOut->CanRead() && bytesRead < MAX_OUTPUT_LEN )
|
||||
{
|
||||
char buffer[4096];
|
||||
buffer[ processOut->Read( buffer, sizeof( buffer ) - 1 ).LastRead() ] = '\0';
|
||||
error.append( buffer, sizeof( buffer ) );
|
||||
bytesRead += processOut->LastRead();
|
||||
}
|
||||
|
||||
m_callback( aStatus, output, error );
|
||||
}
|
||||
}
|
||||
|
||||
static constexpr size_t MAX_OUTPUT_LEN = 1024L * 1024L;
|
||||
|
||||
private:
|
||||
std::function<void(int, const wxString&)> m_callback;
|
||||
std::function<void(int, const wxString&, const wxString&)> m_callback;
|
||||
};
|
||||
|
||||
|
||||
void PYTHON_MANAGER::Execute( const wxString& aArgs,
|
||||
const std::function<void( int, const wxString& )>& aCallback )
|
||||
const std::function<void( int, const wxString&,
|
||||
const wxString& )>& aCallback,
|
||||
const wxExecuteEnv* aEnv )
|
||||
{
|
||||
PYTHON_PROCESS* process = new PYTHON_PROCESS( aCallback );
|
||||
process->Redirect();
|
||||
|
||||
wxString cmd = wxString::Format( wxS( "%s %s" ), m_interpreterPath, aArgs );
|
||||
long pid = wxExecute( cmd, wxEXEC_ASYNC, process );
|
||||
long pid = wxExecute( cmd, wxEXEC_ASYNC, process, aEnv );
|
||||
|
||||
if( pid == 0 )
|
||||
aCallback( -1, wxEmptyString );
|
||||
aCallback( -1, wxEmptyString, _( "Process could not be created" ) );
|
||||
}
|
||||
|
||||
|
||||
wxString PYTHON_MANAGER::FindPythonInterpreter()
|
||||
{
|
||||
#ifdef __WXMSW__
|
||||
// TODO(JE) where
|
||||
#else
|
||||
wxArrayString output;
|
||||
|
||||
if( 0 == wxExecute( wxS( "which -a python" ), output, wxEXEC_SYNC ) )
|
||||
{
|
||||
if( !output.IsEmpty() )
|
||||
return output[0];
|
||||
}
|
||||
#endif
|
||||
|
||||
return wxEmptyString;
|
||||
}
|
||||
|
||||
|
||||
std::optional<wxString> PYTHON_MANAGER::GetPythonEnvironment( const wxString& aNamespace )
|
||||
{
|
||||
wxFileName path( PATHS::GetUserCachePath(), wxEmptyString );
|
||||
path.AppendDir( wxS( "python-environments" ) );
|
||||
path.AppendDir( aNamespace );
|
||||
|
||||
if( !PATHS::EnsurePathExists( path.GetPath() ) )
|
||||
return std::nullopt;
|
||||
|
||||
return path.GetPath();
|
||||
}
|
||||
|
||||
|
||||
std::optional<wxString> PYTHON_MANAGER::GetVirtualPython( const wxString& aNamespace )
|
||||
{
|
||||
std::optional<wxString> envPath = GetPythonEnvironment( aNamespace );
|
||||
|
||||
if( !envPath )
|
||||
return std::nullopt;
|
||||
|
||||
wxFileName python( *envPath, wxEmptyString );
|
||||
python.AppendDir( "bin" );
|
||||
python.SetFullName( "python" );
|
||||
|
||||
if( !python.IsFileExecutable() )
|
||||
return std::nullopt;
|
||||
|
||||
return python.GetFullPath();
|
||||
}
|
||||
|
|
|
@ -35,11 +35,23 @@ public:
|
|||
{}
|
||||
|
||||
void Execute( const wxString& aArgs,
|
||||
const std::function<void(int, const wxString&)>& aCallback );
|
||||
const std::function<void(int, const wxString&, const wxString&)>& aCallback,
|
||||
const wxExecuteEnv* aEnv = nullptr );
|
||||
|
||||
wxString GetInterpreterPath() const { return m_interpreterPath; }
|
||||
void SetInterpreterPath( const wxString& aPath ) { m_interpreterPath = aPath; }
|
||||
|
||||
/**
|
||||
* Searches for a Python intepreter on the user's system
|
||||
* @return the absolute path to a Python interpreter, or an empty string if one was not found
|
||||
*/
|
||||
static wxString FindPythonInterpreter();
|
||||
|
||||
static std::optional<wxString> GetPythonEnvironment( const wxString& aNamespace );
|
||||
|
||||
/// Returns a full path to the python binary in a venv, if it exists
|
||||
static std::optional<wxString> GetVirtualPython( const wxString& aNamespace );
|
||||
|
||||
private:
|
||||
wxString m_interpreterPath;
|
||||
};
|
||||
|
|
Loading…
Reference in New Issue