kicad-source/scripting/python_manager.cpp

244 lines
7.3 KiB
C++
Raw Normal View History

/*
* This program source code file is part of KiCad, a free EDA CAD application.
*
* Copyright (C) 2023 Jon Evans <jon@craftyjon.com>
* Copyright The KiCad Developers, see AUTHORS.txt for contributors.
*
* This program is free software: you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* General Public License for more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include <config.h>
#include <gestfich.h>
#include <wx/process.h>
#include <future>
#include <utility>
#include <api/api_utils.h>
#include <paths.h>
#include <pgm_base.h>
#include <python_manager.h>
2025-01-10 22:41:07 -05:00
#include <thread_pool.h>
#include <wx_filename.h>
class PYTHON_PROCESS : public wxProcess
{
public:
PYTHON_PROCESS( std::function<void(int, const wxString&, const wxString&)> aCallback ) :
wxProcess(),
m_callback( std::move( aCallback ) )
{}
void OnTerminate( int aPid, int aStatus ) override
{
// Print stdout trace info from the monitor thread
wxLog::GetActiveTarget()->Flush();
if( m_callback )
{
wxString output, error;
wxInputStream* processOut = GetInputStream();
size_t bytesRead = 0;
while( processOut->CanRead() && bytesRead < MAX_OUTPUT_LEN )
{
char buffer[4096];
buffer[ processOut->Read( buffer, sizeof( buffer ) - 1 ).LastRead() ] = '\0';
output.append( buffer, processOut->LastRead() );
bytesRead += processOut->LastRead();
}
processOut = GetErrorStream();
bytesRead = 0;
while( processOut->CanRead() && bytesRead < MAX_OUTPUT_LEN )
{
char buffer[4096];
buffer[ processOut->Read( buffer, sizeof( buffer ) - 1 ).LastRead() ] = '\0';
error.append( buffer, processOut->LastRead() );
bytesRead += processOut->LastRead();
}
m_callback( aStatus, output, error );
}
}
static constexpr size_t MAX_OUTPUT_LEN = 1024L * 1024L;
private:
std::function<void(int, const wxString&, const wxString&)> m_callback;
};
PYTHON_MANAGER::PYTHON_MANAGER( const wxString& aInterpreterPath )
{
wxFileName path( aInterpreterPath );
path.Normalize( FN_NORMALIZE_FLAGS );
m_interpreterPath = path.GetFullPath();
}
long PYTHON_MANAGER::Execute( const wxString& aArgs,
const std::function<void(int, const wxString&, const wxString&)>& aCallback,
const wxExecuteEnv* aEnv, bool aSaveOutput )
{
PYTHON_PROCESS* process = new PYTHON_PROCESS( aCallback );
process->Redirect();
auto monitor =
[]( PYTHON_PROCESS* aProcess )
{
wxInputStream* processOut = aProcess->GetInputStream();
while( aProcess->IsInputOpened() )
{
if( processOut->CanRead() )
{
char buffer[4096];
buffer[processOut->Read( buffer, sizeof( buffer ) - 1 ).LastRead()] = '\0';
wxString stdOut( buffer, processOut->LastRead() );
stdOut = stdOut.BeforeLast( '\n' );
wxLogTrace( traceApi, wxString::Format( "Python: %s", stdOut ) );
}
}
};
wxString cmd = wxString::Format( wxS( "%s %s" ), m_interpreterPath, aArgs );
wxLogTrace( traceApi, wxString::Format( "Execute: %s", cmd ) );
long pid = wxExecute( cmd, wxEXEC_ASYNC, process, aEnv );
if( pid == 0 )
{
delete process;
aCallback( -1, wxEmptyString, _( "Process could not be created" ) );
}
else
{
// On Windows, if there is a lot of stdout written by the process, this can
// hang up the wxProcess such that it will never call OnTerminate. To work
// around this, we use this monitor thread to just dump the stdout to the
// trace log, which prevents the hangup. This flag is provided to keep the
// old behavior for commands where we need to read the output directly,
// which is currently only used for detecting the interpreter version.
// If we need to use the async monitor thread approach and preserve the stdout
// contents in the future, a more complicated hack might be necessary.
if( !aSaveOutput )
2025-01-10 22:41:07 -05:00
{
thread_pool& tp = GetKiCadThreadPool();
auto ret = tp.submit( monitor, process );
}
}
return pid;
}
wxString PYTHON_MANAGER::FindPythonInterpreter()
{
// First, attempt to use a Python we distribute with KiCad
#if defined( __WINDOWS__ )
2025-01-10 22:41:07 -05:00
wxFileName pythonExe = FindKicadFile( "pythonw.exe" );
if( pythonExe.IsFileExecutable() )
return pythonExe.GetFullPath();
#elif defined( __WXMAC__ )
wxFileName pythonExe( PATHS::GetOSXKicadDataDir(), wxEmptyString );
pythonExe.RemoveLastDir();
pythonExe.AppendDir( wxT( "Frameworks" ) );
pythonExe.AppendDir( wxT( "Python.framework" ) );
pythonExe.AppendDir( wxT( "Versions" ) );
pythonExe.AppendDir( wxT( "Current" ) );
pythonExe.AppendDir( wxT( "bin" ) );
pythonExe.SetFullName(wxT( "python3" ) );
if( pythonExe.IsFileExecutable() )
return pythonExe.GetFullPath();
2025-01-02 00:42:22 +08:00
#else
wxFileName pythonExe;
#endif
// In case one is forced with cmake
pythonExe.Assign( wxString::FromUTF8Unchecked( PYTHON_EXECUTABLE ) );
if( pythonExe.IsFileExecutable() )
return pythonExe.GetFullPath();
// Fall back on finding any Python in the user's path
2024-06-01 04:14:48 +03:00
#ifdef _WIN32
wxArrayString output;
2025-01-10 22:41:07 -05:00
if( 0 == wxExecute( wxS( "where pythonw.exe" ), output, wxEXEC_SYNC ) )
{
if( !output.IsEmpty() )
return output[0];
}
#else
wxArrayString output;
if( 0 == wxExecute( wxS( "which -a python3" ), output, wxEXEC_SYNC ) )
{
if( !output.IsEmpty() )
return output[0];
}
if( 0 == wxExecute( wxS( "which -a python" ), output, wxEXEC_SYNC ) )
{
if( !output.IsEmpty() )
return output[0];
}
#endif
return wxEmptyString;
}
std::optional<wxString> PYTHON_MANAGER::GetPythonEnvironment( const wxString& aNamespace )
{
wxFileName path( PATHS::GetUserCachePath(), wxEmptyString );
path.AppendDir( wxS( "python-environments" ) );
path.AppendDir( aNamespace );
if( !PATHS::EnsurePathExists( path.GetPath() ) )
return std::nullopt;
return path.GetPath();
}
std::optional<wxString> PYTHON_MANAGER::GetVirtualPython( const wxString& aNamespace )
{
std::optional<wxString> envPath = GetPythonEnvironment( aNamespace );
if( !envPath )
return std::nullopt;
wxFileName python( *envPath, wxEmptyString );
#ifdef _WIN32
python.AppendDir( "Scripts" );
python.SetFullName( "pythonw.exe" );
#else
python.AppendDir( "bin" );
python.SetFullName( "python" );
#endif
if( !python.IsFileExecutable() )
return std::nullopt;
return python.GetFullPath();
}