kiwi.command
1# Copyright (c) 2015 SUSE Linux GmbH. All rights reserved. 2# 3# This file is part of kiwi. 4# 5# kiwi is free software: you can redistribute it and/or modify 6# it under the terms of the GNU General Public License as published by 7# the Free Software Foundation, either version 3 of the License, or 8# (at your option) any later version. 9# 10# kiwi is distributed in the hope that it will be useful, 11# but WITHOUT ANY WARRANTY; without even the implied warranty of 12# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13# GNU General Public License for more details. 14# 15# You should have received a copy of the GNU General Public License 16# along with kiwi. If not, see <http://www.gnu.org/licenses/> 17# 18from typing import IO, Callable, Literal, List, MutableMapping, NamedTuple, Optional, overload 19import logging 20import os 21import select 22import subprocess 23 24# project 25from kiwi.utils.codec import Codec 26 27from kiwi.exceptions import ( 28 KiwiCommandError, 29 KiwiCommandNotFound 30) 31 32log = logging.getLogger('kiwi') 33 34 35class CommandT(NamedTuple): 36 output: str 37 error: str 38 returncode: int 39 40 41class CommandCallT(NamedTuple): 42 output: IO[bytes] 43 output_available: Callable[[], bool] 44 error: IO[bytes] 45 error_available: Callable[[], bool] 46 process: subprocess.Popen 47 48 49class Command: 50 """ 51 **Implements command invocation** 52 53 An instance of Command provides methods to invoke external 54 commands in blocking and non blocking mode. Control of 55 stdout and stderr is given to the caller 56 """ 57 58 @overload 59 @staticmethod 60 def run( 61 command: List[str], custom_env: Optional[MutableMapping[str, str]] = None, 62 raise_on_error: bool = True, stderr_to_stdout: bool = False, 63 raise_on_command_not_found: Literal[False] = False 64 ) -> CommandT: 65 ... # pragma: no cover 66 67 @overload 68 @staticmethod 69 def run( 70 command: List[str], custom_env: Optional[MutableMapping[str, str]] = None, 71 raise_on_error: bool = True, stderr_to_stdout: bool = False, 72 raise_on_command_not_found: bool = True 73 ) -> Optional[CommandT]: 74 ... # pragma: no cover 75 76 @staticmethod 77 def run( 78 command: List[str], custom_env: Optional[MutableMapping[str, str]] = None, 79 raise_on_error: bool = True, stderr_to_stdout: bool = False, 80 raise_on_command_not_found: bool = True 81 ) -> Optional[CommandT]: 82 """ 83 Execute a program and block the caller. The return value 84 is a CommandT namedtuple containing the stdout, stderr 85 and return code information. Unless raise_on_error is 86 set to `False` an exception is thrown if the command 87 exits with an error code not equal to zero. If 88 raise_on_command_not_found is `False` and the command is 89 not found, then `None` is returned. 90 91 Example: 92 93 .. code:: python 94 95 result = Command.run(['ls', '-l']) 96 97 :param list command: command and arguments 98 :param dict custom_env: custom os.environ 99 :param bool raise_on_error: control error behaviour 100 :param bool stderr_to_stdout: redirects stderr to stdout 101 102 :return: 103 Contains call results in command type 104 105 .. code:: python 106 107 CommandT(output='string', error='string', returncode=int) 108 109 :rtype: CommandT 110 """ 111 from .path import Path 112 environment = custom_env or os.environ 113 cmd_abspath: Optional[str] 114 if command[0].startswith("/"): 115 cmd_abspath = command[0] 116 if not os.path.exists(cmd_abspath): 117 cmd_abspath = None 118 else: 119 cmd_abspath = Path.which( 120 command[0], custom_env=environment, access_mode=os.X_OK 121 ) 122 123 if not cmd_abspath: 124 message = f'Command "{command[0]}" not found in the environment' 125 if raise_on_command_not_found: 126 raise KiwiCommandNotFound(message) 127 log.debug('EXEC: %s', message) 128 return None 129 stderr = subprocess.STDOUT if stderr_to_stdout else subprocess.PIPE 130 log.debug('EXEC: [%s]', ' '.join(command)) 131 try: 132 process = subprocess.Popen( 133 [cmd_abspath] + command[1:], 134 stdout=subprocess.PIPE, 135 stderr=stderr, 136 env=environment 137 ) 138 except (OSError, subprocess.SubprocessError) as e: 139 raise KiwiCommandError( 140 f'{command[0]}: {type(e).__name__}: {format(e)}' 141 ) from e 142 143 output, error = process.communicate() 144 if process.returncode != 0 and raise_on_error: 145 if not error: 146 error = bytes(b'(no output on stderr)') 147 if not output: 148 output = bytes(b'(no output on stdout)') 149 log.debug( 150 'EXEC: Failed with stderr: {0}, stdout: {1}'.format( 151 Codec.decode(error), Codec.decode(output) 152 ) 153 ) 154 raise KiwiCommandError( 155 '{0}: stderr: {1}, stdout: {2}'.format( 156 command[0], Codec.decode(error), Codec.decode(output) 157 ) 158 ) 159 return CommandT( 160 output=Codec.decode(output), 161 error=Codec.decode(error), 162 returncode=process.returncode 163 ) 164 165 @staticmethod 166 def call( 167 command: List[str], 168 custom_env: Optional[MutableMapping[str, str]] = None 169 ) -> CommandCallT: 170 """ 171 Execute a program and return an io file handle pair back. 172 stdout and stderr are both on different channels. The caller 173 must read from the output file handles in order to actually 174 run the command. This can be done using the CommandIterator 175 from command_process 176 177 Example: 178 179 .. code:: python 180 181 process = Command.call(['ls', '-l']) 182 183 :param list command: command and arguments 184 :param list custom_env: custom os.environ 185 186 :return: 187 Contains process results in command type 188 189 .. code:: python 190 191 command( 192 output='string', output_available=bool, 193 error='string', error_available=bool, 194 process=subprocess 195 ) 196 197 :rtype: namedtuple 198 """ 199 from .path import Path 200 log.debug('EXEC: [%s]', ' '.join(command)) 201 environment = custom_env or os.environ 202 if not Path.which( 203 command[0], custom_env=environment, access_mode=os.X_OK 204 ): 205 raise KiwiCommandNotFound( 206 f'Command "{command[0]}" not found in the environment' 207 ) 208 try: 209 process = subprocess.Popen( 210 command, 211 stdout=subprocess.PIPE, 212 stderr=subprocess.PIPE, 213 env=environment 214 ) 215 except Exception as e: 216 raise KiwiCommandError( 217 f'{type(e).__name__}: {format(e)}' 218 ) from e 219 220 # guaranteed to be true as stdout & stderr equal subprocess.PIPE 221 assert process.stdout and process.stderr 222 223 def output_available() -> Callable[[], bool]: 224 def _select(): 225 readable, _, exceptional = select.select( 226 [process.stdout], [], [process.stdout], 1e-4 227 ) 228 if readable and not exceptional: 229 return True 230 return False 231 return _select 232 233 def error_available() -> Callable[[], bool]: 234 def _select(): 235 readable, _, exceptional = select.select( 236 [process.stderr], [], [process.stderr], 1e-4 237 ) 238 if readable and not exceptional: 239 return True 240 return False 241 return _select 242 243 return CommandCallT( 244 output=process.stdout, 245 output_available=output_available(), 246 error=process.stderr, 247 error_available=error_available(), 248 process=process 249 )
CommandT(output, error, returncode)
42class CommandCallT(NamedTuple): 43 output: IO[bytes] 44 output_available: Callable[[], bool] 45 error: IO[bytes] 46 error_available: Callable[[], bool] 47 process: subprocess.Popen
CommandCallT(output, output_available, error, error_available, process)
50class Command: 51 """ 52 **Implements command invocation** 53 54 An instance of Command provides methods to invoke external 55 commands in blocking and non blocking mode. Control of 56 stdout and stderr is given to the caller 57 """ 58 59 @overload 60 @staticmethod 61 def run( 62 command: List[str], custom_env: Optional[MutableMapping[str, str]] = None, 63 raise_on_error: bool = True, stderr_to_stdout: bool = False, 64 raise_on_command_not_found: Literal[False] = False 65 ) -> CommandT: 66 ... # pragma: no cover 67 68 @overload 69 @staticmethod 70 def run( 71 command: List[str], custom_env: Optional[MutableMapping[str, str]] = None, 72 raise_on_error: bool = True, stderr_to_stdout: bool = False, 73 raise_on_command_not_found: bool = True 74 ) -> Optional[CommandT]: 75 ... # pragma: no cover 76 77 @staticmethod 78 def run( 79 command: List[str], custom_env: Optional[MutableMapping[str, str]] = None, 80 raise_on_error: bool = True, stderr_to_stdout: bool = False, 81 raise_on_command_not_found: bool = True 82 ) -> Optional[CommandT]: 83 """ 84 Execute a program and block the caller. The return value 85 is a CommandT namedtuple containing the stdout, stderr 86 and return code information. Unless raise_on_error is 87 set to `False` an exception is thrown if the command 88 exits with an error code not equal to zero. If 89 raise_on_command_not_found is `False` and the command is 90 not found, then `None` is returned. 91 92 Example: 93 94 .. code:: python 95 96 result = Command.run(['ls', '-l']) 97 98 :param list command: command and arguments 99 :param dict custom_env: custom os.environ 100 :param bool raise_on_error: control error behaviour 101 :param bool stderr_to_stdout: redirects stderr to stdout 102 103 :return: 104 Contains call results in command type 105 106 .. code:: python 107 108 CommandT(output='string', error='string', returncode=int) 109 110 :rtype: CommandT 111 """ 112 from .path import Path 113 environment = custom_env or os.environ 114 cmd_abspath: Optional[str] 115 if command[0].startswith("/"): 116 cmd_abspath = command[0] 117 if not os.path.exists(cmd_abspath): 118 cmd_abspath = None 119 else: 120 cmd_abspath = Path.which( 121 command[0], custom_env=environment, access_mode=os.X_OK 122 ) 123 124 if not cmd_abspath: 125 message = f'Command "{command[0]}" not found in the environment' 126 if raise_on_command_not_found: 127 raise KiwiCommandNotFound(message) 128 log.debug('EXEC: %s', message) 129 return None 130 stderr = subprocess.STDOUT if stderr_to_stdout else subprocess.PIPE 131 log.debug('EXEC: [%s]', ' '.join(command)) 132 try: 133 process = subprocess.Popen( 134 [cmd_abspath] + command[1:], 135 stdout=subprocess.PIPE, 136 stderr=stderr, 137 env=environment 138 ) 139 except (OSError, subprocess.SubprocessError) as e: 140 raise KiwiCommandError( 141 f'{command[0]}: {type(e).__name__}: {format(e)}' 142 ) from e 143 144 output, error = process.communicate() 145 if process.returncode != 0 and raise_on_error: 146 if not error: 147 error = bytes(b'(no output on stderr)') 148 if not output: 149 output = bytes(b'(no output on stdout)') 150 log.debug( 151 'EXEC: Failed with stderr: {0}, stdout: {1}'.format( 152 Codec.decode(error), Codec.decode(output) 153 ) 154 ) 155 raise KiwiCommandError( 156 '{0}: stderr: {1}, stdout: {2}'.format( 157 command[0], Codec.decode(error), Codec.decode(output) 158 ) 159 ) 160 return CommandT( 161 output=Codec.decode(output), 162 error=Codec.decode(error), 163 returncode=process.returncode 164 ) 165 166 @staticmethod 167 def call( 168 command: List[str], 169 custom_env: Optional[MutableMapping[str, str]] = None 170 ) -> CommandCallT: 171 """ 172 Execute a program and return an io file handle pair back. 173 stdout and stderr are both on different channels. The caller 174 must read from the output file handles in order to actually 175 run the command. This can be done using the CommandIterator 176 from command_process 177 178 Example: 179 180 .. code:: python 181 182 process = Command.call(['ls', '-l']) 183 184 :param list command: command and arguments 185 :param list custom_env: custom os.environ 186 187 :return: 188 Contains process results in command type 189 190 .. code:: python 191 192 command( 193 output='string', output_available=bool, 194 error='string', error_available=bool, 195 process=subprocess 196 ) 197 198 :rtype: namedtuple 199 """ 200 from .path import Path 201 log.debug('EXEC: [%s]', ' '.join(command)) 202 environment = custom_env or os.environ 203 if not Path.which( 204 command[0], custom_env=environment, access_mode=os.X_OK 205 ): 206 raise KiwiCommandNotFound( 207 f'Command "{command[0]}" not found in the environment' 208 ) 209 try: 210 process = subprocess.Popen( 211 command, 212 stdout=subprocess.PIPE, 213 stderr=subprocess.PIPE, 214 env=environment 215 ) 216 except Exception as e: 217 raise KiwiCommandError( 218 f'{type(e).__name__}: {format(e)}' 219 ) from e 220 221 # guaranteed to be true as stdout & stderr equal subprocess.PIPE 222 assert process.stdout and process.stderr 223 224 def output_available() -> Callable[[], bool]: 225 def _select(): 226 readable, _, exceptional = select.select( 227 [process.stdout], [], [process.stdout], 1e-4 228 ) 229 if readable and not exceptional: 230 return True 231 return False 232 return _select 233 234 def error_available() -> Callable[[], bool]: 235 def _select(): 236 readable, _, exceptional = select.select( 237 [process.stderr], [], [process.stderr], 1e-4 238 ) 239 if readable and not exceptional: 240 return True 241 return False 242 return _select 243 244 return CommandCallT( 245 output=process.stdout, 246 output_available=output_available(), 247 error=process.stderr, 248 error_available=error_available(), 249 process=process 250 )
Implements command invocation
An instance of Command provides methods to invoke external commands in blocking and non blocking mode. Control of stdout and stderr is given to the caller
77 @staticmethod 78 def run( 79 command: List[str], custom_env: Optional[MutableMapping[str, str]] = None, 80 raise_on_error: bool = True, stderr_to_stdout: bool = False, 81 raise_on_command_not_found: bool = True 82 ) -> Optional[CommandT]: 83 """ 84 Execute a program and block the caller. The return value 85 is a CommandT namedtuple containing the stdout, stderr 86 and return code information. Unless raise_on_error is 87 set to `False` an exception is thrown if the command 88 exits with an error code not equal to zero. If 89 raise_on_command_not_found is `False` and the command is 90 not found, then `None` is returned. 91 92 Example: 93 94 .. code:: python 95 96 result = Command.run(['ls', '-l']) 97 98 :param list command: command and arguments 99 :param dict custom_env: custom os.environ 100 :param bool raise_on_error: control error behaviour 101 :param bool stderr_to_stdout: redirects stderr to stdout 102 103 :return: 104 Contains call results in command type 105 106 .. code:: python 107 108 CommandT(output='string', error='string', returncode=int) 109 110 :rtype: CommandT 111 """ 112 from .path import Path 113 environment = custom_env or os.environ 114 cmd_abspath: Optional[str] 115 if command[0].startswith("/"): 116 cmd_abspath = command[0] 117 if not os.path.exists(cmd_abspath): 118 cmd_abspath = None 119 else: 120 cmd_abspath = Path.which( 121 command[0], custom_env=environment, access_mode=os.X_OK 122 ) 123 124 if not cmd_abspath: 125 message = f'Command "{command[0]}" not found in the environment' 126 if raise_on_command_not_found: 127 raise KiwiCommandNotFound(message) 128 log.debug('EXEC: %s', message) 129 return None 130 stderr = subprocess.STDOUT if stderr_to_stdout else subprocess.PIPE 131 log.debug('EXEC: [%s]', ' '.join(command)) 132 try: 133 process = subprocess.Popen( 134 [cmd_abspath] + command[1:], 135 stdout=subprocess.PIPE, 136 stderr=stderr, 137 env=environment 138 ) 139 except (OSError, subprocess.SubprocessError) as e: 140 raise KiwiCommandError( 141 f'{command[0]}: {type(e).__name__}: {format(e)}' 142 ) from e 143 144 output, error = process.communicate() 145 if process.returncode != 0 and raise_on_error: 146 if not error: 147 error = bytes(b'(no output on stderr)') 148 if not output: 149 output = bytes(b'(no output on stdout)') 150 log.debug( 151 'EXEC: Failed with stderr: {0}, stdout: {1}'.format( 152 Codec.decode(error), Codec.decode(output) 153 ) 154 ) 155 raise KiwiCommandError( 156 '{0}: stderr: {1}, stdout: {2}'.format( 157 command[0], Codec.decode(error), Codec.decode(output) 158 ) 159 ) 160 return CommandT( 161 output=Codec.decode(output), 162 error=Codec.decode(error), 163 returncode=process.returncode 164 )
Execute a program and block the caller. The return value
is a CommandT namedtuple containing the stdout, stderr
and return code information. Unless raise_on_error is
set to False an exception is thrown if the command
exits with an error code not equal to zero. If
raise_on_command_not_found is False and the command is
not found, then None is returned.
Example:
.. code:: python
result = Command.run(['ls', '-l'])
Parameters
- list command: command and arguments
- dict custom_env: custom os.environ
- bool raise_on_error: control error behaviour
- bool stderr_to_stdout: redirects stderr to stdout
Returns
Contains call results in command type .. code:: python CommandT(output='string', error='string', returncode=int)
166 @staticmethod 167 def call( 168 command: List[str], 169 custom_env: Optional[MutableMapping[str, str]] = None 170 ) -> CommandCallT: 171 """ 172 Execute a program and return an io file handle pair back. 173 stdout and stderr are both on different channels. The caller 174 must read from the output file handles in order to actually 175 run the command. This can be done using the CommandIterator 176 from command_process 177 178 Example: 179 180 .. code:: python 181 182 process = Command.call(['ls', '-l']) 183 184 :param list command: command and arguments 185 :param list custom_env: custom os.environ 186 187 :return: 188 Contains process results in command type 189 190 .. code:: python 191 192 command( 193 output='string', output_available=bool, 194 error='string', error_available=bool, 195 process=subprocess 196 ) 197 198 :rtype: namedtuple 199 """ 200 from .path import Path 201 log.debug('EXEC: [%s]', ' '.join(command)) 202 environment = custom_env or os.environ 203 if not Path.which( 204 command[0], custom_env=environment, access_mode=os.X_OK 205 ): 206 raise KiwiCommandNotFound( 207 f'Command "{command[0]}" not found in the environment' 208 ) 209 try: 210 process = subprocess.Popen( 211 command, 212 stdout=subprocess.PIPE, 213 stderr=subprocess.PIPE, 214 env=environment 215 ) 216 except Exception as e: 217 raise KiwiCommandError( 218 f'{type(e).__name__}: {format(e)}' 219 ) from e 220 221 # guaranteed to be true as stdout & stderr equal subprocess.PIPE 222 assert process.stdout and process.stderr 223 224 def output_available() -> Callable[[], bool]: 225 def _select(): 226 readable, _, exceptional = select.select( 227 [process.stdout], [], [process.stdout], 1e-4 228 ) 229 if readable and not exceptional: 230 return True 231 return False 232 return _select 233 234 def error_available() -> Callable[[], bool]: 235 def _select(): 236 readable, _, exceptional = select.select( 237 [process.stderr], [], [process.stderr], 1e-4 238 ) 239 if readable and not exceptional: 240 return True 241 return False 242 return _select 243 244 return CommandCallT( 245 output=process.stdout, 246 output_available=output_available(), 247 error=process.stderr, 248 error_available=error_available(), 249 process=process 250 )
Execute a program and return an io file handle pair back. stdout and stderr are both on different channels. The caller must read from the output file handles in order to actually run the command. This can be done using the CommandIterator from command_process
Example:
.. code:: python
process = Command.call(['ls', '-l'])
Parameters
- list command: command and arguments
- list custom_env: custom os.environ
Returns
Contains process results in command type .. code:: python command( output='string', output_available=bool, error='string', error_available=bool, process=subprocess )