KiCad PCB EDA Suite
Loading...
Searching...
No Matches
api_plugin.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 along
18 * with this program. If not, see <http://www.gnu.org/licenses/>.
19 */
20
21#include <magic_enum.hpp>
22#include <json_common.h>
23#include <wx/log.h>
24#include <wx/regex.h>
25#include <wx/stdstream.h>
26#include <wx/wfstream.h>
27
28#include <api/api_plugin.h>
30#include <api/api_utils.h>
31#include <json_conversions.h>
33
34
39
40
41void LOGGING_ERROR_HANDLER::error( const nlohmann::json::json_pointer& ptr,
42 const nlohmann::json& instance,
43 const std::string& message )
44{
45 m_hasError = true;
46 wxLogTrace( traceApi,
47 wxString::Format( wxS( "JSON error: at %s, value:\n%s\n%s" ),
48 ptr.to_string(), instance.dump(), message ) );
49
50 wxString location = wxString::FromUTF8( ptr.to_string() );
51
52 if( location.IsEmpty() )
53 location = wxS( "/" );
54
55 if( !m_errorMessage.IsEmpty() )
56 m_errorMessage << '\n';
57
58 m_errorMessage << wxString::Format( _( "invalid plugin configuration at '%s': %s" ),
59 location, wxString::FromUTF8( message ) );
60}
61
62
63tl::expected<bool, wxString> PLUGIN_RUNTIME::FromJson( const nlohmann::json& aJson )
64{
65 try
66 {
67 type = magic_enum::enum_cast<PLUGIN_RUNTIME_TYPE>( aJson.at( "type" ).get<std::string>(),
68 magic_enum::case_insensitive )
70 }
71 catch( std::exception& e )
72 {
73 return tl::unexpected( wxString::Format( _( "invalid plugin runtime: %s" ), e.what() ) );
74 }
75
77}
78
79
81{
82 API_PLUGIN_CONFIG( API_PLUGIN& aParent, const wxFileName& aConfigFile,
83 const JSON_SCHEMA_VALIDATOR& aValidator );
84
85 bool valid;
86 wxString error_message;
87 wxString identifier;
88 wxString name;
89 wxString description;
91 std::vector<PLUGIN_ACTION> actions;
92
94};
95
96
97API_PLUGIN_CONFIG::API_PLUGIN_CONFIG( API_PLUGIN& aParent, const wxFileName& aConfigFile,
98 const JSON_SCHEMA_VALIDATOR& aValidator ) :
99 parent( aParent )
100{
101 valid = false;
102
103 if( !aConfigFile.IsFileReadable() )
104 {
105 error_message = _( "could not read plugin configuration file" );
106 return;
107 }
108
109 wxLogTrace( traceApi, "Plugin: parsing config file" );
110
111 wxFFileInputStream fp( aConfigFile.GetFullPath(), wxT( "rt" ) );
112 wxStdInputStream fstream( fp );
113
114 nlohmann::json js;
115
116 try
117 {
118 js = nlohmann::json::parse( fstream, nullptr,
119 /* allow_exceptions = */ true,
120 /* ignore_comments = */ true );
121 }
122 catch( const std::exception& e )
123 {
124 wxLogTrace( traceApi, "Plugin: exception during parse" );
125 error_message = wxString::Format( _( "plugin configuration file error: %s" ),
126 wxString::FromUTF8( e.what() ) );
127 return;
128 }
129
130 LOGGING_ERROR_HANDLER handler;
131 aValidator.Validate( js, handler, nlohmann::json_uri( "#/definitions/Plugin" ) );
132
133 if( !handler.HasError() )
134 wxLogTrace( traceApi, "Plugin: schema validation successful" );
135 else
136 error_message = handler.ErrorMessage();
137
138 // All of these are required; any exceptions here leave us with valid == false
139 try
140 {
141 identifier = js.at( "identifier" ).get<wxString>();
142 name = js.at( "name" ).get<wxString>();
143 description = js.at( "description" ).get<wxString>();
144
145 if( !runtime.FromJson( js.at( "runtime" ) ).or_else(
146 [this]( const wxString& aError )
147 {
148 wxLogTrace( traceApi, "Plugin %s: %s", identifier, aError );
149 error_message = aError;
150 } ).has_value() )
151 {
152 return;
153 }
154 }
155 catch( const std::exception& e )
156 {
157 wxLogTrace( traceApi, "Plugin: exception while parsing required keys" );
158 error_message = wxString::Format( _( "missing or invalid required keys: %s" ),
159 wxString::FromUTF8( e.what() ) );
160 return;
161 }
162
164 {
165 wxLogTrace( traceApi, wxString::Format( "Plugin: identifier %s does not meet requirements",
166 identifier ) );
167 error_message = wxString::Format( _( "identifier '%s' is invalid" ), identifier );
168 return;
169 }
170
171 wxLogTrace( traceApi, wxString::Format( "Plugin: %s (%s)", identifier, name ) );
172
173 try
174 {
175 const nlohmann::json& actionsJs = js.at( "actions" );
176
177 if( actionsJs.is_array() )
178 {
179 for( const nlohmann::json& actionJs : actionsJs )
180 {
181 if( std::optional<PLUGIN_ACTION> a = parent.createActionFromJson( actionJs ) )
182 {
183 a->identifier = wxString::Format( "%s.%s", identifier, a->identifier );
184 wxLogTrace( traceApi, wxString::Format( "Plugin: loaded action %s",
185 a->identifier ) );
186 actions.emplace_back( *a );
187 }
188 }
189 }
190 }
191 catch( const std::exception& e )
192 {
193 wxLogTrace( traceApi, "Plugin: exception while parsing actions" );
194 error_message = wxString::Format( _( "actions section is invalid: %s" ),
195 wxString::FromUTF8( e.what() ) );
196 return;
197 }
198
199 valid = true;
200 error_message = wxEmptyString;
201}
202
203
204API_PLUGIN::API_PLUGIN( const wxFileName& aConfigFile, const JSON_SCHEMA_VALIDATOR& aValidator ) :
205 m_configFile( aConfigFile ),
206 m_config( std::make_unique<API_PLUGIN_CONFIG>( *this, aConfigFile, aValidator ) )
207{
208}
209
210
214
215
217{
218 return m_config->valid;
219}
220
221
222const wxString& API_PLUGIN::ErrorMessage() const
223{
224 return m_config->error_message;
225}
226
227
228bool API_PLUGIN::IsValidIdentifier( const wxString& aIdentifier )
229{
230 // At minimum, we need a reverse-DNS style identifier with two dots and a 2+ character TLD
231 wxRegEx identifierRegex( wxS( "[\\w\\d]{2,}\\.[\\w\\d]+\\.[\\w\\d]+" ) );
232 return identifierRegex.Matches( aIdentifier );
233}
234
235
236const wxString& API_PLUGIN::Identifier() const
237{
238 return m_config->identifier;
239}
240
241
242const wxString& API_PLUGIN::Name() const
243{
244 return m_config->name;
245}
246
247
248const wxString& API_PLUGIN::Description() const
249{
250 return m_config->description;
251}
252
253
255{
256 return m_config->runtime;
257}
258
259
260const std::vector<PLUGIN_ACTION>& API_PLUGIN::Actions() const
261{
262 return m_config->actions;
263}
264
265
266wxString API_PLUGIN::BasePath() const
267{
268 return m_configFile.GetPath();
269}
270
271
272wxString API_PLUGIN::ActionSettingsKey( const PLUGIN_ACTION& aAction ) const
273{
274 return Identifier() + "." + aAction.identifier;
275}
276
277
278
279std::optional<PLUGIN_ACTION> API_PLUGIN::createActionFromJson( const nlohmann::json& aJson )
280{
281 // TODO move to tl::expected and give user feedback about parse errors
282 PLUGIN_ACTION action( *this );
283
284 try
285 {
286 action.identifier = aJson.at( "identifier" ).get<wxString>();
287 wxLogTrace( traceApi, wxString::Format( "Plugin: load action %s", action.identifier ) );
288 action.name = aJson.at( "name" ).get<wxString>();
289 action.description = aJson.at( "description" ).get<wxString>();
290 action.entrypoint = aJson.at( "entrypoint" ).get<wxString>();
291 action.show_button = aJson.contains( "show-button" ) && aJson.at( "show-button" ).get<bool>();
292 }
293 catch( ... )
294 {
295 wxLogTrace( traceApi, "Plugin: exception while parsing action required keys" );
296 return std::nullopt;
297 }
298
299 wxFileName f( action.entrypoint );
300
301 if( !f.IsRelative() )
302 {
303 wxLogTrace( traceApi, wxString::Format( "Plugin: action contains abs path %s; skipping",
304 action.entrypoint ) );
305 return std::nullopt;
306 }
307
308 f.Normalize( wxPATH_NORM_ABSOLUTE, m_configFile.GetPath() );
309
310 if( !f.IsFileReadable() )
311 {
312 wxLogTrace( traceApi, wxString::Format( "WARNING: action entrypoint %s is not readable",
313 f.GetFullPath() ) );
314 }
315
316 if( aJson.contains( "args" ) && aJson.at( "args" ).is_array() )
317 {
318 for( const nlohmann::json& argJs : aJson.at( "args" ) )
319 {
320 try
321 {
322 action.args.emplace_back( argJs.get<wxString>() );
323 }
324 catch( ... )
325 {
326 wxLogTrace( traceApi, "Plugin: exception while parsing action args" );
327 continue;
328 }
329 }
330 }
331
332 if( aJson.contains( "scopes" ) && aJson.at( "scopes" ).is_array() )
333 {
334 for( const nlohmann::json& scopeJs : aJson.at( "scopes" ) )
335 {
336 try
337 {
338 action.scopes.insert( magic_enum::enum_cast<PLUGIN_ACTION_SCOPE>(
339 scopeJs.get<std::string>(), magic_enum::case_insensitive )
340 .value_or( PLUGIN_ACTION_SCOPE::INVALID ) );
341 }
342 catch( ... )
343 {
344 wxLogTrace( traceApi, "Plugin: exception while parsing action scopes" );
345 continue;
346 }
347 }
348 }
349
350 auto handleBitmap =
351 [&]( const std::string& aKey, wxBitmapBundle& aDest )
352 {
353 if( aJson.contains( aKey ) && aJson.at( aKey ).is_array() )
354 {
355 wxVector<wxBitmap> bitmaps;
356
357 for( const nlohmann::json& iconJs : aJson.at( aKey ) )
358 {
359 wxFileName iconFile;
360
361 try
362 {
363 iconFile = iconJs.get<wxString>();
364 }
365 catch( ... )
366 {
367 continue;
368 }
369
370 iconFile.Normalize( wxPATH_NORM_ABSOLUTE, m_configFile.GetPath() );
371
372 wxLogTrace( traceApi,
373 wxString::Format( "Plugin: action %s: loading icon %s",
374 action.identifier, iconFile.GetFullPath() ) );
375
376
377 if( !iconFile.IsFileReadable() )
378 {
379 wxLogTrace( traceApi, "Plugin: icon file could not be read" );
380 continue;
381 }
382
383 wxBitmap bmp;
384 // TODO: If necessary; support types other than PNG
385 bmp.LoadFile( iconFile.GetFullPath(), wxBITMAP_TYPE_PNG );
386
387 if( bmp.IsOk() )
388 bitmaps.push_back( bmp );
389 else
390 wxLogTrace( traceApi, "Plugin: icon file not a valid bitmap" );
391 }
392
393 aDest = wxBitmapBundle::FromBitmaps( bitmaps );
394 }
395 };
396
397 handleBitmap( "icons-light", action.icon_light );
398 handleBitmap( "icons-dark", action.icon_dark );
399
400 return action;
401}
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 & Name() const
const std::vector< PLUGIN_ACTION > & Actions() const
friend struct API_PLUGIN_CONFIG
Definition api_plugin.h:119
wxString ActionSettingsKey(const PLUGIN_ACTION &aAction) const
const wxString & Identifier() const
std::unique_ptr< API_PLUGIN_CONFIG > m_config
Definition api_plugin.h:125
wxFileName m_configFile
Definition api_plugin.h:123
bool IsOk() const
wxString BasePath() const
static bool IsValidIdentifier(const wxString &aIdentifier)
const wxString & Description() const
API_PLUGIN(const wxFileName &aConfigFile, const JSON_SCHEMA_VALIDATOR &aValidator)
std::optional< PLUGIN_ACTION > createActionFromJson(const nlohmann::json &aJson)
const wxString & ErrorMessage() const
nlohmann::json Validate(const nlohmann::json &aJson, nlohmann::json_schema::error_handler &aErrorHandler, const nlohmann::json_uri &aInitialUri=nlohmann::json_uri("#")) const
bool HasError() const
Definition api_plugin.h:146
const wxString & ErrorMessage() const
Definition api_plugin.h:148
void error(const nlohmann::json::json_pointer &ptr, const nlohmann::json &instance, const std::string &message) override
#define _(s)
const wxChar *const traceApi
Flag to enable debug output related to the IPC API and its plugin system.
Definition api_utils.cpp:29
STL namespace.
API_PLUGIN_CONFIG(API_PLUGIN &aParent, const wxFileName &aConfigFile, const JSON_SCHEMA_VALIDATOR &aValidator)
wxString error_message
PLUGIN_RUNTIME runtime
API_PLUGIN & parent
std::vector< PLUGIN_ACTION > actions
An action performed by a plugin via the IPC API.
Definition api_plugin.h:72
wxBitmapBundle icon_light
Definition api_plugin.h:84
wxString name
Definition api_plugin.h:78
wxString description
Definition api_plugin.h:79
std::set< PLUGIN_ACTION_SCOPE > scopes
Definition api_plugin.h:82
wxString identifier
Definition api_plugin.h:77
wxString entrypoint
Definition api_plugin.h:81
wxBitmapBundle icon_dark
Definition api_plugin.h:85
std::vector< wxString > args
Definition api_plugin.h:83
tl::expected< bool, wxString > FromJson(const nlohmann::json &aJson)
PLUGIN_RUNTIME_TYPE type
Definition api_plugin.h:62
VECTOR2I location