You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

252 lines
7.5 KiB

  1. /*
  2. * This program source code file is part of KiCad, a free EDA CAD application.
  3. *
  4. * Copyright (C) 2023 Jon Evans <jon@craftyjon.com>
  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. #include <config.h>
  21. #include <gestfich.h>
  22. #include <wx/process.h>
  23. #include <future>
  24. #include <utility>
  25. #include <api/api_utils.h>
  26. #include <paths.h>
  27. #include <pgm_base.h>
  28. #include <python_manager.h>
  29. #include <thread_pool.h>
  30. #include <wx_filename.h>
  31. class PYTHON_PROCESS : public wxProcess
  32. {
  33. public:
  34. PYTHON_PROCESS( std::function<void(int, const wxString&, const wxString&)> aCallback ) :
  35. wxProcess(),
  36. m_callback( std::move( aCallback ) )
  37. {}
  38. void OnTerminate( int aPid, int aStatus ) override
  39. {
  40. // Print stdout trace info from the monitor thread
  41. wxLog::GetActiveTarget()->Flush();
  42. if( m_callback )
  43. {
  44. wxString output, error;
  45. wxInputStream* processOut = GetInputStream();
  46. size_t bytesRead = 0;
  47. while( processOut->CanRead() && bytesRead < MAX_OUTPUT_LEN )
  48. {
  49. char buffer[4096];
  50. buffer[ processOut->Read( buffer, sizeof( buffer ) - 1 ).LastRead() ] = '\0';
  51. output.append( buffer, processOut->LastRead() );
  52. bytesRead += processOut->LastRead();
  53. }
  54. processOut = GetErrorStream();
  55. bytesRead = 0;
  56. while( processOut->CanRead() && bytesRead < MAX_OUTPUT_LEN )
  57. {
  58. char buffer[4096];
  59. buffer[ processOut->Read( buffer, sizeof( buffer ) - 1 ).LastRead() ] = '\0';
  60. error.append( buffer, processOut->LastRead() );
  61. bytesRead += processOut->LastRead();
  62. }
  63. m_callback( aStatus, output, error );
  64. }
  65. }
  66. static constexpr size_t MAX_OUTPUT_LEN = 1024L * 1024L;
  67. private:
  68. std::function<void(int, const wxString&, const wxString&)> m_callback;
  69. };
  70. PYTHON_MANAGER::PYTHON_MANAGER( const wxString& aInterpreterPath )
  71. {
  72. wxFileName path( aInterpreterPath );
  73. path.Normalize( FN_NORMALIZE_FLAGS );
  74. m_interpreterPath = path.GetFullPath();
  75. }
  76. long PYTHON_MANAGER::Execute( const std::vector<wxString>& aArgs,
  77. const std::function<void(int, const wxString&, const wxString&)>& aCallback,
  78. const wxExecuteEnv* aEnv, bool aSaveOutput )
  79. {
  80. PYTHON_PROCESS* process = new PYTHON_PROCESS( aCallback );
  81. process->Redirect();
  82. auto monitor =
  83. []( PYTHON_PROCESS* aProcess )
  84. {
  85. wxInputStream* processOut = aProcess->GetInputStream();
  86. while( aProcess->IsInputOpened() )
  87. {
  88. if( processOut->CanRead() )
  89. {
  90. char buffer[4096];
  91. buffer[processOut->Read( buffer, sizeof( buffer ) - 1 ).LastRead()] = '\0';
  92. wxString stdOut( buffer, processOut->LastRead() );
  93. stdOut = stdOut.BeforeLast( '\n' );
  94. wxLogTrace( traceApi, wxString::Format( "Python: %s", stdOut ) );
  95. }
  96. }
  97. };
  98. wxString argsStr;
  99. std::vector<const wchar_t*> args = { m_interpreterPath.wc_str() };
  100. for( const wxString& arg : aArgs )
  101. {
  102. args.emplace_back( arg.wc_str() );
  103. argsStr << arg << " ";
  104. }
  105. args.emplace_back( nullptr );
  106. wxLogTrace( traceApi, wxString::Format( "Execute: %s %s", m_interpreterPath, argsStr ) );
  107. long pid = wxExecute( args.data(), wxEXEC_ASYNC, process, aEnv );
  108. if( pid == 0 )
  109. {
  110. delete process;
  111. aCallback( -1, wxEmptyString, _( "Process could not be created" ) );
  112. }
  113. else
  114. {
  115. // On Windows, if there is a lot of stdout written by the process, this can
  116. // hang up the wxProcess such that it will never call OnTerminate. To work
  117. // around this, we use this monitor thread to just dump the stdout to the
  118. // trace log, which prevents the hangup. This flag is provided to keep the
  119. // old behavior for commands where we need to read the output directly,
  120. // which is currently only used for detecting the interpreter version.
  121. // If we need to use the async monitor thread approach and preserve the stdout
  122. // contents in the future, a more complicated hack might be necessary.
  123. if( !aSaveOutput )
  124. {
  125. thread_pool& tp = GetKiCadThreadPool();
  126. auto ret = tp.submit( monitor, process );
  127. }
  128. }
  129. return pid;
  130. }
  131. wxString PYTHON_MANAGER::FindPythonInterpreter()
  132. {
  133. // First, attempt to use a Python we distribute with KiCad
  134. #if defined( __WINDOWS__ )
  135. wxFileName pythonExe = FindKicadFile( "pythonw.exe" );
  136. if( pythonExe.IsFileExecutable() )
  137. return pythonExe.GetFullPath();
  138. #elif defined( __WXMAC__ )
  139. wxFileName pythonExe( PATHS::GetOSXKicadDataDir(), wxEmptyString );
  140. pythonExe.RemoveLastDir();
  141. pythonExe.AppendDir( wxT( "Frameworks" ) );
  142. pythonExe.AppendDir( wxT( "Python.framework" ) );
  143. pythonExe.AppendDir( wxT( "Versions" ) );
  144. pythonExe.AppendDir( wxT( "Current" ) );
  145. pythonExe.AppendDir( wxT( "bin" ) );
  146. pythonExe.SetFullName(wxT( "python3" ) );
  147. if( pythonExe.IsFileExecutable() )
  148. return pythonExe.GetFullPath();
  149. #else
  150. wxFileName pythonExe;
  151. #endif
  152. // In case one is forced with cmake
  153. pythonExe.Assign( wxString::FromUTF8Unchecked( PYTHON_EXECUTABLE ) );
  154. if( pythonExe.IsFileExecutable() )
  155. return pythonExe.GetFullPath();
  156. // Fall back on finding any Python in the user's path
  157. #ifdef _WIN32
  158. wxArrayString output;
  159. if( 0 == wxExecute( wxS( "where pythonw.exe" ), output, wxEXEC_SYNC ) )
  160. {
  161. if( !output.IsEmpty() )
  162. return output[0];
  163. }
  164. #else
  165. wxArrayString output;
  166. if( 0 == wxExecute( wxS( "which -a python3" ), output, wxEXEC_SYNC ) )
  167. {
  168. if( !output.IsEmpty() )
  169. return output[0];
  170. }
  171. if( 0 == wxExecute( wxS( "which -a python" ), output, wxEXEC_SYNC ) )
  172. {
  173. if( !output.IsEmpty() )
  174. return output[0];
  175. }
  176. #endif
  177. return wxEmptyString;
  178. }
  179. std::optional<wxString> PYTHON_MANAGER::GetPythonEnvironment( const wxString& aNamespace )
  180. {
  181. wxFileName path( PATHS::GetUserCachePath(), wxEmptyString );
  182. path.AppendDir( wxS( "python-environments" ) );
  183. path.AppendDir( aNamespace );
  184. if( !PATHS::EnsurePathExists( path.GetPath() ) )
  185. return std::nullopt;
  186. return path.GetPath();
  187. }
  188. std::optional<wxString> PYTHON_MANAGER::GetVirtualPython( const wxString& aNamespace )
  189. {
  190. std::optional<wxString> envPath = GetPythonEnvironment( aNamespace );
  191. if( !envPath )
  192. return std::nullopt;
  193. wxFileName python( *envPath, wxEmptyString );
  194. #ifdef _WIN32
  195. python.AppendDir( "Scripts" );
  196. python.SetFullName( "pythonw.exe" );
  197. #else
  198. python.AppendDir( "bin" );
  199. python.SetFullName( "python" );
  200. #endif
  201. if( !python.IsFileExecutable() )
  202. return std::nullopt;
  203. return python.GetFullPath();
  204. }