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 wxFileName schPath( openProjectPath );
196 schPath.SetExt( FILEEXT::KiCadSchematicFileExtension );
197 doc->set_schematic_filename( schPath.GetFullName().ToStdString() );
198 }
199
200 doc->mutable_project()->set_name( project.GetProjectName().ToStdString() );
201 doc->mutable_project()->set_path( project.GetProjectDirectory().ToStdString() );
202
203 return response;
204 };
205
206 auto closeDocument =
207 [&]( const commands::CloseDocument& aRequest ) -> HANDLER_RESULT<google::protobuf::Empty>
208 {
209 if( openDocumentType == types::DOCTYPE_UNKNOWN )
210 {
211 ApiResponseStatus e;
212 e.set_status( ApiStatusCode::AS_BAD_REQUEST );
213 e.set_error_message( "No document is currently open" );
214 return tl::unexpected( e );
215 }
216
217 if( aRequest.has_document() )
218 {
219 if( aRequest.document().type() != openDocumentType )
220 {
221 ApiResponseStatus e;
222 e.set_status( ApiStatusCode::AS_BAD_REQUEST );
223 e.set_error_message( "Requested document type does not match the open document" );
224 return tl::unexpected( e );
225 }
226
227 wxFileName expectedPath( openProjectPath );
228 expectedPath.SetExt( docFileExtension( openDocumentType ) );
229
230 wxString requestedName;
231
232 if( openDocumentType == types::DOCTYPE_PCB
233 && !aRequest.document().board_filename().empty() )
234 {
235 requestedName = wxString::FromUTF8( aRequest.document().board_filename() );
236 }
237 else if( openDocumentType == types::DOCTYPE_SCHEMATIC
238 && !aRequest.document().schematic_filename().empty() )
239 {
240 requestedName = wxString::FromUTF8( aRequest.document().schematic_filename() );
241 }
242
243 if( !requestedName.IsEmpty() && expectedPath.GetFullName() != requestedName )
244 {
245 ApiResponseStatus e;
246 e.set_status( ApiStatusCode::AS_BAD_REQUEST );
247 e.set_error_message( "Requested document does not match the open document" );
248 return tl::unexpected( e );
249 }
250 }
251
252 wxString docFileName;
253
254 if( openDocumentType == types::DOCTYPE_PCB || openDocumentType == types::DOCTYPE_SCHEMATIC )
255 {
256 wxFileName expectedPath( openProjectPath );
257 expectedPath.SetExt( docFileExtension( openDocumentType ) );
258 docFileName = expectedPath.GetFullName();
259 }
260
261 wxString error;
262
263 if( !aKiway.ProcessApiCloseDocument( openFace, docFileName, server.get(), &error ) )
264 {
265 ApiResponseStatus e;
266 e.set_status( ApiStatusCode::AS_BAD_REQUEST );
267 e.set_error_message( error.ToStdString() );
268 return tl::unexpected( e );
269 }
270
271 openProjectPath.Clear();
272 openDocumentType = types::DOCTYPE_UNKNOWN;
273
274 return google::protobuf::Empty();
275 };
276
277 commonHandler.SetOpenDocumentHandler( openDocument );
278 commonHandler.SetCloseDocumentHandler( closeDocument );
279
280 server->RegisterHandler( &commonHandler );
281 server->Start();
282
283 if( !server->Running() )
284 {
285 wxFprintf( stderr, _( "Failed to start API server\n" ) );
287 }
288
289 wxString preloadPath = wxString::FromUTF8( m_argParser.get<std::string>( ARG_PATH ) );
290
291 if( !preloadPath.IsEmpty() )
292 {
293 using namespace kiapi::common;
294
295 wxFileName preloadFile( preloadPath );
296 types::DocumentType preloadType = types::DOCTYPE_PROJECT;
297
298 if( preloadFile.GetExt() == FILEEXT::KiCadSchematicFileExtension )
299 preloadType = types::DOCTYPE_SCHEMATIC;
300 else if( preloadFile.GetExt() == FILEEXT::KiCadPcbFileExtension )
301 preloadType = types::DOCTYPE_PCB;
302
303 commands::OpenDocument request;
304 request.set_type( preloadType );
305 request.set_path( preloadPath.ToStdString() );
306
307 auto preloadResult = openDocument( request );
308
309 if( !preloadResult )
310 {
311 wxFprintf( stderr, "%s\n", preloadResult.error().error_message() );
312 server->DeregisterHandler( &commonHandler );
314 }
315 }
316
317 server->SetReadyToReply( true );
318
319 wxString listenPath = wxString::FromUTF8( server->SocketPath() );
320 wxFprintf( stdout, "KiCad API server listening at %s\n", listenPath );
321
322 auto oldSigInt = std::signal( SIGINT, apiServerSignalHandler );
323#ifdef SIGTERM
324 auto oldSigTerm = std::signal( SIGTERM, apiServerSignalHandler );
325#endif
326
327 g_apiServerExitRequested.store( false );
328
329 while( !g_apiServerExitRequested.load() )
330 {
331 wxTheApp->ProcessPendingEvents();
332 wxMilliSleep( 10 );
333 }
334
335 std::signal( SIGINT, oldSigInt );
336#ifdef SIGTERM
337 std::signal( SIGTERM, oldSigTerm );
338#endif
339
340 wxFprintf( stdout, "Shutting down\n" );
341
342 closeCurrentDocument();
343 server->DeregisterHandler( &commonHandler );
344
345 return EXIT_CODES::OK;
346}
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:781
bool ProcessApiOpenDocument(KIWAY::FACE_T aFace, const wxString &aPath, KICAD_API_SERVER *aServer, wxString *aError=nullptr)
Definition kiway.cpp:764
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.