KiCad PCB EDA Suite
Loading...
Searching...
No Matches
api_e2e_utils.h
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 *
6 * This program is free software: you can redistribute it and/or modify it
7 * under the terms of the GNU General Public License as published by the
8 * Free Software Foundation, either version 3 of the License, or (at your
9 * option) any later version.
10 *
11 * This program is distributed in the hope that it will be useful, but
12 * WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14 * General Public License for more details.
15 *
16 * You should have received a copy of the GNU General Public License along
17 * with this program. If not, see <http://www.gnu.org/licenses/>.
18 */
19
20#ifndef QA_API_E2E_UTILS_H
21#define QA_API_E2E_UTILS_H
22
23#include <chrono>
24#include <functional>
25#include <string>
26#include <vector>
27
28#include <google/protobuf/any.pb.h>
29#include <magic_enum.hpp>
30#include <wx/filename.h>
31#include <wx/process.h>
32#include <wx/stream.h>
33#include <wx/utils.h>
34
35#include <nng/nng.h>
36#include <nng/protocol/reqrep0/req.h>
37
38#include <import_export.h>
39#include <api/api_utils.h>
40#include <api/common/envelope.pb.h>
41#include <api/common/commands/base_commands.pb.h>
42#include <api/common/commands/editor_commands.pb.h>
43#include <api/common/commands/project_commands.pb.h>
44#include <api/common/types/base_types.pb.h>
45#include <api/common/types/enums.pb.h>
46#include <api/common/types/jobs.pb.h>
47#include <footprint.h>
50#include <wx/log.h>
51
52
53#ifndef QA_KICAD_CLI_PATH
54#define QA_KICAD_CLI_PATH "kicad-cli"
55#endif
56
57
58class API_SERVER_PROCESS : public wxProcess
59{
60public:
61 API_SERVER_PROCESS( std::function<void( int, const wxString&, const wxString& )> aCallback ) :
62 wxProcess(),
63 m_callback( std::move( aCallback ) )
64 {
65 }
66
67 void OnTerminate( int aPid, int aStatus ) override
68 {
69 wxString output;
70 wxString error;
71
72 drainStream( GetInputStream(), output );
73 drainStream( GetErrorStream(), error );
74
75 if( m_callback )
76 m_callback( aStatus, output, error );
77 }
78
79 static void drainStream( wxInputStream* aStream, wxString& aDest )
80 {
81 if( !aStream || !aStream->CanRead() )
82 return;
83
84 constexpr size_t chunkSize = 1024;
85 char buffer[chunkSize + 1] = { 0 };
86
87 while( aStream->CanRead() )
88 {
89 aStream->Read( buffer, chunkSize );
90 size_t bytesRead = aStream->LastRead();
91
92 if( bytesRead == 0 )
93 break;
94
95 buffer[bytesRead] = '\0';
96 aDest += wxString::FromUTF8( buffer );
97 }
98 }
99
100private:
101 std::function<void( int, const wxString&, const wxString& )> m_callback;
102};
103
104
106{
107public:
109 {
110 int ret = nng_req0_open( &m_socket );
111
112 if( ret == 0 )
113 {
114 m_isOpen = true;
115 nng_socket_set_ms( m_socket, NNG_OPT_RECVTIMEO, 10000 );
116 nng_socket_set_ms( m_socket, NNG_OPT_SENDTIMEO, 10000 );
117 }
118 else
119 {
121 wxString::Format( wxS( "nng_req0_open failed: %s" ), wxString::FromUTF8( nng_strerror( ret ) ) );
122 }
123 }
124
126 {
127 if( m_isOpen )
128 nng_close( m_socket );
129 }
130
131 bool Connect( const wxString& aSocketUrl )
132 {
133 if( !m_isOpen )
134 return false;
135
136 if( m_isConnected )
137 return true;
138
139 int ret = nng_dial( m_socket, aSocketUrl.ToStdString().c_str(), nullptr, 0 );
140
141 if( ret != 0 )
142 {
143 m_lastError = wxString::Format( wxS( "nng_dial failed: %s" ), wxString::FromUTF8( nng_strerror( ret ) ) );
144 return false;
145 }
146
147 m_isConnected = true;
148 return true;
149 }
150
151 const wxString& LastError() const { return m_lastError; }
152
153 bool Ping( kiapi::common::ApiStatusCode* aStatusOut = nullptr )
154 {
155 kiapi::common::commands::Ping ping;
156 kiapi::common::ApiResponse response;
157
158 if( !sendCommand( ping, &response ) )
159 return false;
160
161 if( aStatusOut )
162 *aStatusOut = response.status().status();
163
164 return true;
165 }
166
168 {
169 kiapi::common::commands::GetVersion request;
170 kiapi::common::ApiResponse response;
171
172 if( !sendCommand( request, &response ) )
173 return false;
174
175 if( response.status().status() != kiapi::common::AS_OK )
176 {
177 m_lastError = response.status().error_message();
178 return false;
179 }
180
181 kiapi::common::commands::GetVersionResponse version;
182
183 if( !response.message().UnpackTo( &version ) )
184 {
185 m_lastError = wxS( "Failed to unpack GetVersionResponse" );
186 return false;
187 }
188
189 return true;
190 }
191
192 bool OpenDocument( const wxString& aPath, kiapi::common::types::DocumentType aType,
193 kiapi::common::types::DocumentSpecifier* aDocument = nullptr )
194 {
195 kiapi::common::commands::OpenDocument request;
196 request.set_type( aType );
197 request.set_path( aPath.ToStdString() );
198
199 kiapi::common::ApiResponse response;
200
201 if( !sendCommand( request, &response ) )
202 return false;
203
204 if( response.status().status() != kiapi::common::AS_OK )
205 {
206 m_lastError = response.status().error_message();
207 return false;
208 }
209
210 kiapi::common::commands::OpenDocumentResponse openResponse;
211
212 if( !response.message().UnpackTo( &openResponse ) )
213 {
214 m_lastError = wxS( "Failed to unpack OpenDocumentResponse" );
215 return false;
216 }
217
218 if( aDocument )
219 *aDocument = openResponse.document();
220
221 return true;
222 }
223
224 // Convenience overload for PCB documents (backward compatible)
225 bool OpenDocument( const wxString& aPath, kiapi::common::types::DocumentSpecifier* aDocument = nullptr )
226 {
227 return OpenDocument( aPath, kiapi::common::types::DOCTYPE_PCB, aDocument );
228 }
229
230 bool GetItemsCount( const kiapi::common::types::DocumentSpecifier& aDocument,
231 kiapi::common::types::KiCadObjectType aType, int* aCount )
232 {
233 kiapi::common::commands::GetItems request;
234 *request.mutable_header()->mutable_document() = aDocument;
235 request.add_types( aType );
236
237 kiapi::common::ApiResponse response;
238
239 if( !sendCommand( request, &response ) )
240 return false;
241
242 if( response.status().status() != kiapi::common::AS_OK )
243 {
244 m_lastError = response.status().error_message();
245 return false;
246 }
247
248 kiapi::common::commands::GetItemsResponse itemsResponse;
249
250 if( !response.message().UnpackTo( &itemsResponse ) )
251 {
252 m_lastError = wxS( "Failed to unpack GetItemsResponse" );
253 return false;
254 }
255
256 if( itemsResponse.status() != kiapi::common::types::IRS_OK )
257 {
258 m_lastError = wxString::Format( wxS( "GetItems returned non-OK status: %d" ),
259 static_cast<int>( itemsResponse.status() ) );
260 return false;
261 }
262
263 *aCount = itemsResponse.items_size();
264 return true;
265 }
266
267 bool GetFirstFootprint( const kiapi::common::types::DocumentSpecifier& aDocument, FOOTPRINT* aFootprint )
268 {
269 wxCHECK( aFootprint, false );
270
271 kiapi::common::commands::GetItems request;
272 *request.mutable_header()->mutable_document() = aDocument;
273 request.add_types( kiapi::common::types::KOT_PCB_FOOTPRINT );
274
275 kiapi::common::ApiResponse response;
276
277 if( !sendCommand( request, &response ) )
278 {
279 m_lastError = wxS( "Failed to send command" );
280 return false;
281 }
282
283 if( response.status().status() != kiapi::common::AS_OK )
284 {
285 m_lastError = response.status().error_message();
286 return false;
287 }
288
289 kiapi::common::commands::GetItemsResponse itemsResponse;
290
291 if( !response.message().UnpackTo( &itemsResponse ) )
292 {
293 m_lastError = wxS( "Failed to unpack GetItemsResponse" );
294 return false;
295 }
296
297 if( itemsResponse.status() != kiapi::common::types::IRS_OK )
298 {
299 m_lastError = wxString::Format( wxS( "GetItems returned non-OK status: %s" ),
300 magic_enum::enum_name( itemsResponse.status() ) );
301 return false;
302 }
303
304 if( itemsResponse.items_size() == 0 )
305 {
306 m_lastError = wxS( "GetItems returned no footprints" );
307 return false;
308 }
309
310 if( !aFootprint->Deserialize( itemsResponse.items( 0 ) ) )
311 {
312 m_lastError = wxS( "Failed to deserialize first footprint from GetItems response" );
313 return false;
314 }
315
316 return true;
317 }
318
319 bool CloseDocument( const kiapi::common::types::DocumentSpecifier* aDocument,
320 kiapi::common::ApiStatusCode* aStatus = nullptr )
321 {
322 kiapi::common::commands::CloseDocument request;
323
324 if( aDocument )
325 *request.mutable_document() = *aDocument;
326
327 kiapi::common::ApiResponse response;
328
329 if( !sendCommand( request, &response ) )
330 {
331 m_lastError = wxS( "Failed to send command" );
332 return false;
333 }
334
335 if( aStatus )
336 *aStatus = response.status().status();
337
338 if( response.status().status() != kiapi::common::AS_OK )
339 m_lastError = response.status().error_message();
340
341 return true;
342 }
343
347 template <typename T>
348 bool RunJob( const T& aJobRequest, kiapi::common::types::RunJobResponse* aResponse )
349 {
350 kiapi::common::ApiResponse apiResponse;
351
352 if( !sendCommand( aJobRequest, &apiResponse ) )
353 return false;
354
355 if( apiResponse.status().status() != kiapi::common::AS_OK )
356 {
357 m_lastError = apiResponse.status().error_message();
358 return false;
359 }
360
361 if( !apiResponse.message().UnpackTo( aResponse ) )
362 {
363 m_lastError = wxS( "Failed to unpack RunJobResponse" );
364 return false;
365 }
366
367 return true;
368 }
369
370 template <typename T>
371 bool SendCommand( const T& aMessage, kiapi::common::ApiResponse* aResponse )
372 {
373 return sendCommand( aMessage, aResponse );
374 }
375
376private:
377 template <typename T>
378 bool sendCommand( const T& aMessage, kiapi::common::ApiResponse* aResponse )
379 {
380 if( !m_isConnected )
381 {
382 m_lastError = wxS( "API client is not connected" );
383 return false;
384 }
385
386 kiapi::common::ApiRequest request;
387 request.mutable_header()->set_client_name( "kicad.qa" );
388
389 if( !request.mutable_message()->PackFrom( aMessage ) )
390 {
391 m_lastError = wxS( "Failed to pack command into ApiRequest" );
392 return false;
393 }
394
395 std::string requestStr = request.SerializeAsString();
396 void* sendBuf = nng_alloc( requestStr.size() );
397
398 if( !sendBuf )
399 {
400 m_lastError = wxS( "nng_alloc failed" );
401 return false;
402 }
403
404 memcpy( sendBuf, requestStr.data(), requestStr.size() );
405
406 int ret = nng_send( m_socket, sendBuf, requestStr.size(), NNG_FLAG_ALLOC );
407
408 if( ret != 0 )
409 {
410 m_lastError = wxString::Format( wxS( "nng_send failed: %s" ), wxString::FromUTF8( nng_strerror( ret ) ) );
411 return false;
412 }
413
414 char* reply = nullptr;
415 size_t replySize = 0;
416
417 ret = nng_recv( m_socket, &reply, &replySize, NNG_FLAG_ALLOC );
418
419 if( ret != 0 )
420 {
421 m_lastError = wxString::Format( wxS( "nng_recv failed: %s" ), wxString::FromUTF8( nng_strerror( ret ) ) );
422 return false;
423 }
424
425 std::string responseStr( reply, replySize );
426 nng_free( reply, replySize );
427
428 if( !aResponse->ParseFromString( responseStr ) )
429 {
430 m_lastError = wxS( "Failed to parse reply from KiCad" );
431 return false;
432 }
433
434 return true;
435 }
436
437 nng_socket m_socket;
438 bool m_isOpen = false;
439 bool m_isConnected = false;
440 wxString m_lastError;
441};
442
443
445{
446public:
448 {
449 m_cliPath = wxString::FromUTF8( QA_KICAD_CLI_PATH );
450
451#ifdef __UNIX__
452 m_socketPath = wxString::Format( wxS( "/tmp/kicad-api-e2e-%ld.sock" ), wxGetProcessId() );
453#else
454 wxString tempPath = wxFileName::CreateTempFileName( wxS( "kicad-api-e2e" ) );
455
456 if( wxFileExists( tempPath ) )
457 wxRemoveFile( tempPath );
458
459 m_socketPath = tempPath + wxS( ".sock" );
460#endif
461
462 if( wxFileName::Exists( m_socketPath ) )
463 wxRemoveFile( m_socketPath );
464 }
465
467 {
468 stopServer();
469
470 if( wxFileName::Exists( m_socketPath ) )
471 wxRemoveFile( m_socketPath );
472 }
473
474 bool Start( const wxString& aCliPathOverride = wxString() )
475 {
476 if( !startServerProcess( aCliPathOverride ) )
477 return false;
478
479 auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds( 15 );
480 wxString lastProbeError;
481
482 while( std::chrono::steady_clock::now() < deadline )
483 {
484 wxMilliSleep( 100 );
486
487 if( !isProcessAlive() )
488 {
489 m_lastError = wxS( "kicad-cli process exited before ready" );
490 return false;
491 }
492
493 wxString socketUrl = wxS( "ipc://" ) + m_socketPath;
494
495 if( !m_client.Connect( socketUrl ) )
496 {
497 wxLogTrace( traceApi, "kicad-cli connect attempt failed, will retry" );
498 continue;
499 }
500
501 kiapi::common::ApiStatusCode pingStatus = kiapi::common::AS_UNKNOWN;
502
503 if( m_client.Ping( &pingStatus ) )
504 {
505 if( pingStatus == kiapi::common::AS_OK )
506 return true;
507
508 if( pingStatus != kiapi::common::AS_NOT_READY )
509 {
510 m_lastError = wxString::Format( wxS( "Ping returned unexpected status: %s" ),
511 magic_enum::enum_name( pingStatus ) );
512 return false;
513 }
514
515 wxLogTrace( traceApi, "kicad-cli connected but returned AS_NOT_READY, will retry" );
516 }
517 else
518 {
519 wxLogTrace( traceApi, "kicad-cli ping attempt failed, will retry" );
520 }
521 }
522
523 m_lastError = wxS( "Timed out waiting for kicad-cli to start up" );
524 return false;
525 }
526
528
529 wxString CliPath() const { return m_cliPath; }
530
531 const wxString& LastError() const { return m_lastError; }
532
533private:
534 bool startServerProcess( const wxString& aCliPathOverride )
535 {
536 if( m_pid != 0 )
537 return true;
538
539 wxString cliPath = aCliPathOverride.IsEmpty() ? m_cliPath : aCliPathOverride;
540
541 if( !wxFileExists( cliPath ) )
542 {
543 m_lastError = wxS( "kicad-cli path does not exist: " ) + cliPath;
544 return false;
545 }
546
547 m_stdout.clear();
548 m_stderr.clear();
549 m_exitCode = 0;
550
552 [this]( int aStatus, const wxString& aOutput, const wxString& aError )
553 {
554 m_exitCode = aStatus;
555 m_stdout += aOutput;
556 m_stderr += aError;
557 m_pid = 0;
558 m_process = nullptr;
559 } );
560
561 m_process->Redirect();
562
563 std::vector<const wchar_t*> args = { cliPath.wc_str(), wxS( "api-server" ), wxS( "--socket" ),
564 m_socketPath.wc_str(), nullptr };
565
566 m_pid = wxExecute( args.data(), wxEXEC_ASYNC, m_process );
567
568 if( m_pid == 0 )
569 {
570 delete m_process;
571 m_process = nullptr;
572 m_lastError = wxS( "Failed to launch kicad-cli api-server" );
573 return false;
574 }
575
576 return true;
577 }
578
580 {
581 if( !m_process )
582 return;
583
586 }
587
589 {
591
592 if( isProcessAlive() )
593 {
594 wxProcess::Kill( m_pid, wxSIGTERM, wxKILL_CHILDREN );
595
596 auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds( 5 );
597
598 while( isProcessAlive() && std::chrono::steady_clock::now() < deadline )
599 wxMilliSleep( 50 );
600
601 if( isProcessAlive() )
602 wxProcess::Kill( m_pid, wxSIGKILL, wxKILL_CHILDREN );
603 }
604
606 m_pid = 0;
607 m_process = nullptr;
608 }
609
610 bool isProcessAlive() const
611 {
612 if( m_pid == 0 )
613 return false;
614
615 return wxProcess::Exists( m_pid );
616 }
617
618private:
621 long m_pid = 0;
622 wxString m_cliPath;
623 wxString m_socketPath;
624 wxString m_stdout;
625 wxString m_stderr;
626 wxString m_lastError;
627 mutable int m_exitCode = 0;
628};
629
630#endif // QA_API_E2E_UTILS_H
#define QA_KICAD_CLI_PATH
General utilities for PCB file IO for QA programs.
bool Start(const wxString &aCliPathOverride=wxString())
bool isProcessAlive() const
API_TEST_CLIENT m_client
const wxString & LastError() const
API_TEST_CLIENT & Client()
bool startServerProcess(const wxString &aCliPathOverride)
API_SERVER_PROCESS * m_process
wxString CliPath() const
void OnTerminate(int aPid, int aStatus) override
API_SERVER_PROCESS(std::function< void(int, const wxString &, const wxString &)> aCallback)
std::function< void(int, const wxString &, const wxString &)> m_callback
static void drainStream(wxInputStream *aStream, wxString &aDest)
bool GetItemsCount(const kiapi::common::types::DocumentSpecifier &aDocument, kiapi::common::types::KiCadObjectType aType, int *aCount)
bool OpenDocument(const wxString &aPath, kiapi::common::types::DocumentType aType, kiapi::common::types::DocumentSpecifier *aDocument=nullptr)
bool RunJob(const T &aJobRequest, kiapi::common::types::RunJobResponse *aResponse)
Send an arbitrary job request and receive a RunJobResponse.
nng_socket m_socket
bool sendCommand(const T &aMessage, kiapi::common::ApiResponse *aResponse)
bool CloseDocument(const kiapi::common::types::DocumentSpecifier *aDocument, kiapi::common::ApiStatusCode *aStatus=nullptr)
bool Connect(const wxString &aSocketUrl)
const wxString & LastError() const
bool OpenDocument(const wxString &aPath, kiapi::common::types::DocumentSpecifier *aDocument=nullptr)
bool SendCommand(const T &aMessage, kiapi::common::ApiResponse *aResponse)
bool Ping(kiapi::common::ApiStatusCode *aStatusOut=nullptr)
bool GetFirstFootprint(const kiapi::common::types::DocumentSpecifier &aDocument, FOOTPRINT *aFootprint)
bool Deserialize(const google::protobuf::Any &aContainer) override
Deserializes the given protobuf message into this object.
const wxChar *const traceApi
Flag to enable debug output related to the IPC API and its plugin system.
Definition api_utils.cpp:27
STL namespace.
nlohmann::json output