KiCad PCB EDA Suite
Loading...
Searching...
No Matches
api_server.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) 2023 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 <fmt/format.h>
22#include <wx/app.h>
23#include <wx/datetime.h>
24#include <wx/event.h>
25#include <wx/stdpaths.h>
26
27#include <advanced_config.h>
28#include <api/api_handler.h>
29#include <api/api_utils.h> // traceApi
30#include <api/api_server.h>
31#include <kiid.h>
32#include <kinng.h>
33#include <paths.h>
34#include <pgm_base.h>
36#include <string_utils.h>
37
38#include <api/common/envelope.pb.h>
39
40#ifdef __UNIX__
41#include <sys/file.h>
42#endif
43
44using kiapi::common::ApiRequest, kiapi::common::ApiResponse, kiapi::common::ApiStatusCode;
45
46
47wxString KICAD_API_SERVER::s_logFileName = "api.log";
48
49
50wxDEFINE_EVENT( API_REQUEST_EVENT, wxCommandEvent );
51
52
54 wxEvtHandler(),
55 m_token( KIID().AsStdString() ),
56 m_readyToReply( false )
57{
58 if( !Pgm().GetCommonSettings()->m_Api.enable_server )
59 {
60 wxLogTrace( traceApi, "Server: disabled by user preferences." );
61 return;
62 }
63
64 Start();
65}
66
67
69{
70 Stop();
71}
72
73
75{
76 if( Running() )
77 return;
78
79 wxFileName socket;
80#ifdef __WXMAC__
81 socket.AssignDir( wxS( "/tmp" ) );
82#else
83 socket.AssignDir( wxStandardPaths::Get().GetTempDir() );
84#endif
85 socket.AppendDir( wxS( "kicad" ) );
86 socket.SetFullName( wxS( "api.sock" ) );
87
88 if( !PATHS::EnsurePathExists( socket.GetPath() ) )
89 {
90 wxLogTrace( traceApi, wxString::Format( "Server: socket path %s could not be created",
91 socket.GetPath() ) );
92 return;
93 }
94
95#ifndef __WINDOWS__
96 // We use non-abstract sockets because macOS and some other non-Linux platforms don't support
97 // abstract sockets, which means there might be an old socket to unlink. In order to try to
98 // recover this, we lock a file (which will be unlocked on process exit) and if we get the lock,
99 // we know the old socket is orphaned and can be removed.
100 wxFileName lockFilePath( socket.GetPath(), wxS( "api.lock" ) );
101
102 int lockFile = open( lockFilePath.GetFullPath().c_str(), O_RDONLY | O_CREAT, 0600 );
103
104 if( lockFile >= 0 && flock( lockFile, LOCK_EX | LOCK_NB ) == 0 )
105 {
106 if( socket.Exists() )
107 {
108 wxLogTrace( traceApi, wxString::Format( "Server: cleaning up stale socket path %s",
109 socket.GetFullPath() ) );
110 wxRemoveFile( socket.GetFullPath() );
111 }
112 }
113#endif
114
115 if( socket.Exists() )
116 {
117 socket.SetFullName( wxString::Format( wxS( "api-%lu.sock" ), ::wxGetProcessId() ) );
118
119 if( socket.Exists() )
120 {
121 wxLogTrace( traceApi, wxString::Format( "Server: PID socket path %s already exists!",
122 socket.GetFullPath() ) );
123 return;
124 }
125 }
126
127 m_server = std::make_unique<KINNG_REQUEST_SERVER>(
128 fmt::format( "ipc://{}", socket.GetFullPath().ToStdString() ) );
129 m_server->SetCallback( [&]( std::string* aRequest ) { onApiRequest( aRequest ); } );
130
131 m_logFilePath.AssignDir( PATHS::GetLogsPath() );
132 m_logFilePath.SetName( s_logFileName );
133
135 {
137 log( fmt::format( "--- KiCad API server started at {} ---\n", SocketPath() ) );
138 }
139
140 wxLogTrace( traceApi, wxString::Format( "Server: listening at %s", SocketPath() ) );
141
142 Bind( API_REQUEST_EVENT, &KICAD_API_SERVER::handleApiEvent, this );
143}
144
145
147{
148 if( !Running() )
149 return;
150
151 wxLogTrace( traceApi, "Stopping server" );
152 Unbind( API_REQUEST_EVENT, &KICAD_API_SERVER::handleApiEvent, this );
153
154 m_server->Stop();
155 m_server.reset( nullptr );
156}
157
158
160{
161 return m_server && m_server->Running();
162}
163
164
166{
167 wxCHECK( aHandler, /* void */ );
168 m_handlers.insert( aHandler );
169}
170
171
173{
174 m_handlers.erase( aHandler );
175}
176
177
179{
180 return m_server ? m_server->SocketPath() : "";
181}
182
183
184void KICAD_API_SERVER::onApiRequest( std::string* aRequest )
185{
186 if( !m_readyToReply )
187 {
188 ApiResponse notHandled;
189 notHandled.mutable_status()->set_status( ApiStatusCode::AS_NOT_READY );
190 notHandled.mutable_status()->set_error_message( "KiCad is not ready to reply" );
191 m_server->Reply( notHandled.SerializeAsString() );
192 log( "Got incoming request but was not yet ready to reply." );
193 return;
194 }
195
196 wxCommandEvent* evt = new wxCommandEvent( API_REQUEST_EVENT );
197
198 // We don't actually need write access to this string, but client data is non-const
199 evt->SetClientData( static_cast<void*>( aRequest ) );
200
201 // Takes ownership and frees the wxCommandEvent
202 QueueEvent( evt );
203}
204
205
206void KICAD_API_SERVER::handleApiEvent( wxCommandEvent& aEvent )
207{
208 std::string& requestString = *static_cast<std::string*>( aEvent.GetClientData() );
209 ApiRequest request;
210
211 if( !request.ParseFromString( requestString ) )
212 {
213 ApiResponse error;
214 error.mutable_header()->set_kicad_token( m_token );
215 error.mutable_status()->set_status( ApiStatusCode::AS_BAD_REQUEST );
216 error.mutable_status()->set_error_message( "request could not be parsed" );
217 m_server->Reply( error.SerializeAsString() );
218
220 log( "Response (ERROR): " + error.Utf8DebugString() );
221 }
222
224 log( "Request: " + request.Utf8DebugString() );
225
226 if( !request.header().kicad_token().empty() &&
227 request.header().kicad_token().compare( m_token ) != 0 )
228 {
229 ApiResponse error;
230 error.mutable_header()->set_kicad_token( m_token );
231 error.mutable_status()->set_status( ApiStatusCode::AS_TOKEN_MISMATCH );
232 error.mutable_status()->set_error_message(
233 "the provided kicad_token did not match this KiCad instance's token" );
234 m_server->Reply( error.SerializeAsString() );
235
237 log( "Response (ERROR): " + error.Utf8DebugString() );
238
239 return;
240 }
241
242 API_RESULT result;
243
244 for( API_HANDLER* handler : m_handlers )
245 {
246 result = handler->Handle( request );
247
248 if( result.has_value() )
249 break;
250 else if( result.error().status() != ApiStatusCode::AS_UNHANDLED )
251 break;
252 }
253
254 // Note: at the point we call Reply(), we no longer own requestString.
255
256 if( result.has_value() )
257 {
258 result->mutable_header()->set_kicad_token( m_token );
259 m_server->Reply( result->SerializeAsString() );
260
262 log( "Response: " + result->Utf8DebugString() );
263 }
264 else
265 {
266 ApiResponse error;
267 error.mutable_status()->CopyFrom( result.error() );
268 error.mutable_header()->set_kicad_token( m_token );
269
270 if( result.error().status() == ApiStatusCode::AS_UNHANDLED )
271 {
272 std::string type = "<unparseable Any>";
273 google::protobuf::Any::ParseAnyTypeUrl( request.message().type_url(), &type );
274 std::string msg = fmt::format( "no handler available for request of type {}", type );
275 error.mutable_status()->set_error_message( msg );
276 }
277
278 m_server->Reply( error.SerializeAsString() );
279
280 if( ADVANCED_CFG::GetCfg().m_EnableAPILogging )
281 log( "Response (ERROR): " + error.Utf8DebugString() );
282 }
283}
284
285
286void KICAD_API_SERVER::log( const std::string& aOutput )
287{
288 FILE* fp = wxFopen( m_logFilePath.GetFullPath(), wxT( "a" ) );
289
290 if( !fp )
291 return;
292
293 wxString out;
294 wxDateTime now = wxDateTime::Now();
295
296 fprintf( fp, "%s", TO_UTF8( out.Format( wxS( "%s: %s" ),
297 now.FormatISOCombined(), aOutput ) ) );
298 fclose( fp );
299}
tl::expected< ApiResponse, ApiResponseStatus > API_RESULT
Definition: api_handler.h:42
wxDEFINE_EVENT(API_REQUEST_EVENT, wxCommandEvent)
static const ADVANCED_CFG & GetCfg()
Get the singleton instance's config, which is shared by all consumers.
std::string m_token
Definition: api_server.h:97
bool Running() const
Definition: api_server.cpp:159
void handleApiEvent(wxCommandEvent &aEvent)
Event handler that receives the event on the main thread sent by onApiRequest.
Definition: api_server.cpp:206
wxFileName m_logFilePath
Definition: api_server.h:103
static wxString s_logFileName
Definition: api_server.h:101
void RegisterHandler(API_HANDLER *aHandler)
Adds a new request handler to the server.
Definition: api_server.cpp:165
void onApiRequest(std::string *aRequest)
Callback that executes on the server thread and generates an event that will be handled by the wxWidg...
Definition: api_server.cpp:184
std::set< API_HANDLER * > m_handlers
Definition: api_server.h:95
std::string SocketPath() const
Definition: api_server.cpp:178
void log(const std::string &aOutput)
Definition: api_server.cpp:286
void DeregisterHandler(API_HANDLER *aHandler)
Definition: api_server.cpp:172
std::unique_ptr< KINNG_REQUEST_SERVER > m_server
Definition: api_server.h:93
Definition: kiid.h:49
static wxString GetLogsPath()
Gets a path to use for user-visible log files.
Definition: paths.cpp:456
static bool EnsurePathExists(const wxString &aPath, bool aPathToFile=false)
Attempts to create a given path if it does not exist.
Definition: paths.cpp:467
bool m_EnableAPILogging
Log IPC API requests and responses.
const wxChar *const traceApi
Flag to enable debug output related to the IPC API and its plugin system.
Definition: api_utils.cpp:26
PGM_BASE & Pgm()
The global program "get" accessor.
Definition: pgm_base.cpp:1073
see class PGM_BASE
#define TO_UTF8(wxstring)
Convert a wxString to a UTF8 encoded C string for all wxWidgets build modes.
Definition: string_utils.h:403