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.

244 lines
7.4 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. void PYTHON_MANAGER::Execute( const 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. #ifdef __WXMAC__
  99. wxString cmd = wxString::Format( wxS( "open -a %s --args %s" ), m_interpreterPath, aArgs );
  100. #else
  101. wxString cmd = wxString::Format( wxS( "%s %s" ), m_interpreterPath, aArgs );
  102. #endif
  103. long pid = wxExecute( cmd, wxEXEC_ASYNC, process, aEnv );
  104. if( pid == 0 )
  105. {
  106. delete process;
  107. aCallback( -1, wxEmptyString, _( "Process could not be created" ) );
  108. }
  109. else
  110. {
  111. // On Windows, if there is a lot of stdout written by the process, this can
  112. // hang up the wxProcess such that it will never call OnTerminate. To work
  113. // around this, we use this monitor thread to just dump the stdout to the
  114. // trace log, which prevents the hangup. This flag is provided to keep the
  115. // old behavior for commands where we need to read the output directly,
  116. // which is currently only used for detecting the interpreter version.
  117. // If we need to use the async monitor thread approach and preserve the stdout
  118. // contents in the future, a more complicated hack might be necessary.
  119. if( !aSaveOutput )
  120. {
  121. thread_pool& tp = GetKiCadThreadPool();
  122. auto ret = tp.submit( monitor, process );
  123. }
  124. }
  125. }
  126. wxString PYTHON_MANAGER::FindPythonInterpreter()
  127. {
  128. // First, attempt to use a Python we distribute with KiCad
  129. #if defined( __WINDOWS__ )
  130. wxFileName pythonExe = FindKicadFile( "pythonw.exe" );
  131. if( pythonExe.IsFileExecutable() )
  132. return pythonExe.GetFullPath();
  133. #elif defined( __WXMAC__ )
  134. wxFileName pythonExe( PATHS::GetOSXKicadDataDir(), wxEmptyString );
  135. pythonExe.RemoveLastDir();
  136. pythonExe.AppendDir( wxT( "Frameworks" ) );
  137. pythonExe.AppendDir( wxT( "Python.framework" ) );
  138. pythonExe.AppendDir( wxT( "Versions" ) );
  139. pythonExe.AppendDir( wxT( "Current" ) );
  140. pythonExe.AppendDir( wxT( "bin" ) );
  141. pythonExe.SetFullName(wxT( "python3" ) );
  142. if( pythonExe.IsFileExecutable() )
  143. return pythonExe.GetFullPath();
  144. #else
  145. wxFileName pythonExe;
  146. #endif
  147. // In case one is forced with cmake
  148. pythonExe.Assign( wxString::FromUTF8Unchecked( PYTHON_EXECUTABLE ) );
  149. if( pythonExe.IsFileExecutable() )
  150. return pythonExe.GetFullPath();
  151. // Fall back on finding any Python in the user's path
  152. #ifdef _WIN32
  153. wxArrayString output;
  154. if( 0 == wxExecute( wxS( "where pythonw.exe" ), output, wxEXEC_SYNC ) )
  155. {
  156. if( !output.IsEmpty() )
  157. return output[0];
  158. }
  159. #else
  160. wxArrayString output;
  161. if( 0 == wxExecute( wxS( "which -a python3" ), output, wxEXEC_SYNC ) )
  162. {
  163. if( !output.IsEmpty() )
  164. return output[0];
  165. }
  166. if( 0 == wxExecute( wxS( "which -a python" ), output, wxEXEC_SYNC ) )
  167. {
  168. if( !output.IsEmpty() )
  169. return output[0];
  170. }
  171. #endif
  172. return wxEmptyString;
  173. }
  174. std::optional<wxString> PYTHON_MANAGER::GetPythonEnvironment( const wxString& aNamespace )
  175. {
  176. wxFileName path( PATHS::GetUserCachePath(), wxEmptyString );
  177. path.AppendDir( wxS( "python-environments" ) );
  178. path.AppendDir( aNamespace );
  179. if( !PATHS::EnsurePathExists( path.GetPath() ) )
  180. return std::nullopt;
  181. return path.GetPath();
  182. }
  183. std::optional<wxString> PYTHON_MANAGER::GetVirtualPython( const wxString& aNamespace )
  184. {
  185. std::optional<wxString> envPath = GetPythonEnvironment( aNamespace );
  186. if( !envPath )
  187. return std::nullopt;
  188. wxFileName python( *envPath, wxEmptyString );
  189. #ifdef _WIN32
  190. python.AppendDir( "Scripts" );
  191. python.SetFullName( "pythonw.exe" );
  192. #else
  193. python.AppendDir( "bin" );
  194. python.SetFullName( "python" );
  195. #endif
  196. if( !python.IsFileExecutable() )
  197. return std::nullopt;
  198. return python.GetFullPath();
  199. }