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
18 * along with this program. If not, see <https://www.gnu.org/licenses/>.
19 */
20
21#include <config.h>
22#include <gestfich.h>
23#include <wx/process.h>
24#include <wx/textfile.h>
25
26#include <future>
27#include <utility>
28
29#include <api/api_utils.h>
30#include <paths.h>
31#include <pgm_base.h>
32#include <api/python_manager.h>
33#include <thread_pool.h>
34#include <wx_filename.h>
35
36
37class PYTHON_PROCESS : public wxProcess
38{
39public:
40 PYTHON_PROCESS( std::function<void(int, const wxString&, const wxString&)> aCallback ) :
41 wxProcess(),
42 m_callback( std::move( aCallback ) )
43 {}
44
45 void OnTerminate( int aPid, int aStatus ) override
46 {
47 // Print stdout trace info from the monitor thread
48 wxLog::GetActiveTarget()->Flush();
49
50 if( m_callback )
51 {
52 wxString output, error;
53 wxInputStream* processOut = GetInputStream();
54 size_t bytesRead = 0;
55
56 while( processOut->CanRead() && bytesRead < MAX_OUTPUT_LEN )
57 {
58 char buffer[4096];
59 buffer[ processOut->Read( buffer, sizeof( buffer ) - 1 ).LastRead() ] = '\0';
60 output.append( buffer, processOut->LastRead() );
61 bytesRead += processOut->LastRead();
62 }
63
64 processOut = GetErrorStream();
65 bytesRead = 0;
66
67 while( processOut->CanRead() && bytesRead < MAX_OUTPUT_LEN )
68 {
69 char buffer[4096];
70 buffer[ processOut->Read( buffer, sizeof( buffer ) - 1 ).LastRead() ] = '\0';
71 error.append( buffer, processOut->LastRead() );
72 bytesRead += processOut->LastRead();
73 }
74
75 m_callback( aStatus, output, error );
76 }
77 }
78
79 static constexpr size_t MAX_OUTPUT_LEN = 1024L * 1024L;
80
81private:
82 std::function<void(int, const wxString&, const wxString&)> m_callback;
83};
84
85
86PYTHON_MANAGER::PYTHON_MANAGER( const wxString& aInterpreterPath )
87{
88 wxFileName path( aInterpreterPath );
89 path.Normalize( FN_NORMALIZE_FLAGS );
90 m_interpreterPath = path.GetFullPath();
91}
92
93
94static wxString readAllFromStream( wxInputStream* aStream )
95{
96 wxString result;
97
98 if( aStream )
99 {
100 char buffer[4096];
101
102 while( aStream->CanRead() )
103 {
104 aStream->Read( buffer, sizeof( buffer ) );
105 result.Append( buffer, aStream->LastRead() );
106 }
107 }
108
109 return result;
110};
111
112
113long PYTHON_MANAGER::Execute( const std::vector<wxString>& aArgs,
114 const std::function<void(int, const wxString&, const wxString&)>& aCallback,
115 const wxExecuteEnv* aEnv, bool aSaveOutput )
116{
117 PYTHON_PROCESS* process = new PYTHON_PROCESS( aCallback );
118 process->Redirect();
119
120 auto monitor =
121 []( PYTHON_PROCESS* aProcess )
122 {
123 wxInputStream* processOut = aProcess->GetInputStream();
124
125 while( aProcess->IsInputOpened() )
126 {
127 if( processOut->CanRead() )
128 {
129 char buffer[4096];
130 buffer[processOut->Read( buffer, sizeof( buffer ) - 1 ).LastRead()] = '\0';
131 wxString stdOut( buffer, processOut->LastRead() );
132 stdOut = stdOut.BeforeLast( '\n' );
133 wxLogTrace( traceApi, wxString::Format( "Python: %s", stdOut ) );
134 }
135 }
136 };
137
138 wxString argsStr;
139 std::vector<const wchar_t*> args = { m_interpreterPath.wc_str() };
140
141 for( const wxString& arg : aArgs )
142 {
143 args.emplace_back( arg.wc_str() );
144 argsStr << arg << " ";
145 }
146
147 args.emplace_back( nullptr );
148
149 if( wxTheApp->UsesEventLoop() )
150 {
151 wxLogTrace( traceApi, wxString::Format( "Execute async: %s %s", m_interpreterPath, argsStr ) );
152 long pid = wxExecute( args.data(), wxEXEC_ASYNC, process, aEnv );
153
154 if( pid == 0 )
155 {
156 wxLogTrace( traceApi, wxString::Format( "Execute error: process could not be created" ) );
157 delete process;
158 aCallback( -1, wxEmptyString, _( "Process could not be created" ) );
159 }
160 else
161 {
162 wxLogTrace( traceApi, wxString::Format( "Execute: pid %ld", pid ) );
163
164 // On Windows, if there is a lot of stdout written by the process, this can
165 // hang up the wxProcess such that it will never call OnTerminate. To work
166 // around this, we use this monitor thread to just dump the stdout to the
167 // trace log, which prevents the hangup. This flag is provided to keep the
168 // old behavior for commands where we need to read the output directly,
169 // which is currently only used for detecting the interpreter version.
170 // If we need to use the async monitor thread approach and preserve the stdout
171 // contents in the future, a more complicated hack might be necessary.
172 if( !aSaveOutput )
173 {
175 auto ret = tp.submit_task( [monitor, process] { monitor( process ); } );
176 }
177 }
178
179 return pid;
180 }
181 else
182 {
183 wxWCharBuffer interpreterBuf = m_interpreterPath.wc_str();
184 std::vector<wxWCharBuffer> argBufs;
185 argBufs.reserve( aArgs.size() );
186
187 for( const wxString& arg : aArgs )
188 argBufs.emplace_back( arg.wc_str() );
189
190 std::vector<const wchar_t*> syncArgs = { interpreterBuf.data() };
191
192 for( const wxWCharBuffer& buf : argBufs )
193 syncArgs.emplace_back( buf.data() );
194
195 syncArgs.emplace_back( nullptr );
196
197 wxLogTrace( traceApi, wxString::Format( "Execute sync: %s %s", m_interpreterPath, argsStr ) );
198
199 wxProcess syncProcess;
200 syncProcess.Redirect();
201
202 long ret = wxExecute( syncArgs.data(), wxEXEC_BLOCK, &syncProcess, aEnv );
203
204 aCallback( static_cast<int>( ret ), readAllFromStream( syncProcess.GetInputStream() ),
205 readAllFromStream( syncProcess.GetErrorStream() ) );
206
207 return ret;
208 }
209}
210
211
212long PYTHON_MANAGER::ExecuteSync( const std::vector<wxString>& aArgs,
213 wxString* aStdout, wxString* aStderr,
214 const wxExecuteEnv* aEnv )
215{
216 wxWCharBuffer interpreterBuf = m_interpreterPath.wc_str();
217 std::vector<wxWCharBuffer> argBufs;
218 argBufs.reserve( aArgs.size() );
219
220 for( const wxString& arg : aArgs )
221 argBufs.emplace_back( arg.wc_str() );
222
223 std::vector<const wchar_t*> args = { interpreterBuf.data() };
224
225 for( const wxWCharBuffer& buf : argBufs )
226 args.emplace_back( buf.data() );
227
228 args.emplace_back( nullptr );
229
230 wxString argsStr;
231
232 for( const wxString& arg : aArgs )
233 argsStr << arg << " ";
234
235 wxLogTrace( traceApi, wxString::Format( "Execute sync: %s %s", m_interpreterPath, argsStr ) );
236
237 wxProcess process;
238 process.Redirect();
239
240 long ret = wxExecute( args.data(), wxEXEC_BLOCK, &process, aEnv );
241
242 if( aStdout )
243 *aStdout = readAllFromStream( process.GetInputStream() );
244
245 if( aStderr )
246 *aStderr = readAllFromStream( process.GetErrorStream() );
247
248 return ret;
249}
250
251
253{
254 // First, attempt to use a Python we distribute with KiCad
255#if defined( __WINDOWS__ )
256 wxFileName pythonExe = FindKicadFile( "pythonw.exe" );
257
258 if( pythonExe.IsFileExecutable() )
259 return pythonExe.GetFullPath();
260#else
261 wxFileName pythonExe;
262#endif
263
264 // In case one is forced with cmake
265 pythonExe.Assign( wxString::FromUTF8Unchecked( PYTHON_EXECUTABLE ) );
266
267 if( pythonExe.IsFileExecutable() )
268 return pythonExe.GetFullPath();
269
270 // Fall back on finding any Python in the user's path
271
272#ifdef _WIN32
273 wxArrayString output;
274
275 if( 0 == wxExecute( wxS( "where pythonw.exe" ), output, wxEXEC_SYNC ) )
276 {
277 if( !output.IsEmpty() )
278 return output[0];
279 }
280#else
281 wxArrayString output;
282
283 if( 0 == wxExecute( wxS( "which -a python3" ), output, wxEXEC_SYNC ) )
284 {
285 if( !output.IsEmpty() )
286 return output[0];
287 }
288
289 if( 0 == wxExecute( wxS( "which -a python" ), output, wxEXEC_SYNC ) )
290 {
291 if( !output.IsEmpty() )
292 return output[0];
293 }
294#endif
295
296 return wxEmptyString;
297}
298
299
300std::optional<wxString> PYTHON_MANAGER::GetPythonEnvironment( const wxString& aNamespace )
301{
302 wxFileName path( PATHS::GetUserCachePath(), wxEmptyString );
303 path.AppendDir( wxS( "python-environments" ) );
304 path.AppendDir( aNamespace );
305
306 if( !PATHS::EnsurePathExists( path.GetPath() ) )
307 return std::nullopt;
308
309 return path.GetPath();
310}
311
312
313std::optional<wxString> PYTHON_MANAGER::GetVirtualPython( const wxString& aNamespace )
314{
315 std::optional<wxString> envPath = GetPythonEnvironment( aNamespace );
316
317 if( !envPath )
318 return std::nullopt;
319
320 wxFileName python( *envPath, wxEmptyString );
321
322#ifdef _WIN32
323 python.AppendDir( "Scripts" );
324 python.SetFullName( "pythonw.exe" );
325#else
326 python.AppendDir( "bin" );
327 python.SetFullName( "python" );
328#endif
329
330 if( !python.IsFileExecutable() )
331 return std::nullopt;
332
333 return python.GetFullPath();
334}
335
336
337static wxString venvPythonPath( const wxString& aEnvPath )
338{
339 wxFileName python( aEnvPath, wxEmptyString );
340
341#ifdef _WIN32
342 python.AppendDir( "Scripts" );
343 python.SetFullName( "pythonw.exe" );
344#else
345 python.AppendDir( "bin" );
346 python.SetFullName( "python" );
347#endif
348
349 return python.GetFullPath();
350}
351
352
353std::optional<wxString> PYTHON_MANAGER::GetVenvInterpreter( const wxString& aEnvPath )
354{
355 wxFileName cfg( aEnvPath, wxS( "pyvenv.cfg" ) );
356
357 if( !cfg.IsFileReadable() )
358 return std::nullopt;
359
360 wxTextFile textFile( cfg.GetFullPath() );
361
362 if( !textFile.Open() )
363 return std::nullopt;
364
365 wxString executable;
366
367 for( wxString line = textFile.GetFirstLine(); !textFile.Eof(); line = textFile.GetNextLine() )
368 {
369 if( line.Trim( true ).Trim( false ).StartsWith( wxS( "executable" ) ) )
370 {
371 executable = line.AfterFirst( '=' ).Trim( true ).Trim( false );
372 break;
373 }
374 }
375
376 textFile.Close();
377
378 if( executable.IsEmpty() )
379 return std::nullopt;
380
381 return executable;
382}
383
384
385bool PYTHON_MANAGER::IsVenvUsable( const wxString& aEnvPath )
386{
387 if( wxString pythonPath = venvPythonPath( aEnvPath ); !wxFileName( pythonPath ).IsFileExecutable() )
388 return false;
389
390 if( std::optional<wxString> interpreter = GetVenvInterpreter( aEnvPath ) )
391 {
392 if( wxFileName base( *interpreter ); !base.IsFileExecutable() )
393 return false;
394 }
395
396 return true;
397}
398
399
400static wxString canonicalizePath( const wxString& aPath )
401{
402#ifndef _WIN32
403 char buffer[PATH_MAX];
404
405 if( char* resolved = realpath( aPath.ToUTF8().data(), buffer ) )
406 return wxString::FromUTF8( resolved );
407#endif
408
409 wxFileName fn( aPath );
410 fn.Normalize( FN_NORMALIZE_FLAGS );
411 return fn.GetFullPath();
412}
413
414
415bool PYTHON_MANAGER::IsVenvStale( const wxString& aEnvPath, const wxString& aConfiguredInterpreter )
416{
417 if( std::optional<wxString> executable = GetVenvInterpreter( aEnvPath ) )
418 return canonicalizePath( *executable ) != canonicalizePath( aConfiguredInterpreter );
419
420 wxFileName cfg( aEnvPath, wxS( "pyvenv.cfg" ) );
421
422 if( !cfg.IsFileReadable() )
423 return false;
424
425 wxTextFile textFile( cfg.GetFullPath() );
426
427 if( !textFile.Open() )
428 return false;
429
430 wxString home;
431
432 for( wxString line = textFile.GetFirstLine(); !textFile.Eof(); line = textFile.GetNextLine() )
433 {
434 if( line.Trim( true ).Trim( false ).StartsWith( wxS( "home" ) ) )
435 {
436 home = line.AfterFirst( '=' ).Trim( true ).Trim( false );
437 break;
438 }
439 }
440
441 textFile.Close();
442
443 if( home.IsEmpty() )
444 return false;
445
446 wxFileName configuredFn( canonicalizePath( aConfiguredInterpreter ) );
447 return canonicalizePath( home ) != configuredFn.GetPath();
448}
static bool EnsurePathExists(const wxString &aPath, bool aPathToFile=false)
Attempts to create a given path if it does not exist.
Definition paths.cpp:508
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 > GetVenvInterpreter(const wxString &aEnvPath)
Returns the path of the interpreter that was used to create the venv in the given environment directo...
static bool IsVenvUsable(const wxString &aEnvPath)
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)
static bool IsVenvStale(const wxString &aEnvPath, const wxString &aConfiguredInterpreter)
Returns true if the venv at aEnvPath was created by an interpreter other than aConfiguredInterpreter.
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:67
const wxChar *const traceApi
Flag to enable debug output related to the IPC API and its plugin system.
Definition api_utils.cpp:33
STL namespace.
static PGM_BASE * process
see class PGM_BASE
static wxString readAllFromStream(wxInputStream *aStream)
static wxString venvPythonPath(const wxString &aEnvPath)
static wxString canonicalizePath(const wxString &aPath)
std::string path
wxString result
Test unit parsing edge cases and error handling.
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:27
#define FN_NORMALIZE_FLAGS
Default flags to pass to wxFileName::Normalize().
Definition wx_filename.h:35