KiCad PCB EDA Suite
Loading...
Searching...
No Matches
command_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 The KiCad Developers, see AUTHORS.txt for contributors.
5 * @author Jon Evans <[email protected]>
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 <csignal>
22#include <atomic>
23
25#include <api/api_server.h>
26#include <cli/exit_codes.h>
27#include <lib_id.h>
30#include <wx/app.h>
31#include <wx/crt.h>
32#include <wx/filename.h>
33
34#include "command_api_server.h"
35
36#define ARG_PATH "path"
37#define ARG_SOCKET "--socket"
38
39
40std::atomic_bool g_apiServerExitRequested{ false };
41
43{
44 g_apiServerExitRequested.store( true );
45}
46
47
49 COMMAND( "api-server" )
50{
51 m_argParser.add_description( UTF8STDSTR( _( "Run the KiCad IPC API server in headless mode" ) ) );
52
53 m_argParser.add_argument( ARG_PATH )
54 .default_value( std::string() )
55 .nargs( argparse::nargs_pattern::optional )
56 .help( UTF8STDSTR( _( "Optional path to a .kicad_pro, .kicad_pcb, or .kicad_sch file to pre-load" ) ) )
57 .metavar( "PROJECT_OR_FILE" );
58
59 m_argParser.add_argument( ARG_SOCKET )
60 .default_value( std::string() )
61 .help( UTF8STDSTR( _( "Override API socket path" ) ) )
62 .metavar( "SOCKET_PATH" );
63}
64
65
67{
68 using namespace kiapi::common;
69
70 std::unique_ptr<KICAD_API_SERVER> server = std::make_unique<KICAD_API_SERVER>( false );
71 API_HANDLER_COMMON commonHandler;
72
73 wxString socketPath = wxString::FromUTF8( m_argParser.get<std::string>( ARG_SOCKET ) );
74
75 if( !socketPath.IsEmpty() )
76 server->SetSocketPath( socketPath );
77
78 types::DocumentType openDocumentType = types::DOCTYPE_UNKNOWN;
80 wxFileName openProjectPath;
81
82 auto faceForDocument = []( types::DocumentType aType ) -> KIWAY::FACE_T
83 {
84 switch( aType )
85 {
86 case types::DOCTYPE_SCHEMATIC: return KIWAY::FACE_SCH;
87 case types::DOCTYPE_PCB: return KIWAY::FACE_PCB;
88 case types::DOCTYPE_FOOTPRINT: return KIWAY::FACE_PCB;
89 default: return KIWAY::KIWAY_FACE_COUNT;
90 }
91 };
92
93 auto docFileExtension = []( types::DocumentType aType ) -> std::string
94 {
95 switch( aType )
96 {
97 case types::DOCTYPE_SCHEMATIC: return FILEEXT::KiCadSchematicFileExtension;
98 case types::DOCTYPE_PCB: return FILEEXT::KiCadPcbFileExtension;
99 default: return "";
100 }
101 };
102
103 auto closeCurrentDocument = [&]()
104 {
105 if( openDocumentType != types::DOCTYPE_UNKNOWN )
106 {
107 wxString docFileName;
108
109 if( openDocumentType == types::DOCTYPE_PCB || openDocumentType == types::DOCTYPE_SCHEMATIC )
110 {
111 wxFileName docPath( openProjectPath );
112 docPath.SetExt( docFileExtension( openDocumentType ) );
113 docFileName = docPath.GetFullName();
114 }
115
116 wxString error;
117 aKiway.ProcessApiCloseDocument( openFace, docFileName, server.get(), &error );
118 }
119
120 openProjectPath.Clear();
121 openDocumentType = types::DOCTYPE_UNKNOWN;
122 };
123
124 auto openDocument = [&]( const commands::OpenDocument& aRequest )
126 {
127 types::DocumentType requestType = aRequest.type();
128
129 if( requestType != types::DOCTYPE_PCB && requestType != types::DOCTYPE_SCHEMATIC
130 && requestType != types::DOCTYPE_PROJECT && requestType != types::DOCTYPE_FOOTPRINT )
131 {
132 ApiResponseStatus e;
133 e.set_status( ApiStatusCode::AS_UNIMPLEMENTED );
134 e.set_error_message( "Only PCB, schematic, footprint, and project document types are supported" );
135 return tl::unexpected( e );
136 }
137
138 wxString inputPath = wxString::FromUTF8( aRequest.path() );
139
140 if( inputPath.IsEmpty() )
141 {
142 ApiResponseStatus e;
143 e.set_status( ApiStatusCode::AS_BAD_REQUEST );
144 e.set_error_message( "OpenDocument requires a non-empty path" );
145 return tl::unexpected( e );
146 }
147
148 closeCurrentDocument();
149
150 KIWAY::FACE_T face = faceForDocument( requestType );
151 wxString error;
152
153 if( face == KIWAY::KIWAY_FACE_COUNT )
154 {
155 ApiResponseStatus e;
156 e.set_status( ApiStatusCode::AS_BAD_REQUEST );
157 e.set_error_message( "unsupported document type" );
158 return tl::unexpected( e );
159 }
160
161 wxFileName projectPath;
162 wxString openPath;
163
164 projectPath = wxFileName( inputPath );
165
166 // TODO(JE) if the API client just gives a project path rather than sch/board,
167 // we won't dispatch correctly. We could instead try both handlers until one
168 // succeeds, like we do with other API calls.
169
170 projectPath.SetExt( FILEEXT::ProjectFileExtension );
171 projectPath.MakeAbsolute();
172 openPath = projectPath.GetFullPath();
173
174 if( !aKiway.ProcessApiOpenDocument( face, openPath, server.get(), &error ) )
175 {
176 ApiResponseStatus e;
177 e.set_status( ApiStatusCode::AS_BAD_REQUEST );
178 e.set_error_message( error.ToStdString() );
179 return tl::unexpected( e );
180 }
181
182 openProjectPath = projectPath;
183 openDocumentType = requestType;
184 openFace = face;
185
186 commands::OpenDocumentResponse response;
187 types::DocumentSpecifier* doc = response.mutable_document();
189
190 doc->set_type( openDocumentType );
191
192 if( openDocumentType == types::DOCTYPE_PCB )
193 {
194 wxFileName boardPath( openProjectPath );
195 boardPath.SetExt( FILEEXT::KiCadPcbFileExtension );
196 doc->set_board_filename( boardPath.GetFullName().ToStdString() );
197 }
198 else if( openDocumentType == types::DOCTYPE_SCHEMATIC )
199 {
200 // TODO(JE) stateful sheet path handling?
201 }
202
203 doc->mutable_project()->set_name( project.GetProjectName().ToUTF8() );
204 doc->mutable_project()->set_path( project.GetProjectPath().ToUTF8() );
205
206 return response;
207 };
208
209 auto closeDocument =
210 [&]( const commands::CloseDocument& aRequest ) -> HANDLER_RESULT<google::protobuf::Empty>
211 {
212 if( openDocumentType == types::DOCTYPE_UNKNOWN )
213 {
214 ApiResponseStatus e;
215 e.set_status( ApiStatusCode::AS_BAD_REQUEST );
216 e.set_error_message( "No document is currently open" );
217 return tl::unexpected( e );
218 }
219
220 if( aRequest.has_document() )
221 {
222 if( aRequest.document().type() != openDocumentType )
223 {
224 ApiResponseStatus e;
225 e.set_status( ApiStatusCode::AS_BAD_REQUEST );
226 e.set_error_message( "Requested document type does not match the open document" );
227 return tl::unexpected( e );
228 }
229
230 wxFileName expectedPath( openProjectPath );
231 expectedPath.SetExt( docFileExtension( openDocumentType ) );
232
233 wxString requestedName;
234
235 if( openDocumentType == types::DOCTYPE_PCB
236 && !aRequest.document().board_filename().empty() )
237 {
238 requestedName = wxString::FromUTF8( aRequest.document().board_filename() );
239 }
240 else if( openDocumentType == types::DOCTYPE_SCHEMATIC
241 && !aRequest.document().project().path().empty() )
242 {
243 requestedName = wxString::FromUTF8( aRequest.document().project().name() )
245 }
246
247 if( !requestedName.IsEmpty() && expectedPath.GetFullName() != requestedName )
248 {
249 ApiResponseStatus e;
250 e.set_status( ApiStatusCode::AS_BAD_REQUEST );
251 e.set_error_message( "Requested document does not match the open document" );
252 return tl::unexpected( e );
253 }
254 }
255
256 wxString docFileName;
257
258 if( openDocumentType == types::DOCTYPE_PCB || openDocumentType == types::DOCTYPE_SCHEMATIC )
259 {
260 wxFileName expectedPath( openProjectPath );
261 expectedPath.SetExt( docFileExtension( openDocumentType ) );
262 docFileName = expectedPath.GetFullName();
263 }
264
265 wxString error;
266
267 if( !aKiway.ProcessApiCloseDocument( openFace, docFileName, server.get(), &error ) )
268 {
269 ApiResponseStatus e;
270 e.set_status( ApiStatusCode::AS_BAD_REQUEST );
271 e.set_error_message( error.ToStdString() );
272 return tl::unexpected( e );
273 }
274
275 openProjectPath.Clear();
276 openDocumentType = types::DOCTYPE_UNKNOWN;
277
278 return google::protobuf::Empty();
279 };
280
281 commonHandler.SetOpenDocumentHandler( openDocument );
282 commonHandler.SetCloseDocumentHandler( closeDocument );
283
284 server->RegisterHandler( &commonHandler );
285 server->Start();
286
287 if( !server->Running() )
288 {
289 wxFprintf( stderr, _( "Failed to start API server\n" ) );
291 }
292
293 wxString preloadPath = wxString::FromUTF8( m_argParser.get<std::string>( ARG_PATH ) );
294
295 if( !preloadPath.IsEmpty() )
296 {
297 using namespace kiapi::common;
298
299 wxFileName preloadFile( preloadPath );
300 types::DocumentType preloadType = types::DOCTYPE_PROJECT;
301
302 if( preloadFile.GetExt() == FILEEXT::KiCadSchematicFileExtension )
303 preloadType = types::DOCTYPE_SCHEMATIC;
304 else if( preloadFile.GetExt() == FILEEXT::KiCadPcbFileExtension )
305 preloadType = types::DOCTYPE_PCB;
306
307 commands::OpenDocument request;
308 request.set_type( preloadType );
309 request.set_path( preloadPath.ToStdString() );
310
311 auto preloadResult = openDocument( request );
312
313 if( !preloadResult )
314 {
315 wxFprintf( stderr, "%s\n", preloadResult.error().error_message() );
316 server->DeregisterHandler( &commonHandler );
318 }
319 }
320
321 server->SetReadyToReply( true );
322
323 wxString listenPath = wxString::FromUTF8( server->SocketPath() );
324 wxFprintf( stdout, "KiCad API server listening at %s\n", listenPath );
325
326 auto oldSigInt = std::signal( SIGINT, apiServerSignalHandler );
327#ifdef SIGTERM
328 auto oldSigTerm = std::signal( SIGTERM, apiServerSignalHandler );
329#endif
330
331 g_apiServerExitRequested.store( false );
332
333 while( !g_apiServerExitRequested.load() )
334 {
335 wxTheApp->ProcessPendingEvents();
336 wxMilliSleep( 10 );
337 }
338
339 std::signal( SIGINT, oldSigInt );
340#ifdef SIGTERM
341 std::signal( SIGTERM, oldSigTerm );
342#endif
343
344 wxFprintf( stdout, "Shutting down\n" );
345
346 closeCurrentDocument();
347 server->DeregisterHandler( &commonHandler );
348
349 return EXIT_CODES::OK;
350}
tl::expected< T, ApiResponseStatus > HANDLER_RESULT
Definition api_handler.h:45
void SetOpenDocumentHandler(OPEN_DOCUMENT_HANDLER aHandler)
void SetCloseDocumentHandler(CLOSE_DOCUMENT_HANDLER aHandler)
int doPerform(KIWAY &aKiway) override
The internal handler that should be overloaded to implement command specific processing and work.
argparse::ArgumentParser m_argParser
Definition command.h:113
COMMAND(const std::string &aName)
Define a new COMMAND instance.
Definition command.cpp:30
A minimalistic software bus for communications between various DLLs/DSOs (DSOs) within the same KiCad...
Definition kiway.h:315
bool ProcessApiCloseDocument(KIWAY::FACE_T aFace, const wxString &aPath, KICAD_API_SERVER *aServer, wxString *aError=nullptr)
Definition kiway.cpp:783
bool ProcessApiOpenDocument(KIWAY::FACE_T aFace, const wxString &aPath, KICAD_API_SERVER *aServer, wxString *aError=nullptr)
Definition kiway.cpp:766
FACE_T
Known KIFACE implementations.
Definition kiway.h:321
@ KIWAY_FACE_COUNT
Definition kiway.h:330
@ FACE_SCH
eeschema DSO
Definition kiway.h:322
@ FACE_PCB
pcbnew DSO
Definition kiway.h:323
virtual SETTINGS_MANAGER & GetSettingsManager() const
Definition pgm_base.h:130
Container for project specific data.
Definition project.h:66
PROJECT & Prj() const
A helper while we are not MDI-capable – return the one and only project.
#define UTF8STDSTR(s)
Definition command.h:27
std::atomic_bool g_apiServerExitRequested
#define ARG_PATH
void apiServerSignalHandler(int)
#define ARG_SOCKET
#define _(s)
static const std::string ProjectFileExtension
static const std::string KiCadSchematicFileExtension
static const std::string KiCadPcbFileExtension
static const int ERR_ARGS
Definition exit_codes.h:31
static const int OK
Definition exit_codes.h:30
static const int ERR_UNKNOWN
Definition exit_codes.h:32
PGM_BASE & Pgm()
The global program "get" accessor.
Definition of file extensions used in Kicad.