75 std::unique_ptr<KICAD_API_SERVER> server = std::make_unique<KICAD_API_SERVER>(
false );
81 designBlockLibrariesHandler.
SetKiway( &aKiway );
83 [&server](
KIFACE* aKiface )
90 if( !socketPath.IsEmpty() )
91 server->SetSocketPath( socketPath );
97 std::optional<wxFileName> openProjectPath;
101 types::DocumentType type;
110 if( type == types::DOCTYPE_FOOTPRINT || type == types::DOCTYPE_SYMBOL )
118 spec.
path = specPath.IsEmpty() ? fileName : specPath;
125 std::vector<OPEN_DOCUMENT> openDocuments;
127 auto faceForDocument = []( types::DocumentType aType ) ->
KIWAY::FACE_T
139 auto closeAllDocuments =
144 for(
const OPEN_DOCUMENT& doc : openDocuments )
147 if( doc.type == types::DOCTYPE_PROJECT )
154 if( !errors.IsEmpty() )
155 errors.Append( wxS(
"; " ) );
157 errors.Append( wxString::Format( wxS(
"Failed to close %s: %s" ), doc.fileName, error ) );
161 openDocuments.clear();
163 if( openProjectPath )
169 openProjectPath.reset();
171 if( !errors.IsEmpty() )
174 e.set_status( ApiStatusCode::AS_INTERNAL_ERROR );
175 e.set_error_message( errors.ToStdString() );
176 return tl::unexpected( e );
179 return google::protobuf::Empty();
182 auto openDocument = [&](
const commands::OpenDocument& aRequest )
185 types::DocumentType requestType = aRequest.type();
187 if( requestType != types::DOCTYPE_PCB && requestType != types::DOCTYPE_SCHEMATIC
188 && requestType != types::DOCTYPE_PROJECT && requestType != types::DOCTYPE_FOOTPRINT
189 && requestType != types::DOCTYPE_SYMBOL )
192 e.set_status( ApiStatusCode::AS_UNIMPLEMENTED );
193 e.set_error_message(
"Only PCB, schematic, footprint, symbol, and project document types are supported" );
194 return tl::unexpected( e );
197 wxString inputPath = wxString::FromUTF8( aRequest.path() );
199 if( inputPath.IsEmpty() )
202 e.set_status( ApiStatusCode::AS_BAD_REQUEST );
203 e.set_error_message(
"OpenDocument requires a non-empty path" );
204 return tl::unexpected( e );
207 if( requestType == types::DOCTYPE_FOOTPRINT )
211 if( fpid.
Parse( inputPath ) >= 0 )
214 e.set_status( ApiStatusCode::AS_BAD_REQUEST );
215 e.set_error_message( wxString::Format( wxS(
"Invalid footprint LIB_ID: %s" ),
217 return tl::unexpected( e );
224 if( openProjectPath )
225 spec.
path = openProjectPath->GetFullPath();
232 e.set_status( ApiStatusCode::AS_BAD_REQUEST );
233 e.set_error_message( error.ToStdString() );
234 return tl::unexpected( e );
238 doc.type = requestType;
240 openDocuments.push_back( doc );
242 commands::OpenDocumentResponse response;
243 types::DocumentSpecifier* docSpec = response.mutable_document();
244 docSpec->set_type( requestType );
250 if( requestType == types::DOCTYPE_SYMBOL )
254 if( libId.
Parse( inputPath ) >= 0 )
257 e.set_status( ApiStatusCode::AS_BAD_REQUEST );
258 e.set_error_message( wxString::Format( wxS(
"Invalid symbol LIB_ID: %s" ), inputPath ).
ToStdString() );
259 return tl::unexpected( e );
266 if( openProjectPath )
267 spec.
path = openProjectPath->GetFullPath();
274 e.set_status( ApiStatusCode::AS_BAD_REQUEST );
275 e.set_error_message( error.ToStdString() );
276 return tl::unexpected( e );
280 doc.type = requestType;
282 openDocuments.push_back( doc );
284 commands::OpenDocumentResponse response;
285 types::DocumentSpecifier* docSpec = response.mutable_document();
286 docSpec->set_type( requestType );
293 wxFileName projectPath( inputPath );
295 projectPath.MakeAbsolute();
297 if( openProjectPath && projectPath.GetFullPath() != openProjectPath->GetFullPath() )
300 e.set_status( ApiStatusCode::AS_BAD_REQUEST );
301 e.set_error_message( wxString::Format(
"cannot open a document from project '%s' because project "
302 "'%s' is already open.",
303 projectPath.GetFullName(), openProjectPath->GetFullName() )
305 return tl::unexpected( e );
308 if( requestType == types::DOCTYPE_PROJECT )
310 if( !openProjectPath )
312 if( !openDocuments.empty() )
314 auto closeResult = closeAllDocuments( commands::CloseAllDocuments() );
317 return tl::unexpected( closeResult.error() );
320 if( !
Pgm().GetSettingsManager().LoadProject( projectPath.GetFullPath(),
true ) )
322 wxLogTrace(
traceApi,
"Warning: no project file found for %s", inputPath );
325 if( !
Pgm().GetSettingsManager().GetProject( projectPath.GetFullPath() ) )
328 e.set_status( ApiStatusCode::AS_BAD_REQUEST );
329 e.set_error_message( wxString::Format(
"failed to load project '%s'", projectPath.GetFullPath() )
331 return tl::unexpected( e );
334 openProjectPath = projectPath;
337 if( std::ranges::find_if( openDocuments,
338 [](
const OPEN_DOCUMENT& d )
340 return d.type == types::DOCTYPE_PROJECT;
341 } ) == openDocuments.end() )
344 doc.type = types::DOCTYPE_PROJECT;
345 doc.fileName = projectPath.GetFullName();
346 openDocuments.push_back( doc );
349 commands::OpenDocumentResponse response;
350 types::DocumentSpecifier* doc = response.mutable_document();
353 doc->set_type( types::DOCTYPE_PROJECT );
354 doc->mutable_project()->set_name(
project.GetProjectName().ToUTF8() );
355 doc->mutable_project()->set_path(
project.GetProjectPath().ToUTF8() );
365 e.set_status( ApiStatusCode::AS_BAD_REQUEST );
366 e.set_error_message(
"unsupported document type" );
367 return tl::unexpected( e );
370 if( requestType == types::DOCTYPE_PCB || requestType == types::DOCTYPE_SCHEMATIC )
372 auto existing = std::ranges::find_if( openDocuments,
373 [&](
const OPEN_DOCUMENT& d )
375 return d.type == requestType;
378 if( existing != openDocuments.end() )
381 e.set_status( ApiStatusCode::AS_BAD_REQUEST );
382 e.set_error_message(
"a document of this type is already open" );
383 return tl::unexpected( e );
389 spec.
path = projectPath.GetFullPath();
396 e.set_status( ApiStatusCode::AS_BAD_REQUEST );
397 e.set_error_message( error.ToStdString() );
398 return tl::unexpected( e );
401 wxFileName docFile( inputPath );
402 docFile.MakeAbsolute();
405 doc.type = requestType;
406 doc.fileName = docFile.GetFullName();
407 doc.specPath = docFile.GetFullPath();
408 openDocuments.push_back( doc );
410 openProjectPath = projectPath;
412 commands::OpenDocumentResponse response;
413 types::DocumentSpecifier* docSpec = response.mutable_document();
416 docSpec->set_type( requestType );
418 if( requestType == types::DOCTYPE_PCB )
419 docSpec->set_board_filename( doc.fileName.ToStdString() );
421 docSpec->mutable_project()->set_name(
project.GetProjectName().ToUTF8() );
422 docSpec->mutable_project()->set_path(
project.GetProjectPath().ToUTF8() );
427 auto createDocument =
430 types::DocumentType requestType = aRequest.type();
434 if( requestType != types::DOCTYPE_PCB && requestType != types::DOCTYPE_SCHEMATIC )
437 e.set_status( ApiStatusCode::AS_UNIMPLEMENTED );
438 e.set_error_message(
"Only PCB and schematic documents can be created" );
439 return tl::unexpected( e );
442 wxString inputPath = wxString::FromUTF8( aRequest.path() );
444 if( inputPath.IsEmpty() )
447 e.set_status( ApiStatusCode::AS_BAD_REQUEST );
448 e.set_error_message(
"CreateDocument requires a non-empty path" );
449 return tl::unexpected( e );
452 wxFileName docPath( inputPath );
453 docPath.MakeAbsolute();
459 spec.
path = docPath.GetFullPath();
467 e.set_status( ApiStatusCode::AS_BAD_REQUEST );
468 e.set_error_message( error.ToStdString() );
469 return tl::unexpected( e );
477 doc.type = requestType;
478 doc.fileName = docPath.GetFullName();
479 doc.specPath = docPath.GetFullPath();
481 std::erase_if( openDocuments,
482 [&](
const OPEN_DOCUMENT& d )
484 return d.type == requestType;
487 openDocuments.push_back( doc );
489 commands::OpenDocumentResponse response;
490 types::DocumentSpecifier* docSpec = response.mutable_document();
492 docSpec->set_type( requestType );
494 if( requestType == types::DOCTYPE_PCB )
495 docSpec->set_board_filename( doc.fileName.ToStdString() );
505 if( openDocuments.empty() )
508 e.set_status( ApiStatusCode::AS_BAD_REQUEST );
509 e.set_error_message(
"No document is currently open" );
510 return tl::unexpected( e );
513 auto it = openDocuments.end();
515 if( aRequest.has_document() )
517 types::DocumentType typeToClose = aRequest.document().type();
519 it = std::ranges::find_if( openDocuments,
520 [&](
const OPEN_DOCUMENT& d )
522 return d.type == typeToClose;
525 if( it == openDocuments.end() )
528 e.set_status( ApiStatusCode::AS_BAD_REQUEST );
529 e.set_error_message(
"Requested document type does not match any open document" );
530 return tl::unexpected( e );
533 if( typeToClose == types::DOCTYPE_PCB
534 && !aRequest.document().board_filename().empty()
535 && it->fileName != wxString::FromUTF8( aRequest.document().board_filename() ) )
538 e.set_status( ApiStatusCode::AS_BAD_REQUEST );
539 e.set_error_message(
"Requested document does not match the open document" );
540 return tl::unexpected( e );
543 if( ( typeToClose == types::DOCTYPE_SCHEMATIC || typeToClose == types::DOCTYPE_PROJECT )
544 && aRequest.document().has_project()
546 && aRequest.document().project().name() != openProjectPath->GetName().ToStdString() )
549 e.set_status( ApiStatusCode::AS_BAD_REQUEST );
550 e.set_error_message(
"Requested document does not match the open project" );
551 return tl::unexpected( e );
554 if( ( typeToClose == types::DOCTYPE_FOOTPRINT || typeToClose == types::DOCTYPE_SYMBOL )
555 && aRequest.document().has_lib_id() )
562 e.set_status( ApiStatusCode::AS_BAD_REQUEST );
563 e.set_error_message( wxString::Format( wxS(
"Invalid library item LIB_ID: %s" ),
565 return tl::unexpected( e );
568 if( it->libId != docId )
571 e.set_status( ApiStatusCode::AS_BAD_REQUEST );
572 e.set_error_message(
"Requested document does not match the open document" );
573 return tl::unexpected( e );
580 it = openDocuments.begin();
583 if( it->type == types::DOCTYPE_PROJECT )
585 return closeAllDocuments( commands::CloseAllDocuments() );
592 server.get(), &error ) )
595 e.set_status( ApiStatusCode::AS_BAD_REQUEST );
596 e.set_error_message( error.ToStdString() );
597 return tl::unexpected( e );
601 openDocuments.erase( it );
603 if( openDocuments.empty() && openProjectPath )
607 openProjectPath.reset();
610 return google::protobuf::Empty();
618 server->SetRenameListener(
619 [&]( types::DocumentType aType,
const wxString& aOldPath,
const wxString& aNewPath )
621 auto it = std::ranges::find_if( openDocuments,
622 [&](
const OPEN_DOCUMENT& d )
624 return d.type == aType && d.specPath == aOldPath;
627 if( it == openDocuments.end() )
630 wxFileName newFile( aNewPath );
631 newFile.MakeAbsolute();
633 it->fileName = newFile.GetFullName();
634 it->specPath = newFile.GetFullPath();
636 if( openProjectPath )
638 wxFileName newProject( newFile );
640 openProjectPath = newProject;
644 server->RegisterHandler( &commonHandler );
645 server->RegisterHandler( &designBlockLibrariesHandler );
648 if( !server->Running() )
650 wxFprintf( stderr,
_(
"Failed to start API server\n" ) );
656 if( !preloadPath.IsEmpty() )
660 wxFileName preloadFile( preloadPath );
661 types::DocumentType preloadType = types::DOCTYPE_PROJECT;
664 preloadType = types::DOCTYPE_SCHEMATIC;
666 preloadType = types::DOCTYPE_PCB;
668 commands::OpenDocument request;
669 request.set_type( preloadType );
670 request.set_path( preloadPath.ToStdString() );
672 auto preloadResult = openDocument( request );
676 wxFprintf( stderr,
"%s\n", preloadResult.error().error_message() );
677 server->DeregisterHandler( &commonHandler );
678 server->DeregisterHandler( &designBlockLibrariesHandler );
683 server->SetReadyToReply(
true );
685 wxString listenPath = wxString::FromUTF8( server->SocketPath() );
686 wxFprintf( stdout,
"KiCad API server listening at %s\n", listenPath );
697 wxTheApp->ProcessPendingEvents();
701 std::signal( SIGINT, oldSigInt );
703 std::signal( SIGTERM, oldSigTerm );
706 wxFprintf( stdout,
"Shutting down\n" );
708 commands::CloseAllDocuments closeAllReq;
709 closeAllDocuments( closeAllReq );
710 server->DeregisterHandler( &commonHandler );
711 server->DeregisterHandler( &designBlockLibrariesHandler );