KiCad PCB EDA Suite
Loading...
Searching...
No Matches
python_manager.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 (C) 2023 Jon Evans <[email protected]>
5 * Copyright The KiCad Developers, see AUTHORS.txt for contributors.
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 <config.h>
22#include <gestfich.h>
23#include <wx/process.h>
24
25#include <future>
26#include <utility>
27
28#include <api/api_utils.h>
29#include <paths.h>
30#include <pgm_base.h>
31#include <api/python_manager.h>
32#include <thread_pool.h>
33#include <wx_filename.h>
34
35
36class PYTHON_PROCESS : public wxProcess
37{
38public:
39 PYTHON_PROCESS( std::function<void(int, const wxString&, const wxString&)> aCallback ) :
40 wxProcess(),
41 m_callback( std::move( aCallback ) )
42 {}
43
44 void OnTerminate( int aPid, int aStatus ) override
45 {
46 // Print stdout trace info from the monitor thread
47 wxLog::GetActiveTarget()->Flush();
48
49 if( m_callback )
50 {
51 wxString output, error;
52 wxInputStream* processOut = GetInputStream();
53 size_t bytesRead = 0;
54
55 while( processOut->CanRead() && bytesRead < MAX_OUTPUT_LEN )
56 {
57 char buffer[4096];
58 buffer[ processOut->Read( buffer, sizeof( buffer ) - 1 ).LastRead() ] = '\0';
59 output.append( buffer, processOut->LastRead() );
60 bytesRead += processOut->LastRead();
61 }
62
63 processOut = GetErrorStream();
64 bytesRead = 0;
65
66 while( processOut->CanRead() && bytesRead < MAX_OUTPUT_LEN )
67 {
68 char buffer[4096];
69 buffer[ processOut->Read( buffer, sizeof( buffer ) - 1 ).LastRead() ] = '\0';
70 error.append( buffer, processOut->LastRead() );
71 bytesRead += processOut->LastRead();
72 }
73
74 m_callback( aStatus, output, error );
75 }
76 }
77
78 static constexpr size_t MAX_OUTPUT_LEN = 1024L * 1024L;
79
80private:
81 std::function<void(int, const wxString&, const wxString&)> m_callback;
82};
83
84
85PYTHON_MANAGER::PYTHON_MANAGER( const wxString& aInterpreterPath )
86{
87 wxFileName path( aInterpreterPath );
88 path.Normalize( FN_NORMALIZE_FLAGS );
89 m_interpreterPath = path.GetFullPath();
90}
91
92
93long PYTHON_MANAGER::Execute( const std::vector<wxString>& aArgs,
94 const std::function<void(int, const wxString&, const wxString&)>& aCallback,
95 const wxExecuteEnv* aEnv, bool aSaveOutput )
96{
97 PYTHON_PROCESS* process = new PYTHON_PROCESS( aCallback );
98 process->Redirect();
99
100 auto monitor =
101 []( PYTHON_PROCESS* aProcess )
102 {
103 wxInputStream* processOut = aProcess->GetInputStream();
104
105 while( aProcess->IsInputOpened() )
106 {
107 if( processOut->CanRead() )
108 {
109 char buffer[4096];
110 buffer[processOut->Read( buffer, sizeof( buffer ) - 1 ).LastRead()] = '\0';
111 wxString stdOut( buffer, processOut->LastRead() );
112 stdOut = stdOut.BeforeLast( '\n' );
113 wxLogTrace( traceApi, wxString::Format( "Python: %s", stdOut ) );
114 }
115 }
116 };
117
118 wxString argsStr;
119 std::vector<const wchar_t*> args = { m_interpreterPath.wc_str() };
120
121 for( const wxString& arg : aArgs )
122 {
123 args.emplace_back( arg.wc_str() );
124 argsStr << arg << " ";
125 }
126
127 args.emplace_back( nullptr );
128
129 if( wxTheApp->UsesEventLoop() )
130 {
131 wxLogTrace( traceApi, wxString::Format( "Execute async: %s %s", m_interpreterPath, argsStr ) );
132 long pid = wxExecute( args.data(), wxEXEC_ASYNC, process, aEnv );
133
134 if( pid == 0 )
135 {
136 wxLogTrace( traceApi, wxString::Format( "Execute error: process could not be created" ) );
137 delete process;
138 aCallback( -1, wxEmptyString, _( "Process could not be created" ) );
139 }
140 else
141 {
142 wxLogTrace( traceApi, wxString::Format( "Execute: pid %ld", pid ) );
143
144 // On Windows, if there is a lot of stdout written by the process, this can
145 // hang up the wxProcess such that it will never call OnTerminate. To work
146 // around this, we use this monitor thread to just dump the stdout to the
147 // trace log, which prevents the hangup. This flag is provided to keep the
148 // old behavior for commands where we need to read the output directly,
149 // which is currently only used for detecting the interpreter version.
150 // If we need to use the async monitor thread approach and preserve the stdout
151 // contents in the future, a more complicated hack might be necessary.
152 if( !aSaveOutput )
153 {
155 auto ret = tp.submit_task( [monitor, process] { monitor( process ); } );
156 }
157 }
158
159 return pid;
160 }
161 else
162 {
163 wxLogTrace( traceApi, wxString::Format( "Execute sync: %s %s", m_interpreterPath, argsStr ) );
164 wxArrayString out, err;
165 wxString cmd = wxString::Format( "%s %s", m_interpreterPath, argsStr );
166 long ret = wxExecute( cmd, out, err, wxEXEC_BLOCK, aEnv );
167
168 wxString strOut, strErr;
169
170 for( const wxString& line : out )
171 strOut << line << "\n";
172
173 for( const wxString& line : err )
174 strErr << line << "\n";
175
176 aCallback( ret, strOut, strErr );
177
178 return ret;
179 }
180}
181
182
183long PYTHON_MANAGER::ExecuteSync( const std::vector<wxString>& aArgs,
184 wxString* aStdout, wxString* aStderr,
185 const wxExecuteEnv* aEnv )
186{
187 wxString argsStr;
188
189 for( const wxString& arg : aArgs )
190 argsStr << arg << " ";
191
192 wxLogTrace( traceApi, wxString::Format( "Execute sync: %s %s", m_interpreterPath, argsStr ) );
193 wxArrayString out, err;
194 wxString cmd = wxString::Format( "%s %s", m_interpreterPath, argsStr );
195 long ret = wxExecute( cmd, out, err, wxEXEC_BLOCK, aEnv );
196
197 wxString strOut, strErr;
198
199 if( aStdout )
200 {
201 for( const wxString& line : out )
202 *aStdout << line << "\n";
203 }
204
205 if( aStderr )
206 {
207 for( const wxString& line : err )
208 *aStderr << line << "\n";
209 }
210
211 return ret;
212}
213
214
216{
217 // First, attempt to use a Python we distribute with KiCad
218#if defined( __WINDOWS__ )
219 wxFileName pythonExe = FindKicadFile( "pythonw.exe" );
220
221 if( pythonExe.IsFileExecutable() )
222 return pythonExe.GetFullPath();
223#else
224 wxFileName pythonExe;
225#endif
226
227 // In case one is forced with cmake
228 pythonExe.Assign( wxString::FromUTF8Unchecked( PYTHON_EXECUTABLE ) );
229
230 if( pythonExe.IsFileExecutable() )
231 return pythonExe.GetFullPath();
232
233 // Fall back on finding any Python in the user's path
234
235#ifdef _WIN32
236 wxArrayString output;
237
238 if( 0 == wxExecute( wxS( "where pythonw.exe" ), output, wxEXEC_SYNC ) )
239 {
240 if( !output.IsEmpty() )
241 return output[0];
242 }
243#else
244 wxArrayString output;
245
246 if( 0 == wxExecute( wxS( "which -a python3" ), output, wxEXEC_SYNC ) )
247 {
248 if( !output.IsEmpty() )
249 return output[0];
250 }
251
252 if( 0 == wxExecute( wxS( "which -a python" ), output, wxEXEC_SYNC ) )
253 {
254 if( !output.IsEmpty() )
255 return output[0];
256 }
257#endif
258
259 return wxEmptyString;
260}
261
262
263std::optional<wxString> PYTHON_MANAGER::GetPythonEnvironment( const wxString& aNamespace )
264{
265 wxFileName path( PATHS::GetUserCachePath(), wxEmptyString );
266 path.AppendDir( wxS( "python-environments" ) );
267 path.AppendDir( aNamespace );
268
269 if( !PATHS::EnsurePathExists( path.GetPath() ) )
270 return std::nullopt;
271
272 return path.GetPath();
273}
274
275
276std::optional<wxString> PYTHON_MANAGER::GetVirtualPython( const wxString& aNamespace )
277{
278 std::optional<wxString> envPath = GetPythonEnvironment( aNamespace );
279
280 if( !envPath )
281 return std::nullopt;
282
283 wxFileName python( *envPath, wxEmptyString );
284
285#ifdef _WIN32
286 python.AppendDir( "Scripts" );
287 python.SetFullName( "pythonw.exe" );
288#else
289 python.AppendDir( "bin" );
290 python.SetFullName( "python" );
291#endif
292
293 if( !python.IsFileExecutable() )
294 return std::nullopt;
295
296 return python.GetFullPath();
297}
static bool EnsurePathExists(const wxString &aPath, bool aPathToFile=false)
Attempts to create a given path if it does not exist.
Definition paths.cpp:518
static wxString GetUserCachePath()
Gets the stock (install) 3d viewer plugins path.
Definition paths.cpp:460
static std::optional< wxString > GetPythonEnvironment(const wxString &aNamespace)
static wxString FindPythonInterpreter()
Searches for a Python intepreter on the user's system.
long Execute(const std::vector< wxString > &aArgs, const std::function< void(int, const wxString &, const wxString &)> &aCallback, const wxExecuteEnv *aEnv=nullptr, bool aSaveOutput=false)
Launches the Python interpreter with the given arguments.
static std::optional< wxString > GetVirtualPython(const wxString &aNamespace)
Returns a full path to the python binary in a venv, if it exists.
long ExecuteSync(const std::vector< wxString > &aArgs, wxString *aStdout=nullptr, wxString *aStderr=nullptr, const wxExecuteEnv *aEnv=nullptr)
wxString m_interpreterPath
PYTHON_MANAGER(const wxString &aInterpreterPath)
static constexpr size_t MAX_OUTPUT_LEN
PYTHON_PROCESS(std::function< void(int, const wxString &, const wxString &)> aCallback)
std::function< void(int, const wxString &, const wxString &)> m_callback
void OnTerminate(int aPid, int aStatus) override
#define _(s)
wxString FindKicadFile(const wxString &shortname)
Search the executable file shortname in KiCad binary path and return full file name if found or short...
Definition gestfich.cpp:61
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.
static PGM_BASE * process
see class PGM_BASE
std::string path
nlohmann::json output
thread_pool & GetKiCadThreadPool()
Get a reference to the current thread pool.
static thread_pool * tp
BS::priority_thread_pool thread_pool
Definition thread_pool.h:31
#define FN_NORMALIZE_FLAGS
Default flags to pass to wxFileName::Normalize().
Definition wx_filename.h:39