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.

595 lines
20 KiB

  1. # vim: fileencoding=utf-8
  2. #
  3. from subprocess import Popen, PIPE, STDOUT
  4. from typing import Union, List
  5. from io import TextIOWrapper
  6. from os import path
  7. from .tools import GenericFS, get_traceback_caller
  8. from glob import glob
  9. import os
  10. import sys
  11. import contextlib
  12. import re
  13. class FilesError(Exception):
  14. pass
  15. class PipeProcess():
  16. def _get_stdout(self):
  17. return PIPE
  18. def close(self):
  19. pass
  20. @property
  21. def shell_command(self):
  22. return ''
  23. class KeyboardInputProcess():
  24. def _get_stdout(self):
  25. return None
  26. def close(self):
  27. pass
  28. @property
  29. def shell_command(self):
  30. return ''
  31. class Process:
  32. '''Класс-обертка для работы с процессами.'''
  33. STDOUT = STDOUT
  34. PIPE = PIPE
  35. def __init__(self, command, *parameters, **kwargs):
  36. if 'stdin' not in kwargs:
  37. self._stdin = PipeProcess()
  38. elif kwargs['stdin'] == PIPE:
  39. self._stdin = PipeProcess()
  40. elif kwargs['stdin'] is None:
  41. self._stdin = KeyboardInputProcess()
  42. else:
  43. self._stdin = kwargs['stdin']
  44. self._stdout = kwargs.get('stdout', PIPE)
  45. self._stderr = kwargs.get('stderr', PIPE)
  46. self._envdict = kwargs.get('envdict', os.environ.copy())
  47. self._envdict['LANG'] = kwargs.get('lang', 'C')
  48. self._timeout = kwargs.get('timeout', None)
  49. self._cwd = kwargs.get('cwd', None)
  50. self._command = get_program_path(command)
  51. if not self._command:
  52. raise FilesError("command not found '{}'".format(command))
  53. self._command = [self._command, *parameters]
  54. self._process = None
  55. self._iterator = iter([])
  56. # Флаги.
  57. self._opened = False
  58. self._is_read = False
  59. self._readable = False
  60. self._writable = False
  61. self._readable_errors = False
  62. # I/O обработчики.
  63. self.stdin_handler = None
  64. self.stdout_handler = None
  65. self.stderr_handler = None
  66. # Кэши.
  67. self._output_cache = ''
  68. self._error_cache = ''
  69. def _get_stdout(self):
  70. self._open_process()
  71. return self.stdout_handler
  72. def _get_stdin(self):
  73. return self.stdin_handler
  74. def _open_process(self):
  75. '''Метод для открытия процесса.'''
  76. try:
  77. piped_stdin = self._stdin._get_stdout()
  78. self._process = Popen(self._command,
  79. stdout=self._stdout,
  80. stdin=piped_stdin,
  81. stderr=self._stderr,
  82. cwd=self._cwd,
  83. close_fds=True,
  84. env=self._envdict)
  85. if piped_stdin == PIPE:
  86. self.stdin_handler = TextIOWrapper(self._process.stdin,
  87. encoding='utf8')
  88. self._writable = True
  89. elif piped_stdin is not None:
  90. self.stdin_handler = self._stdin._get_stdin()
  91. self._writable = True
  92. if self._stdout == PIPE:
  93. self.stdout_handler = TextIOWrapper(self._process.stdout,
  94. encoding='utf8')
  95. self._readable = True
  96. if self._stderr == PIPE:
  97. self.stderr_handler = TextIOWrapper(self._process.stderr,
  98. encoding='utf8')
  99. self._readable_errors = True
  100. self._opened = True
  101. except Exception as error:
  102. raise FilesError(f'Can not open process. Reason: {error}')
  103. def close(self):
  104. '''Метод для закрытия процесса.'''
  105. if self._opened:
  106. if self._process.stdin:
  107. self.stdin_handler.close()
  108. self._stdin.close()
  109. self._opened = False
  110. def write(self, data):
  111. '''Метод для записи данных в stdin процесса.'''
  112. if not self._opened:
  113. self._open_process()
  114. self._is_read = False
  115. self._output_cache = ''
  116. try:
  117. if self._writable:
  118. self.stdin_handler.write(data)
  119. self.stdin_handler.flush()
  120. else:
  121. raise FilesError('Process stdin is not writable.')
  122. except IOError as error:
  123. raise FilesError(str(error))
  124. def read(self):
  125. '''Метод для чтения данных из stdout процесса.'''
  126. if not self._opened and not self._writable:
  127. self._open_process()
  128. if not self._readable:
  129. raise FilesError('Process is not readable.')
  130. try:
  131. if not self._is_read:
  132. if self._writable:
  133. self.close()
  134. if self._readable:
  135. self._output_cache = self.stdout_handler.read()
  136. if self._readable_errors:
  137. self._error_cache = self.stderr_handler.read()
  138. self._process.poll()
  139. self._is_read = True
  140. return self._output_cache
  141. except KeyboardInterrupt:
  142. self.kill()
  143. raise
  144. def read_error(self):
  145. '''Метод для чтения ошибок, появившихся при выполнении процесса, из его
  146. stderr.'''
  147. self.read()
  148. if not self._error_cache:
  149. try:
  150. self._error_cache = self.stderr_handler.read()
  151. except IOError:
  152. self._error_cache = ''
  153. return self._error_cache
  154. def kill(self):
  155. '''Метод для удаления процесса, если он работает.'''
  156. if self._opened:
  157. self._process.kill()
  158. def read_lines(self):
  159. return self.read().split('\n')
  160. def __iter__(self):
  161. if not self._iterator:
  162. self._iterator = iter(self.read_lines())
  163. return self._iterator
  164. def next(self):
  165. return next(self.__iter__(), None)
  166. @property
  167. def writable(self):
  168. '''Метод для проверки возможности записи данных в во входной поток
  169. процесса.'''
  170. return self._writable
  171. @property
  172. def readable(self):
  173. '''Метод для проверки возможности чтения вывода процесса.'''
  174. return self._readable
  175. @property
  176. def readable_errors(self):
  177. '''Метод для проверки возможности чтения ошибок.'''
  178. return self._readable_errors
  179. def return_code(self):
  180. '''Метод возвращающий код возвращенный процессом.'''
  181. self.read()
  182. return self._process.returncode
  183. @property
  184. def shell_command(self):
  185. '''Метод для получения эквивалентной консольной команды.'''
  186. command = ' '.join(self._command)
  187. previous_commands = self._stdin.shell_command
  188. if previous_commands == '':
  189. return command
  190. else:
  191. return ' | '.join([previous_commands, command])
  192. def success(self):
  193. '''Метод для проверки успешности выполнения процесса.'''
  194. return self.return_code() == 0
  195. def failed(self):
  196. '''Метод для проверки неуспешности выполнения процесса.'''
  197. return self.return_code() != 0
  198. class ProgramPathCache:
  199. '''Класс, для поиска и кэширования путей к исполнительным файлам различных
  200. команд.'''
  201. def __init__(self):
  202. self._cache: dict = {}
  203. def __call__(self, program_name: str, prefix: str = '/'):
  204. program_base_name = path.basename(program_name)
  205. PATH = os.environ['PATH']
  206. PATH = PATH.split(':')
  207. cache_key = (program_base_name, prefix)
  208. if cache_key in self._cache:
  209. self._cache[cache_key]
  210. if program_name.startswith('/'):
  211. if path.exists(join_paths(prefix, program_name)):
  212. self._cache[cache_key] = program_name
  213. return program_name
  214. for program_name in (join_paths(bin_path, program_base_name)
  215. for bin_path in PATH):
  216. if path.exists(join_paths(prefix, program_name)):
  217. self._cache[cache_key] = program_name
  218. return program_name
  219. return False
  220. get_program_path = ProgramPathCache()
  221. def check_command(*utils: List[str]):
  222. '''Функция для проверки наличия той или иной команды системе.'''
  223. output = []
  224. for util in utils:
  225. util_path = get_program_path(util)
  226. if not util_path:
  227. raise FilesError("Command not found '{}'".
  228. format(os.path.basename(util)))
  229. output.append(util)
  230. if len(output) == 1:
  231. return output[0]
  232. else:
  233. return output
  234. def join_paths(*paths: List[str]) -> str:
  235. '''Функция для объединения путей. Объединяет также абсолютные пути.'''
  236. if len(paths) == 1:
  237. return next(iter(paths))
  238. paths_to_join = []
  239. for _path in paths[1:]:
  240. if _path.startswith('/'):
  241. _path = _path.strip()[1:]
  242. else:
  243. _path = _path.strip()
  244. if _path and _path != "/":
  245. paths_to_join.append(_path)
  246. output_path = path.join(paths[0], *paths_to_join)
  247. return output_path
  248. def read_link(file_path: str) -> str:
  249. '''Функция для получения целевого пути символьной ссылки.'''
  250. try:
  251. if path.exists(file_path):
  252. source_path = os.readlink(file_path)
  253. return source_path
  254. else:
  255. return None
  256. except (OSError, IOError) as error:
  257. mod, lineno = get_traceback_caller(*sys.exc_info())
  258. FilesError("can not read link: {}({}:{})".
  259. format(str(error), mod, lineno))
  260. def get_target_from_link(link_path: str, link_source: str,
  261. chroot_path: str = '/') -> str:
  262. '''Метод для получения целевого пути из целевого пути символьной ссылки
  263. с учетом того, что целевой путь символьной ссылки может быть
  264. относительным.'''
  265. if os.path.isabs(link_source):
  266. if chroot_path != '/':
  267. return join_paths(chroot_path, link_source)
  268. return link_source
  269. else:
  270. link_source = link_source.split('/')
  271. link_dir = os.path.dirname(link_path).split('/')
  272. if link_source[0] == '.':
  273. link_source.pop()
  274. else:
  275. while link_source[0] == '..':
  276. link_source.pop(0)
  277. link_dir.pop(-1)
  278. link_dir.extend(link_source)
  279. return '/'.join(link_dir)
  280. def read_file(file_path: str, binary: bool = False) -> Union[str, bytes]:
  281. '''Функция для чтения файлов, возвращает текст файла.'''
  282. try:
  283. if path.exists(file_path):
  284. with open(file_path, f'r{"b" if binary else ""}') as opened_file:
  285. return opened_file.read()
  286. except (OSError, IOError) as error:
  287. mod, lineno = get_traceback_caller(*sys.exc_info())
  288. raise FilesError("file read error, {0}({1}:{2})".
  289. format(str(error), mod, lineno))
  290. def grep_file(file_path, regexp, flags=0):
  291. """
  292. Получить из файла данные по регулярному выражению
  293. """
  294. data = read_file(file_path)
  295. m = re.search(regexp, data, flags=flags)
  296. if m:
  297. return m.group()
  298. return ""
  299. def write_file(file_path):
  300. '''Функция для открытия и записи файлов. Создает директории на пути к
  301. целевому файлу если это необходимо. Возвращает файловый объект.'''
  302. directory_path = path.dirname(file_path)
  303. if not path.exists(directory_path):
  304. os.makedirs(directory_path)
  305. file_to_write = open(file_path, 'w')
  306. return file_to_write
  307. def read_file_lines(file_name, grab=False):
  308. '''Функция для чтения файлов построчно.'''
  309. try:
  310. if path.exists(file_name):
  311. for file_line in open(file_name, 'r'):
  312. if grab:
  313. file_line = file_line.strip()
  314. if not file_line.startswith('#'):
  315. yield file_line
  316. else:
  317. yield file_line.strip()
  318. except (OSError, IOError):
  319. pass
  320. def quite_unlink(file_path):
  321. try:
  322. if path.lexists(file_path):
  323. os.unlink(file_path)
  324. except OSError:
  325. pass
  326. def list_directory(directory_path, fullpath=False, only_dir=False):
  327. if not path.exists(directory_path):
  328. return []
  329. try:
  330. if fullpath:
  331. if only_dir:
  332. return [node.path for node in os.scandir(directory_path)
  333. if os.path.isdir(node.path)]
  334. else:
  335. return [node.path for node in os.scandir(directory_path)]
  336. else:
  337. if only_dir:
  338. return [node.name for node in os.scandir(directory_path)
  339. if os.path.isdir(node.path)]
  340. else:
  341. return os.listdir(directory_path)
  342. except OSError:
  343. return []
  344. def get_directory_contents(path: str):
  345. '''Функция для получения списка путей ко всем файлам и директориям,
  346. содержащимся в указанной директории.'''
  347. output = []
  348. for entry in os.scandir(path):
  349. try:
  350. if entry.is_symlink():
  351. output.append(entry.path)
  352. elif entry.is_dir():
  353. output.append(entry.path)
  354. output.extend(get_directory_contents(entry.path))
  355. elif entry.is_file():
  356. output.append(entry.path)
  357. except Exception:
  358. continue
  359. return output
  360. def make_directory(directory_path, force=False):
  361. try:
  362. parent = os.path.split(path.normpath(directory_path))[0]
  363. if not path.exists(parent):
  364. make_directory(parent)
  365. else:
  366. if os.path.exists(directory_path):
  367. if force and not os.path.isdir(directory_path):
  368. os.remove(directory_path)
  369. else:
  370. return True
  371. os.mkdir(directory_path)
  372. return True
  373. except (OSError, IOError):
  374. return False
  375. def check_directory_link(link_path, chroot_path='/'):
  376. '''Функция для проверки наличия зацикливающихся ссылок и их корректности в
  377. целом. В случае успешной проверки возвращает целевой путь ссылки.'''
  378. link_target = read_link(link_path)
  379. link_target = get_target_from_link(link_path, link_target,
  380. chroot_path=chroot_path)
  381. if link_target is None:
  382. # Ссылка не существует.
  383. raise FilesError('the source file does not exist')
  384. if not os.path.isdir(link_target):
  385. # Ссылка не на директорию.
  386. raise FilesError('the source is not directory')
  387. linked_path = os.path.abspath(link_target)
  388. # Добавляем / к концу пути, чтобы показать, что это путь к директории.
  389. if linked_path[-1] != '/':
  390. linked_path = linked_path + '/'
  391. # Пути, которые нужно проверить.
  392. to_check = [linked_path]
  393. # Целевые пути из встреченных ссылок.
  394. linked_paths = {linked_path}
  395. while to_check:
  396. current_directory = to_check.pop()
  397. for entry in os.scandir(current_directory):
  398. # Обходим только директории и ссылки на директории.
  399. if not entry.is_dir():
  400. continue
  401. if entry.is_symlink():
  402. linked_path = read_link(entry.path)
  403. linked_path = get_target_from_link(entry.path,
  404. linked_path,
  405. chroot_path=chroot_path)
  406. if linked_path in linked_paths:
  407. raise FilesError(
  408. 'the source directory contains cycled links')
  409. linked_paths.add(linked_path)
  410. to_check.append(linked_path)
  411. else:
  412. to_check.append(entry.path)
  413. return link_target
  414. class RealFS(GenericFS):
  415. def __init__(self, prefix='/'):
  416. self.prefix = prefix
  417. if prefix == '/':
  418. self.remove_prefix = lambda x: x
  419. else:
  420. self.remove_prefix = self._remove_prefix
  421. def _remove_prefix(self, file_path):
  422. prefix_length = len(self.prefix)
  423. return file_path[:prefix_length]
  424. def _get_path(self, file_path):
  425. return join_paths(self.prefix, file_path)
  426. def exists(self, file_path):
  427. return os.path.lexists(self._get_path(file_path))
  428. def read(self, file_path):
  429. return read_file(self._get_path(file_path))
  430. def glob(self, file_path):
  431. for glob_path in glob(self._get_path(file_path)):
  432. yield self.remove_prefix(glob_path)
  433. def realpath(self, file_path):
  434. return self.remove_prefix(path.realpath(file_path))
  435. def write(self, file_path, data):
  436. with write_file(file_path) as target_file:
  437. target_file.write(data)
  438. def listdir(self, file_path, full_path=False):
  439. if full_path:
  440. return [self.remove_prefix(listed_path)
  441. for listed_path in list_directory(file_path,
  442. full_path=full_path)]
  443. else:
  444. return list_directory(file_path, full_path=full_path)
  445. def get_run_commands(not_chroot=False, chroot=None, uid=None, with_pid=False):
  446. def get_cmdline(process_number):
  447. cmdline_file = '/proc/{}/cmdline'.format(process_number)
  448. try:
  449. if uid is not None:
  450. fstat = os.stat('/proc/{}'.format(process_number))
  451. if fstat.st_uid != uid:
  452. return ''
  453. if path.exists(cmdline_file):
  454. if not_chroot:
  455. root_link = '/proc/{}/root'.format(process_number)
  456. if os.readlink(root_link) != '/':
  457. return ''
  458. if chroot is not None:
  459. root_link = '/proc/{}/root'.format(process_number)
  460. if os.readlink(root_link) != chroot:
  461. return ''
  462. return read_file(cmdline_file).strip()
  463. except Exception:
  464. pass
  465. return ''
  466. if not os.access('/proc', os.R_OK):
  467. return []
  468. proc_directory = list_directory('/proc')
  469. output = []
  470. for file_name in proc_directory:
  471. if file_name.isdigit():
  472. cmdline = get_cmdline(file_name)
  473. if cmdline:
  474. if with_pid:
  475. output.append((file_name, cmdline))
  476. else:
  477. output.append(cmdline)
  478. return output
  479. @contextlib.contextmanager
  480. def stderr_devnull():
  481. oldstderr = sys.stderr
  482. sys.stderr = open(os.devnull, 'w')
  483. try:
  484. yield
  485. finally:
  486. sys.stderr = oldstderr