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