|
7 | 7 | import traceback |
8 | 8 | import shutil # For _test_command_availability and _scan_system_applications |
9 | 9 | import os # For _scan_system_applications |
| 10 | +import subprocess |
10 | 11 |
|
11 | 12 | # Module-level constant for default applications to scan |
12 | 13 | DEFAULT_SCAN_APPLICATIONS = [ |
@@ -1170,84 +1171,115 @@ def _test_command_availability(command): |
1170 | 1171 | try: |
1171 | 1172 | if not command or not command.strip(): |
1172 | 1173 | return False |
1173 | | - |
1174 | | - # Extract base command (first word) |
| 1174 | + |
1175 | 1175 | command_parts = command.strip().split() |
1176 | 1176 | if not command_parts: |
1177 | 1177 | return False |
1178 | 1178 |
|
1179 | 1179 | base_command = command_parts[0] |
1180 | | - current_app.logger.debug(f"Testing command availability: '{base_command}'") |
1181 | | - |
1182 | | - # First check with shutil.which |
1183 | | - command_path = shutil.which(base_command) |
| 1180 | + current_app.logger.debug(f"Testing command availability for: '{base_command}' (full command: '{command}')") |
| 1181 | + |
| 1182 | + command_path = None |
1184 | 1183 |
|
| 1184 | + # Try to find command using 'command -v' |
| 1185 | + try: |
| 1186 | + current_app.logger.debug(f"Attempting to find '{base_command}' using 'command -v'") |
| 1187 | + command_v_result = subprocess.run(['command', '-v', base_command], |
| 1188 | + capture_output=True, text=True, check=False, timeout=5) |
| 1189 | + if command_v_result.returncode == 0 and command_v_result.stdout.strip(): |
| 1190 | + command_path = command_v_result.stdout.strip() |
| 1191 | + current_app.logger.info(f"Command '{base_command}' found using 'command -v': {command_path}") |
| 1192 | + else: |
| 1193 | + current_app.logger.debug(f"'command -v {base_command}' failed or returned empty. RC: {command_v_result.returncode}, Stdout: '{command_v_result.stdout.strip()}'") |
| 1194 | + except FileNotFoundError: |
| 1195 | + current_app.logger.warning("Utility 'command' not found. Falling back to shutil.which for command lookup.") |
| 1196 | + command_path = None # Ensure fallback to shutil.which occurs |
| 1197 | + except subprocess.TimeoutExpired: |
| 1198 | + current_app.logger.warning(f"'command -v {base_command}' timed out.") |
| 1199 | + except Exception as e: # General exception for other subprocess errors |
| 1200 | + current_app.logger.warning(f"Error running 'command -v {base_command}': {e}") |
| 1201 | + |
| 1202 | + # If 'command -v' fails (or 'command' utility not found), try shutil.which |
| 1203 | + if not command_path: |
| 1204 | + current_app.logger.debug(f"Attempting to find '{base_command}' using 'shutil.which'") |
| 1205 | + command_path_shutil = shutil.which(base_command) |
| 1206 | + if command_path_shutil: |
| 1207 | + command_path = command_path_shutil |
| 1208 | + current_app.logger.info(f"Command '{base_command}' found using 'shutil.which': {command_path}") |
| 1209 | + else: |
| 1210 | + current_app.logger.debug(f"'shutil.which({base_command})' did not find the command.") |
| 1211 | + |
1185 | 1212 | if command_path: |
1186 | | - current_app.logger.debug(f"Command '{base_command}' found at: {command_path}") |
| 1213 | + current_app.logger.debug(f"Command '{base_command}' resolved to path: {command_path}") |
1187 | 1214 |
|
1188 | 1215 | # Additional checks for the found command |
1189 | 1216 | try: |
1190 | 1217 | # Check if it's actually executable |
1191 | 1218 | if not os.access(command_path, os.X_OK): |
1192 | | - current_app.logger.debug(f"Command '{base_command}' found but not executable") |
| 1219 | + current_app.logger.warning(f"Command path '{command_path}' for '{base_command}' found but not executable.") |
1193 | 1220 | return False |
1194 | 1221 |
|
| 1222 | + current_app.logger.debug(f"Command path '{command_path}' is executable. Checking file type.") |
1195 | 1223 | # Get file information |
1196 | 1224 | try: |
1197 | | - file_info = subprocess.run(['file', command_path], |
1198 | | - capture_output=True, text=True, timeout=5) |
1199 | | - if file_info.returncode == 0: |
1200 | | - file_type = file_info.stdout.strip().lower() |
1201 | | - current_app.logger.debug(f"File type for '{base_command}': {file_type}") |
| 1225 | + # Run 'file' command without check=True, handle errors manually |
| 1226 | + file_info_process = subprocess.run(['file', command_path], |
| 1227 | + capture_output=True, text=True, timeout=5, check=False) |
| 1228 | + |
| 1229 | + if file_info_process.returncode != 0: |
| 1230 | + current_app.logger.warning(f"'file {command_path}' failed with RC {file_info_process.returncode}: {file_info_process.stderr.strip()}") |
| 1231 | + # If 'file' command fails, we might still consider it available if it's executable |
| 1232 | + # but log a warning. This behavior is similar to previous logic. |
| 1233 | + return True |
| 1234 | + |
| 1235 | + file_type = file_info_process.stdout.strip().lower() |
| 1236 | + current_app.logger.debug(f"File type for '{command_path}': {file_type}") |
1202 | 1237 |
|
1203 | | - # Check for common problematic file types |
1204 | | - if 'cannot execute binary file' in file_type: |
1205 | | - current_app.logger.debug(f"Binary file architecture mismatch: {base_command}") |
1206 | | - return False |
| 1238 | + if 'cannot execute binary file' in file_type: |
| 1239 | + current_app.logger.warning(f"Binary file architecture mismatch for '{command_path}': {file_type}") |
| 1240 | + return False |
1207 | 1241 |
|
1208 | | - # For scripts, check if they have proper shebang or interpreter |
1209 | | - if 'script' in file_type or command_path.endswith(('.sh', '.py', '.pl', '.rb')): |
1210 | | - current_app.logger.debug(f"Detected script: {base_command}") |
1211 | | - |
1212 | | - # For shell scripts, check if they have shebang |
1213 | | - if command_path.endswith('.sh'): |
1214 | | - try: |
1215 | | - with open(command_path, 'r', encoding='utf-8', errors='ignore') as f: |
1216 | | - first_line = f.readline().strip() |
1217 | | - if not first_line.startswith('#!'): |
1218 | | - current_app.logger.debug(f"Shell script without shebang: {base_command}") |
1219 | | - # Still consider it available, bash can handle it |
1220 | | - except: |
1221 | | - pass |
1222 | | - |
1223 | | - return True |
| 1242 | + if 'script' in file_type or command_path.endswith(('.sh', '.py', '.pl', '.rb')): |
| 1243 | + current_app.logger.debug(f"Detected script: '{command_path}'") |
| 1244 | + if command_path.endswith('.sh'): |
| 1245 | + try: |
| 1246 | + with open(command_path, 'r', encoding='utf-8', errors='ignore') as f: |
| 1247 | + first_line = f.readline().strip() |
| 1248 | + if not first_line.startswith('#!'): |
| 1249 | + current_app.logger.debug(f"Shell script '{command_path}' without shebang. Assuming bash can handle.") |
| 1250 | + except Exception as e_script: |
| 1251 | + current_app.logger.warning(f"Could not read script '{command_path}' to check shebang: {e_script}") |
| 1252 | + return True |
1224 | 1253 |
|
1225 | | - # For Java applications |
1226 | | - if 'java' in file_type or command_path.endswith('.jar'): |
1227 | | - current_app.logger.debug(f"Detected Java application: {base_command}") |
1228 | | - # Check if java is available |
1229 | | - if shutil.which('java'): |
1230 | | - return True |
1231 | | - else: |
1232 | | - current_app.logger.debug(f"Java application found but java runtime not available") |
1233 | | - return False |
| 1254 | + if 'java' in file_type or command_path.endswith('.jar'): |
| 1255 | + current_app.logger.debug(f"Detected Java application: '{command_path}'") |
| 1256 | + if shutil.which('java'): |
| 1257 | + return True |
| 1258 | + else: |
| 1259 | + current_app.logger.warning(f"Java application '{command_path}' found, but 'java' runtime is not available in PATH.") |
| 1260 | + return False |
1234 | 1261 |
|
1235 | | - return True |
| 1262 | + return True # Default to available if file type is recognized and not problematic |
1236 | 1263 |
|
1237 | 1264 | except subprocess.TimeoutExpired: |
1238 | | - current_app.logger.debug(f"File command timeout for: {base_command}") |
1239 | | - # If file command times out, assume it's available if other checks passed |
1240 | | - return True |
1241 | | - except Exception as e: |
1242 | | - current_app.logger.debug(f"Error running file command: {e}") |
1243 | | - # If we can't get file info, assume it's available if other checks passed |
| 1265 | + current_app.logger.warning(f"'file {command_path}' timed out. Assuming available as it's executable.") |
| 1266 | + return True # If file command times out, assume it's available |
| 1267 | + except FileNotFoundError: |
| 1268 | + current_app.logger.warning(f"Utility 'file' not found. Skipping file type check for {command_path}.") |
| 1269 | + return True # Proceed as if file check was inconclusive but command is executable |
| 1270 | + except Exception as e_file: |
| 1271 | + current_app.logger.error(f"Error running 'file {command_path}': {e_file}", exc_info=True) |
| 1272 | + # If 'file' command has other issues, but path was found and executable, cautiously return True |
1244 | 1273 | return True |
1245 | 1274 |
|
1246 | | - except Exception as e: |
1247 | | - current_app.logger.debug(f"Error checking executable status: {e}") |
1248 | | - return False |
1249 | | - |
1250 | | - # If not found with which, try absolute path |
| 1275 | + except Exception as e_access: |
| 1276 | + current_app.logger.error(f"Error during access or file checks for '{command_path}': {e_access}", exc_info=True) |
| 1277 | + return False # If any other unexpected error occurs during these checks |
| 1278 | + else: |
| 1279 | + # This block is reached if both 'command -v' and 'shutil.which' failed |
| 1280 | + current_app.logger.warning(f"Command '{base_command}' not found using 'command -v' or 'shutil.which'.") |
| 1281 | + |
| 1282 | + # If not found with 'command -v' or 'shutil.which', try absolute path (existing logic) |
1251 | 1283 | if os.path.isabs(base_command): |
1252 | 1284 | current_app.logger.debug(f"Checking absolute path: {base_command}") |
1253 | 1285 | if os.path.exists(base_command): |
|
0 commit comments