├── plotmanager ├── __init__.py └── library │ ├── __init__.py │ ├── parse │ ├── __init__.py │ └── configuration.py │ ├── commands │ ├── __init__.py │ └── plots.py │ └── utilities │ ├── __init__.py │ ├── exceptions.py │ ├── notifications.py │ ├── objects.py │ ├── print.py │ ├── commands.py │ ├── log.py │ ├── jobs.py │ └── processes.py ├── STATELESS-OVERRIDE.json ├── requirements.txt ├── .github └── FUNDING.yml ├── manager.py ├── .gitignore ├── stateless-manager.py ├── README.md ├── config.yaml.default └── LICENSE /plotmanager/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /plotmanager/library/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /plotmanager/library/parse/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /plotmanager/library/commands/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /plotmanager/library/utilities/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /STATELESS-OVERRIDE.json: -------------------------------------------------------------------------------- 1 | {"stop_plotting": false} -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | dateparser==1.0.0 2 | discord_notify==1.0.0 3 | playsound==1.2.2 4 | psutil==5.8.0 5 | python_pushover==0.4 6 | PyYAML==5.4.1 7 | -------------------------------------------------------------------------------- /plotmanager/library/utilities/exceptions.py: -------------------------------------------------------------------------------- 1 | 2 | class InvalidYAMLConfigException(Exception): 3 | pass 4 | 5 | 6 | class InvalidArgumentException(Exception): 7 | pass 8 | 9 | 10 | class ManagerError(Exception): 11 | pass 12 | 13 | 14 | class TerminationException(Exception): 15 | pass 16 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: swar 4 | patreon: swar 5 | ko_fi: swarpatel 6 | custom: ["https://www.chiaexplorer.com/blockchain/address/xch134evwwqkq50nnsmgehnnag4gc856ydc7ached3xxr6jdk7e8l4usdnw39t", "https://etherscan.io/address/0xf8F7BD24B94D75E54BFD9557fF6904DBE239322E", "https://www.blockchain.com/btc/address/36gnjnHqkttcBiKjjAekoy68z6C3BJ9ekS", "https://www.paypal.com/biz/fund?id=XGVS7J69KYBTY"] 7 | -------------------------------------------------------------------------------- /plotmanager/library/utilities/notifications.py: -------------------------------------------------------------------------------- 1 | import discord_notify 2 | import playsound 3 | import pushover 4 | 5 | 6 | def _send_notifications(title, body, settings): 7 | if settings.get('notify_discord') is True: 8 | notifier = discord_notify.Notifier(settings.get('discord_webhook_url')) 9 | notifier.send(body, print_message=False) 10 | 11 | if settings.get('notify_sound') is True: 12 | playsound.playsound(settings.get('song')) 13 | 14 | if settings.get('notify_pushover') is True: 15 | client = pushover.Client(settings.get('pushover_user_key'), api_token=settings.get('pushover_api_key')) 16 | client.send_message(body, title=title) 17 | 18 | 19 | def send_notifications(title, body, settings): 20 | try: 21 | _send_notifications(title=title, body=body, settings=settings) 22 | except: 23 | pass 24 | -------------------------------------------------------------------------------- /plotmanager/library/commands/plots.py: -------------------------------------------------------------------------------- 1 | def create(size, memory_buffer, temporary_directory, destination_directory, threads, buckets, bitfield, 2 | chia_location='chia', temporary2_directory=None, farmer_public_key=None, pool_public_key=None): 3 | flags = dict( 4 | k=size, 5 | b=memory_buffer, 6 | t=temporary_directory, 7 | d=destination_directory, 8 | r=threads, 9 | u=buckets, 10 | ) 11 | if temporary2_directory is not None: 12 | flags['2'] = temporary2_directory 13 | if farmer_public_key is not None: 14 | flags['f'] = farmer_public_key 15 | if pool_public_key is not None: 16 | flags['p'] = pool_public_key 17 | if bitfield is False: 18 | flags['e'] = '' 19 | 20 | data = [chia_location, 'plots', 'create'] 21 | for key, value in flags.items(): 22 | flag = f'-{key}' 23 | data.append(flag) 24 | if value == '': 25 | continue 26 | data.append(str(value)) 27 | return data 28 | -------------------------------------------------------------------------------- /plotmanager/library/utilities/objects.py: -------------------------------------------------------------------------------- 1 | class Job: 2 | name = None 3 | current_work_id = 0 4 | 5 | farmer_public_key = None 6 | pool_public_key = None 7 | 8 | total_running = 0 9 | total_completed = 0 10 | max_concurrent = 0 11 | max_concurrent_with_start_early = 0 12 | max_plots = 0 13 | temporary2_destination_sync = None 14 | 15 | stagger_minutes = None 16 | max_for_phase_1 = None 17 | concurrency_start_early_phase = None 18 | concurrency_start_early_phase_delay = None 19 | 20 | running_work = [] 21 | 22 | temporary_directory = None 23 | temporary2_directory = None 24 | destination_directory = [] 25 | size = None 26 | bitfield = None 27 | threads = None 28 | buckets = None 29 | memory_buffer = None 30 | 31 | 32 | class Work: 33 | work_id = None 34 | job = None 35 | pid = None 36 | plot_id = None 37 | log_file = None 38 | 39 | temporary_drive = None 40 | temporary2_drive = None 41 | destination_drive = None 42 | 43 | current_phase = 1 44 | 45 | datetime_start = None 46 | datetime_end = None 47 | 48 | phase_times = {} 49 | total_run_time = None 50 | 51 | completed = False 52 | 53 | progress = 0 54 | temp_file_size = 0 55 | k_size = None 56 | 57 | 58 | -------------------------------------------------------------------------------- /manager.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | 3 | from plotmanager.library.utilities.exceptions import InvalidArgumentException 4 | from plotmanager.library.utilities.commands import start_manager, stop_manager, view, analyze_logs 5 | 6 | 7 | parser = argparse.ArgumentParser(description='This is the central manager for Swar\'s Chia Plot Manager.') 8 | 9 | help_description = ''' 10 | There are a few different actions that you can use: "start", "restart", "stop", "view", and "analyze_logs". "start" will 11 | start a manager process. If one already exists, it will display an error message. "restart" will try to kill any 12 | existing manager and start a new one. "stop" will terminate the manager, but all existing plots will be completed. 13 | "view" can be used to display an updating table that will show the progress of your plots. Once a manager has started it 14 | will always be running in the background unless an error occurs. This field is case-sensitive. 15 | 16 | "analyze_logs" is a helper command that will scan all the logs in your log_directory to get your custom settings for 17 | the progress settings in the YAML file. 18 | ''' 19 | 20 | parser.add_argument( 21 | dest='action', 22 | type=str, 23 | help=help_description, 24 | ) 25 | 26 | args = parser.parse_args() 27 | 28 | if args.action == 'start': 29 | start_manager() 30 | elif args.action == 'restart': 31 | stop_manager() 32 | start_manager() 33 | elif args.action == 'stop': 34 | stop_manager() 35 | elif args.action == 'view': 36 | view() 37 | elif args.action == 'analyze_logs': 38 | analyze_logs() 39 | else: 40 | error_message = 'Invalid action provided. The valid options are "start", "restart", "stop", "view", and ' \ 41 | '"analyze_logs".' 42 | raise InvalidArgumentException(error_message) 43 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | 131 | # Custom 132 | config.yaml 133 | *.bak 134 | -------------------------------------------------------------------------------- /stateless-manager.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import time 3 | 4 | from datetime import datetime, timedelta 5 | 6 | from plotmanager.library.parse.configuration import get_config_info 7 | from plotmanager.library.utilities.jobs import has_active_jobs_and_work, load_jobs, monitor_jobs_to_start 8 | from plotmanager.library.utilities.log import check_log_progress 9 | from plotmanager.library.utilities.processes import get_running_plots 10 | 11 | 12 | chia_location, log_directory, config_jobs, manager_check_interval, max_concurrent, progress_settings, \ 13 | notification_settings, debug_level, view_settings = get_config_info() 14 | 15 | logging.basicConfig(format='%(asctime)s [%(levelname)s]: %(message)s', datefmt='%Y-%m-%d %H:%M:%S', level=debug_level) 16 | 17 | logging.info(f'Debug Level: {debug_level}') 18 | logging.info(f'Chia Location: {chia_location}') 19 | logging.info(f'Log Directory: {log_directory}') 20 | logging.info(f'Jobs: {config_jobs}') 21 | logging.info(f'Manager Check Interval: {manager_check_interval}') 22 | logging.info(f'Max Concurrent: {max_concurrent}') 23 | logging.info(f'Progress Settings: {progress_settings}') 24 | logging.info(f'Notification Settings: {notification_settings}') 25 | logging.info(f'View Settings: {view_settings}') 26 | 27 | logging.info(f'Loading jobs into objects.') 28 | jobs = load_jobs(config_jobs) 29 | 30 | next_log_check = datetime.now() 31 | next_job_work = {} 32 | running_work = {} 33 | 34 | logging.info(f'Grabbing running plots.') 35 | jobs, running_work = get_running_plots(jobs, running_work) 36 | for job in jobs: 37 | max_date = None 38 | for pid in job.running_work: 39 | work = running_work[pid] 40 | start = work.datetime_start 41 | if not max_date or start > max_date: 42 | max_date = start 43 | if not max_date: 44 | continue 45 | next_job_work[job.name] = max_date + timedelta(minutes=job.stagger_minutes) 46 | logging.info(f'{job.name} Found. Setting next stagger date to {next_job_work[job.name]}') 47 | 48 | logging.info(f'Starting loop.') 49 | while has_active_jobs_and_work(jobs): 50 | # CHECK LOGS FOR DELETED WORK 51 | logging.info(f'Checking log progress..') 52 | check_log_progress(jobs=jobs, running_work=running_work, progress_settings=progress_settings, 53 | notification_settings=notification_settings, view_settings=view_settings) 54 | next_log_check = datetime.now() + timedelta(seconds=manager_check_interval) 55 | 56 | # DETERMINE IF JOB NEEDS TO START 57 | logging.info(f'Monitoring jobs to start.') 58 | jobs, running_work, next_job_work, next_log_check = monitor_jobs_to_start( 59 | jobs=jobs, 60 | running_work=running_work, 61 | max_concurrent=max_concurrent, 62 | next_job_work=next_job_work, 63 | chia_location=chia_location, 64 | log_directory=log_directory, 65 | next_log_check=next_log_check, 66 | ) 67 | 68 | logging.info(f'Sleeping for {manager_check_interval} seconds.') 69 | time.sleep(manager_check_interval) 70 | -------------------------------------------------------------------------------- /plotmanager/library/parse/configuration.py: -------------------------------------------------------------------------------- 1 | import pathlib 2 | import os 3 | import yaml 4 | 5 | 6 | from plotmanager.library.utilities.exceptions import InvalidYAMLConfigException 7 | 8 | 9 | def _get_config(): 10 | directory = pathlib.Path().resolve() 11 | file_name = 'config.yaml' 12 | file_path = os.path.join(directory, file_name) 13 | if not os.path.exists(file_path): 14 | raise FileNotFoundError(f"Unable to find the config.yaml file. Expected location: {file_path}") 15 | f = open(file_path, 'r') 16 | config = yaml.load(stream=f, Loader=yaml.Loader) 17 | f.close() 18 | return config 19 | 20 | 21 | def _get_chia_location(config): 22 | return config.get('chia_location', 'chia') 23 | 24 | 25 | def _get_progress_settings(config): 26 | progress_setting = config['progress'] 27 | expected_parameters = ['phase1_line_end', 'phase2_line_end', 'phase3_line_end', 'phase4_line_end', 'phase1_weight', 28 | 'phase2_weight', 'phase3_weight', 'phase4_weight', ] 29 | _check_parameters(parameter=progress_setting, expected_parameters=expected_parameters, parameter_type='progress') 30 | return progress_setting 31 | 32 | 33 | def _get_manager_settings(config): 34 | if 'manager' not in config: 35 | raise InvalidYAMLConfigException('Failed to find the log parameter in the YAML.') 36 | manager = config['manager'] 37 | expected_parameters = ['check_interval', 'log_level'] 38 | _check_parameters(parameter=manager, expected_parameters=expected_parameters, parameter_type='manager') 39 | return manager['check_interval'], manager['log_level'] 40 | 41 | 42 | def _get_log_settings(config): 43 | if 'log' not in config: 44 | raise InvalidYAMLConfigException('Failed to find the log parameter in the YAML.') 45 | log = config['log'] 46 | expected_parameters = ['folder_path'] 47 | _check_parameters(parameter=log, expected_parameters=expected_parameters, parameter_type='log') 48 | return log['folder_path'] 49 | 50 | 51 | def _get_jobs(config): 52 | if 'jobs' not in config: 53 | raise InvalidYAMLConfigException('Failed to find the jobs parameter in the YAML.') 54 | return config['jobs'] 55 | 56 | 57 | def _get_global_max_concurrent_config(config): 58 | if 'global' not in config: 59 | raise InvalidYAMLConfigException('Failed to find global parameter in the YAML.') 60 | if 'max_concurrent' not in config['global']: 61 | raise InvalidYAMLConfigException('Failed to find max_concurrent in the global parameter in the YAML.') 62 | max_concurrent = config['global']['max_concurrent'] 63 | if not isinstance(max_concurrent, int): 64 | raise Exception('global -> max_concurrent should be a integer value.') 65 | return max_concurrent 66 | 67 | 68 | def _get_notifications_settings(config): 69 | if 'notifications' not in config: 70 | raise InvalidYAMLConfigException('Failed to find notifications parameter in the YAML.') 71 | notifications = config['notifications'] 72 | expected_parameters = ['notify_discord', 'discord_webhook_url', 'notify_sound', 'song', 'notify_pushover', 73 | 'pushover_user_key', 'pushover_api_key'] 74 | _check_parameters(parameter=notifications, expected_parameters=expected_parameters, parameter_type='notification') 75 | return notifications 76 | 77 | 78 | def _get_view_settings(config): 79 | if 'view' not in config: 80 | raise InvalidYAMLConfigException('Failed to find view parameter in the YAML.') 81 | view = config['view'] 82 | expected_parameters = ['datetime_format', 'include_seconds_for_phase', 'include_drive_info', 'include_cpu', 'include_ram', 83 | 'include_plot_stats', 'check_interval'] 84 | _check_parameters(parameter=view, expected_parameters=expected_parameters, parameter_type='view') 85 | return view 86 | 87 | 88 | def _check_parameters(parameter, expected_parameters, parameter_type): 89 | failed_checks = [] 90 | checks = expected_parameters 91 | for check in checks: 92 | if check in parameter: 93 | continue 94 | failed_checks.append(check) 95 | 96 | if failed_checks: 97 | raise InvalidYAMLConfigException(f'Failed to find the following {parameter_type} parameters: ' 98 | f'{", ".join(failed_checks)}') 99 | 100 | 101 | def get_config_info(): 102 | config = _get_config() 103 | chia_location = _get_chia_location(config=config) 104 | manager_check_interval, log_level = _get_manager_settings(config=config) 105 | log_directory = _get_log_settings(config=config) 106 | if not os.path.exists(log_directory): 107 | os.makedirs(log_directory) 108 | jobs = _get_jobs(config=config) 109 | max_concurrent = _get_global_max_concurrent_config(config=config) 110 | progress_settings = _get_progress_settings(config=config) 111 | notification_settings = _get_notifications_settings(config=config) 112 | view_settings = _get_view_settings(config=config) 113 | 114 | return chia_location, log_directory, jobs, manager_check_interval, max_concurrent, \ 115 | progress_settings, notification_settings, log_level, view_settings 116 | -------------------------------------------------------------------------------- /plotmanager/library/utilities/print.py: -------------------------------------------------------------------------------- 1 | import os 2 | import psutil 3 | 4 | from datetime import datetime, timedelta 5 | 6 | from plotmanager.library.utilities.processes import get_manager_processes, get_chia_drives 7 | 8 | 9 | def _get_row_info(pid, running_work, view_settings): 10 | work = running_work[pid] 11 | phase_times = work.phase_times 12 | elapsed_time = (datetime.now() - work.datetime_start) 13 | elapsed_time = pretty_print_time(elapsed_time.seconds) 14 | phase_time_log = [] 15 | for i in range(1, 5): 16 | if phase_times.get(i): 17 | phase_time_log.append(phase_times.get(i)) 18 | 19 | row = [ 20 | work.job.name if work.job else '?', 21 | work.k_size, 22 | pid, 23 | work.datetime_start.strftime(view_settings['datetime_format']), 24 | elapsed_time, 25 | work.current_phase, 26 | ' / '.join(phase_time_log), 27 | work.progress, 28 | pretty_print_bytes(work.temp_file_size, 'gb', 0, " GiB"), 29 | ] 30 | return [str(cell) for cell in row] 31 | 32 | 33 | def pretty_print_bytes(size, size_type, significant_digits=2, suffix=''): 34 | if size_type.lower() == 'gb': 35 | power = 3 36 | elif size_type.lower() == 'tb': 37 | power = 4 38 | else: 39 | raise Exception('Failed to identify size_type.') 40 | calculated_value = round(size / (1024 ** power), significant_digits) 41 | calculated_value = f'{calculated_value:.{significant_digits}f}' 42 | return f"{calculated_value}{suffix}" 43 | 44 | 45 | def pretty_print_time(seconds, include_seconds=True): 46 | total_minutes, second = divmod(seconds, 60) 47 | hour, minute = divmod(total_minutes, 60) 48 | return f"{hour:02}:{minute:02}{f':{second:02}' if include_seconds else ''}" 49 | 50 | 51 | def pretty_print_table(rows): 52 | max_characters = [0 for cell in rows[0]] 53 | for row in rows: 54 | for i, cell in enumerate(row): 55 | length = len(cell) 56 | if len(cell) <= max_characters[i]: 57 | continue 58 | max_characters[i] = length 59 | 60 | headers = " ".join([cell.center(max_characters[i]) for i, cell in enumerate(rows[0])]) 61 | separator = '=' * (sum(max_characters) + 3 * len(max_characters)) 62 | console = [separator, headers, separator] 63 | for row in rows[1:]: 64 | console.append(" ".join([cell.ljust(max_characters[i]) for i, cell in enumerate(row)])) 65 | console.append(separator) 66 | return "\n".join(console) 67 | 68 | 69 | def get_job_data(jobs, running_work, view_settings): 70 | rows = [] 71 | headers = ['num', 'job', 'k', 'pid', 'start', 'elapsed_time', 'phase', 'phase_times', 'progress', 'temp_size'] 72 | added_pids = [] 73 | for job in jobs: 74 | for pid in job.running_work: 75 | if pid not in running_work: 76 | continue 77 | rows.append(_get_row_info(pid, running_work, view_settings)) 78 | added_pids.append(pid) 79 | for pid in running_work.keys(): 80 | if pid in added_pids: 81 | continue 82 | rows.append(_get_row_info(pid, running_work, view_settings)) 83 | added_pids.append(pid) 84 | rows.sort(key=lambda x: (x[4]), reverse=True) 85 | for i in range(len(rows)): 86 | rows[i] = [str(i+1)] + rows[i] 87 | rows = [headers] + rows 88 | return pretty_print_table(rows) 89 | 90 | 91 | def get_drive_data(drives): 92 | chia_drives = get_chia_drives() 93 | headers = ['type', 'drive', 'used', 'total', 'percent', 'plots'] 94 | rows = [headers] 95 | for drive_type, drives in drives.items(): 96 | for drive in drives: 97 | try: 98 | usage = psutil.disk_usage(drive) 99 | except FileNotFoundError: 100 | continue 101 | rows.append([drive_type, drive, f'{pretty_print_bytes(usage.used, "tb", 2, "TiB")}', 102 | f'{pretty_print_bytes(usage.total, "tb", 2, "TiB")}', f'{usage.percent}%', 103 | str(chia_drives[drive_type].get(drive, '?'))]) 104 | return pretty_print_table(rows) 105 | 106 | 107 | def print_view(jobs, running_work, analysis, drives, next_log_check, view_settings): 108 | # Job Table 109 | job_data = get_job_data(jobs=jobs, running_work=running_work, view_settings=view_settings) 110 | 111 | # Drive Table 112 | drive_data = '' 113 | if view_settings.get('include_drive_info'): 114 | drive_data = get_drive_data(drives) 115 | 116 | manager_processes = get_manager_processes() 117 | 118 | if os.name == 'nt': 119 | os.system('cls') 120 | else: 121 | os.system('clear') 122 | print(job_data) 123 | print(f'Manager Status: {"Running" if manager_processes else "Stopped"}') 124 | print() 125 | 126 | if view_settings.get('include_drive_info'): 127 | print(drive_data) 128 | if view_settings.get('include_cpu'): 129 | print(f'CPU Usage: {psutil.cpu_percent()}%') 130 | if view_settings.get('include_ram'): 131 | ram_usage = psutil.virtual_memory() 132 | print(f'RAM Usage: {pretty_print_bytes(ram_usage.used, "gb")}/{pretty_print_bytes(ram_usage.total, "gb", 2, "GiB")}' 133 | f'({ram_usage.percent}%)') 134 | print() 135 | if view_settings.get('include_plot_stats'): 136 | print(f'Plots Completed Yesterday: {analysis["summary"].get(datetime.now().date() - timedelta(days=1), 0)}') 137 | print(f'Plots Completed Today: {analysis["summary"].get(datetime.now().date(), 0)}') 138 | print() 139 | print(f"Next log check at {next_log_check.strftime('%Y-%m-%d %H:%M:%S')}") 140 | print() 141 | -------------------------------------------------------------------------------- /plotmanager/library/utilities/commands.py: -------------------------------------------------------------------------------- 1 | import os 2 | import pathlib 3 | import psutil 4 | import socket 5 | import sys 6 | import time 7 | 8 | from datetime import datetime, timedelta 9 | 10 | from plotmanager.library.parse.configuration import get_config_info 11 | from plotmanager.library.utilities.exceptions import ManagerError, TerminationException 12 | from plotmanager.library.utilities.jobs import load_jobs 13 | from plotmanager.library.utilities.log import analyze_log_dates, check_log_progress, analyze_log_times 14 | from plotmanager.library.utilities.notifications import send_notifications 15 | from plotmanager.library.utilities.print import print_view 16 | from plotmanager.library.utilities.processes import is_windows, get_manager_processes, get_running_plots, start_process 17 | 18 | 19 | def start_manager(): 20 | if get_manager_processes(): 21 | raise ManagerError('Manager is already running.') 22 | 23 | directory = pathlib.Path().resolve() 24 | stateless_manager_path = os.path.join(directory, 'stateless-manager.py') 25 | if not os.path.exists(stateless_manager_path): 26 | raise FileNotFoundError('Failed to find stateless-manager.') 27 | manager_log_file_path = os.path.join(directory, 'manager.log') 28 | manager_log_file = open(manager_log_file_path, 'a') 29 | python_file_path = sys.executable 30 | 31 | chia_location, log_directory, jobs, manager_check_interval, max_concurrent, progress_settings, \ 32 | notification_settings, debug_level, view_settings = get_config_info() 33 | 34 | extra_args = [] 35 | if is_windows(): 36 | pythonw_file_path = '\\'.join(python_file_path.split('\\')[:-1] + ['pythonw.exe']) 37 | else: 38 | pythonw_file_path = '\\'.join(python_file_path.split('\\')[:-1] + ['python &']) 39 | extra_args.append('&') 40 | if os.path.exists(pythonw_file_path): 41 | python_file_path = pythonw_file_path 42 | 43 | args = [python_file_path, stateless_manager_path] + extra_args 44 | start_process(args=args, log_file=manager_log_file) 45 | time.sleep(3) 46 | if not get_manager_processes(): 47 | raise ManagerError('Failed to start Manager. Please look at manager.log for more details on the error. It is in the same folder as manager.py.') 48 | 49 | send_notifications( 50 | title='Plot manager started', 51 | body=f'Plot Manager has started on {socket.gethostname()}...', 52 | settings=notification_settings, 53 | ) 54 | print('Plot Manager has started...') 55 | 56 | 57 | def stop_manager(): 58 | processes = get_manager_processes() 59 | if not processes: 60 | print("No manager processes were found.") 61 | return 62 | for process in processes: 63 | try: 64 | process.terminate() 65 | except psutil.NoSuchProcess: 66 | pass 67 | if get_manager_processes(): 68 | raise TerminationException("Failed to stop manager processes.") 69 | print("Successfully stopped manager processes.") 70 | 71 | 72 | def view(): 73 | chia_location, log_directory, config_jobs, manager_check_interval, max_concurrent, progress_settings, \ 74 | notification_settings, debug_level, view_settings = get_config_info() 75 | view_check_interval = view_settings['check_interval'] 76 | analysis = {'files': {}} 77 | drives = {'temp': [], 'temp2': [], 'dest': []} 78 | jobs = load_jobs(config_jobs) 79 | for job in jobs: 80 | drive = job.temporary_directory.split('\\')[0] 81 | drives['temp'].append(drive) 82 | directories = { 83 | 'dest': job.destination_directory, 84 | 'temp2': job.temporary2_directory, 85 | } 86 | for key, directory_list in directories.items(): 87 | if directory_list is None: 88 | continue 89 | if isinstance(directory_list, list): 90 | for directory in directory_list: 91 | drive = directory.split('\\')[0] 92 | if drive in drives[key]: 93 | continue 94 | drives[key].append(drive) 95 | else: 96 | drive = directory_list.split('\\')[0] 97 | if drive in drives[key]: 98 | continue 99 | drives[key].append(drive) 100 | 101 | while True: 102 | running_work = {} 103 | try: 104 | analysis = analyze_log_dates(log_directory=log_directory, analysis=analysis) 105 | jobs = load_jobs(config_jobs) 106 | jobs, running_work = get_running_plots(jobs=jobs, running_work=running_work) 107 | check_log_progress(jobs=jobs, running_work=running_work, progress_settings=progress_settings, 108 | notification_settings=notification_settings, view_settings=view_settings) 109 | print_view(jobs=jobs, running_work=running_work, analysis=analysis, drives=drives, 110 | next_log_check=datetime.now() + timedelta(seconds=60), view_settings=view_settings) 111 | time.sleep(view_check_interval) 112 | has_file = False 113 | if len(running_work.values()) == 0: 114 | has_file = True 115 | for work in running_work.values(): 116 | if not work.log_file: 117 | continue 118 | has_file = True 119 | break 120 | if not has_file: 121 | print("Restarting view due to psutil going stale...") 122 | system_args = [f'"{sys.executable}"'] + sys.argv 123 | os.execv(sys.executable, system_args) 124 | except KeyboardInterrupt: 125 | print("Stopped view.") 126 | exit() 127 | 128 | 129 | def analyze_logs(): 130 | chia_location, log_directory, jobs, manager_check_interval, max_concurrent, progress_settings, \ 131 | notification_settings, debug_level, view_settings = get_config_info() 132 | analyze_log_times(log_directory) 133 | -------------------------------------------------------------------------------- /plotmanager/library/utilities/log.py: -------------------------------------------------------------------------------- 1 | import dateparser 2 | import logging 3 | import os 4 | import psutil 5 | import re 6 | import socket 7 | 8 | from plotmanager.library.utilities.notifications import send_notifications 9 | from plotmanager.library.utilities.print import pretty_print_time 10 | 11 | 12 | def get_log_file_name(log_directory, job, datetime): 13 | return os.path.join(log_directory, f'{job.name}_{str(datetime).replace(" ", "_").replace(":", "_").replace(".", "_")}.log') 14 | 15 | 16 | def _analyze_log_end_date(contents): 17 | match = re.search(r'total time = ([\d\.]+) seconds\. CPU \([\d\.]+%\) [A-Za-z]+\s([^\n]+)\n', contents, flags=re.I) 18 | if not match: 19 | return False 20 | total_seconds, date_raw = match.groups() 21 | total_seconds = pretty_print_time(int(float(total_seconds))) 22 | parsed_date = dateparser.parse(date_raw) 23 | return dict( 24 | total_seconds=total_seconds, 25 | date=parsed_date, 26 | ) 27 | 28 | 29 | def _get_date_summary(analysis): 30 | summary = analysis.get('summary', {}) 31 | for file_path in analysis['files'].keys(): 32 | if analysis['files'][file_path]['checked']: 33 | continue 34 | analysis['files'][file_path]['checked'] = True 35 | end_date = analysis['files'][file_path]['data']['date'].date() 36 | if end_date not in summary: 37 | summary[end_date] = 0 38 | summary[end_date] += 1 39 | analysis['summary'] = summary 40 | return analysis 41 | 42 | 43 | def _get_regex(pattern, string, flags=re.I): 44 | return re.search(pattern, string, flags=flags).groups() 45 | 46 | 47 | def get_completed_log_files(log_directory, skip=None): 48 | if not skip: 49 | skip = [] 50 | files = {} 51 | for file in os.listdir(log_directory): 52 | if file[-4:] not in ['.log', '.txt']: 53 | continue 54 | file_path = os.path.join(log_directory, file) 55 | if file_path in skip: 56 | continue 57 | f = open(file_path, 'r') 58 | try: 59 | contents = f.read() 60 | except UnicodeDecodeError: 61 | continue 62 | f.close() 63 | if 'Total time = ' not in contents: 64 | continue 65 | files[file_path] = contents 66 | return files 67 | 68 | 69 | def analyze_log_dates(log_directory, analysis): 70 | files = get_completed_log_files(log_directory, skip=list(analysis['files'].keys())) 71 | for file_path, contents in files.items(): 72 | data = _analyze_log_end_date(contents) 73 | if data is None: 74 | continue 75 | analysis['files'][file_path] = {'data': data, 'checked': False} 76 | analysis = _get_date_summary(analysis) 77 | return analysis 78 | 79 | 80 | def analyze_log_times(log_directory): 81 | total_times = {1: 0, 2: 0, 3: 0, 4: 0} 82 | line_numbers = {1: [], 2: [], 3: [], 4: []} 83 | count = 0 84 | files = get_completed_log_files(log_directory) 85 | for file_path, contents in files.items(): 86 | count += 1 87 | phase_times, phase_dates = get_phase_info(contents, pretty_print=False) 88 | for phase, seconds in phase_times.items(): 89 | total_times[phase] += seconds 90 | splits = contents.split('Time for phase') 91 | phase = 0 92 | new_lines = 1 93 | for split in splits: 94 | phase += 1 95 | if phase >= 5: 96 | break 97 | new_lines += split.count('\n') 98 | line_numbers[phase].append(new_lines) 99 | 100 | for phase in range(1, 5): 101 | print(f' phase{phase}_line_end: {int(round(sum(line_numbers[phase]) / len(line_numbers[phase]), 0))}') 102 | 103 | for phase in range(1, 5): 104 | print(f' phase{phase}_weight: {round(total_times[phase] / sum(total_times.values()) * 100, 2)}') 105 | 106 | 107 | def get_phase_info(contents, view_settings=None, pretty_print=True): 108 | if not view_settings: 109 | view_settings = {} 110 | phase_times = {} 111 | phase_dates = {} 112 | 113 | for phase in range(1, 5): 114 | match = re.search(rf'time for phase {phase} = ([\d\.]+) seconds\. CPU \([\d\.]+%\) [A-Za-z]+\s([^\n]+)\n', contents, flags=re.I) 115 | if match: 116 | seconds, date_raw = match.groups() 117 | seconds = float(seconds) 118 | phase_times[phase] = pretty_print_time(int(seconds), view_settings['include_seconds_for_phase']) if pretty_print else seconds 119 | parsed_date = dateparser.parse(date_raw) 120 | phase_dates[phase] = parsed_date 121 | 122 | return phase_times, phase_dates 123 | 124 | 125 | def get_progress(line_count, progress_settings): 126 | phase1_line_end = progress_settings['phase1_line_end'] 127 | phase2_line_end = progress_settings['phase2_line_end'] 128 | phase3_line_end = progress_settings['phase3_line_end'] 129 | phase4_line_end = progress_settings['phase4_line_end'] 130 | phase1_weight = progress_settings['phase1_weight'] 131 | phase2_weight = progress_settings['phase2_weight'] 132 | phase3_weight = progress_settings['phase3_weight'] 133 | phase4_weight = progress_settings['phase4_weight'] 134 | progress = 0 135 | if line_count > phase1_line_end: 136 | progress += phase1_weight 137 | else: 138 | progress += phase1_weight * (line_count / phase1_line_end) 139 | return progress 140 | if line_count > phase2_line_end: 141 | progress += phase2_weight 142 | else: 143 | progress += phase2_weight * ((line_count - phase1_line_end) / (phase2_line_end - phase1_line_end)) 144 | return progress 145 | if line_count > phase3_line_end: 146 | progress += phase3_weight 147 | else: 148 | progress += phase3_weight * ((line_count - phase2_line_end) / (phase3_line_end - phase2_line_end)) 149 | return progress 150 | if line_count > phase4_line_end: 151 | progress += phase4_weight 152 | else: 153 | progress += phase4_weight * ((line_count - phase3_line_end) / (phase4_line_end - phase3_line_end)) 154 | return progress 155 | 156 | 157 | def check_log_progress(jobs, running_work, progress_settings, notification_settings, view_settings): 158 | for pid, work in list(running_work.items()): 159 | logging.info(f'Checking log progress for PID: {pid}') 160 | if not work.log_file: 161 | continue 162 | f = open(work.log_file, 'r') 163 | data = f.read() 164 | f.close() 165 | 166 | line_count = (data.count('\n') + 1) 167 | 168 | progress = get_progress(line_count=line_count, progress_settings=progress_settings) 169 | 170 | phase_times, phase_dates = get_phase_info(data, view_settings) 171 | current_phase = 1 172 | if phase_times: 173 | current_phase = max(phase_times.keys()) + 1 174 | work.phase_times = phase_times 175 | work.phase_dates = phase_dates 176 | work.current_phase = current_phase 177 | work.progress = f'{progress:.2f}%' 178 | 179 | if psutil.pid_exists(pid) and 'renamed final file from ' not in data.lower(): 180 | logging.info(f'PID still alive: {pid}') 181 | continue 182 | 183 | logging.info(f'PID no longer alive: {pid}') 184 | for job in jobs: 185 | if not job or not work or not work.job: 186 | continue 187 | if job.name != work.job.name: 188 | continue 189 | logging.info(f'Removing PID {pid} from job: {job.name}') 190 | if pid in job.running_work: 191 | job.running_work.remove(pid) 192 | job.total_running -= 1 193 | job.total_completed += 1 194 | 195 | send_notifications( 196 | title='Plot Completed', 197 | body=f'You completed a plot on {socket.gethostname()}!', 198 | settings=notification_settings, 199 | ) 200 | break 201 | del running_work[pid] 202 | -------------------------------------------------------------------------------- /plotmanager/library/utilities/jobs.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import psutil 3 | 4 | from copy import deepcopy 5 | from datetime import datetime, timedelta 6 | 7 | from plotmanager.library.commands import plots 8 | from plotmanager.library.utilities.processes import is_windows, start_process 9 | from plotmanager.library.utilities.objects import Job, Work 10 | from plotmanager.library.utilities.log import get_log_file_name 11 | 12 | 13 | def has_active_jobs_and_work(jobs): 14 | for job in jobs: 15 | if job.total_completed < job.max_plots: 16 | return True 17 | return False 18 | 19 | 20 | def get_target_directories(job): 21 | job_offset = job.total_completed + job.total_running 22 | 23 | destination_directory = job.destination_directory 24 | temporary2_directory = job.temporary2_directory 25 | 26 | if isinstance(job.destination_directory, list): 27 | destination_directory = job.destination_directory[job_offset % len(job.destination_directory)] 28 | if isinstance(job.temporary2_directory, list): 29 | temporary2_directory = job.temporary2_directory[job_offset % len(job.temporary2_directory)] 30 | 31 | return destination_directory, temporary2_directory 32 | 33 | 34 | def load_jobs(config_jobs): 35 | jobs = [] 36 | for info in config_jobs: 37 | job = deepcopy(Job()) 38 | job.total_running = 0 39 | 40 | job.name = info['name'] 41 | job.max_plots = info['max_plots'] 42 | 43 | job.farmer_public_key = info.get('farmer_public_key', None) 44 | job.pool_public_key = info.get('pool_public_key', None) 45 | job.max_concurrent = info['max_concurrent'] 46 | job.max_concurrent_with_start_early = info['max_concurrent_with_start_early'] 47 | job.max_for_phase_1 = info['max_for_phase_1'] 48 | job.stagger_minutes = info.get('stagger_minutes', None) 49 | job.max_for_phase_1 = info.get('max_for_phase_1', None) 50 | job.concurrency_start_early_phase = info.get('concurrency_start_early_phase', None) 51 | job.concurrency_start_early_phase_delay = info.get('concurrency_start_early_phase_delay', None) 52 | job.temporary2_destination_sync = info.get('temporary2_destination_sync', False) 53 | 54 | job.temporary_directory = info['temporary_directory'] 55 | job.destination_directory = info['destination_directory'] 56 | 57 | temporary2_directory = info.get('temporary2_directory', None) 58 | if not temporary2_directory: 59 | temporary2_directory = None 60 | job.temporary2_directory = temporary2_directory 61 | 62 | job.size = info['size'] 63 | job.bitfield = info['bitfield'] 64 | job.threads = info['threads'] 65 | job.buckets = info['buckets'] 66 | job.memory_buffer = info['memory_buffer'] 67 | jobs.append(job) 68 | 69 | return jobs 70 | 71 | 72 | def monitor_jobs_to_start(jobs, running_work, max_concurrent, next_job_work, chia_location, log_directory, next_log_check): 73 | for i, job in enumerate(jobs): 74 | logging.info(f'Checking to queue work for job: {job.name}') 75 | if len(running_work.values()) >= max_concurrent: 76 | logging.info(f'Global concurrent limit met, skipping. Running plots: {len(running_work.values())}, ' 77 | f'Max global concurrent limit: {max_concurrent}') 78 | continue 79 | phase_1_count = 0 80 | for pid in job.running_work: 81 | if running_work[pid].current_phase > 1: 82 | continue 83 | phase_1_count += 1 84 | logging.info(f'Total jobs in phase 1: {phase_1_count}') 85 | if job.max_for_phase_1 and phase_1_count >= job.max_for_phase_1: 86 | logging.info(f'Max for phase 1 met, skipping. Max: {job.max_for_phase_1}') 87 | continue 88 | if job.total_completed >= job.max_plots: 89 | logging.info(f'Job\'s total completed greater than or equal to max plots, skipping. Total Completed: ' 90 | f'{job.total_completed}, Max Plots: {job.max_plots}') 91 | continue 92 | if job.name in next_job_work and next_job_work[job.name] > datetime.now(): 93 | logging.info(f'Waiting for job stagger, skipping. Next allowable time: {next_job_work[job.name]}') 94 | continue 95 | discount_running = 0 96 | if job.concurrency_start_early_phase is not None: 97 | for pid in job.running_work: 98 | work = running_work[pid] 99 | try: 100 | start_early_date = work.phase_dates[job.concurrency_start_early_phase - 1] 101 | except (KeyError, AttributeError): 102 | start_early_date = work.datetime_start 103 | 104 | if work.current_phase < job.concurrency_start_early_phase: 105 | continue 106 | if datetime.now() <= (start_early_date + timedelta(minutes=job.concurrency_start_early_phase_delay)): 107 | continue 108 | discount_running += 1 109 | if (job.total_running - discount_running) >= job.max_concurrent: 110 | logging.info(f'Job\'s max concurrent limit has been met, skipping. Max concurrent minus start_early: ' 111 | f'{job.total_running - discount_running}, Max concurrent: {job.max_concurrent}') 112 | continue 113 | if job.total_running >= job.max_concurrent_with_start_early: 114 | logging.info(f'Job\'s max concurrnet limit with start early has been met, skipping. Max: {job.max_concurrent_with_start_early}') 115 | continue 116 | if job.stagger_minutes: 117 | next_job_work[job.name] = datetime.now() + timedelta(minutes=job.stagger_minutes) 118 | logging.info(f'Calculating new job stagger time. Next stagger kickoff: {next_job_work[job.name]}') 119 | job, work = start_work(job=job, chia_location=chia_location, log_directory=log_directory) 120 | jobs[i] = deepcopy(job) 121 | next_log_check = datetime.now() 122 | running_work[work.pid] = work 123 | 124 | return jobs, running_work, next_job_work, next_log_check 125 | 126 | 127 | def start_work(job, chia_location, log_directory): 128 | logging.info(f'Starting new plot for job: {job.name}') 129 | nice_val = 10 130 | if is_windows(): 131 | nice_val = psutil.NORMAL_PRIORITY_CLASS 132 | 133 | now = datetime.now() 134 | log_file_path = get_log_file_name(log_directory, job, now) 135 | logging.info(f'Job log file path: {log_file_path}') 136 | destination_directory, temporary2_directory = get_target_directories(job) 137 | logging.info(f'Job destination directory: {destination_directory}') 138 | 139 | work = deepcopy(Work()) 140 | work.job = job 141 | work.log_file = log_file_path 142 | work.datetime_start = now 143 | work.work_id = job.current_work_id 144 | 145 | job.current_work_id += 1 146 | 147 | if job.temporary2_destination_sync: 148 | logging.info(f'Job temporary2 and destination sync') 149 | temporary2_directory = destination_directory 150 | logging.info(f'Job temporary2 directory: {temporary2_directory}') 151 | 152 | plot_command = plots.create( 153 | chia_location=chia_location, 154 | farmer_public_key=job.farmer_public_key, 155 | pool_public_key=job.pool_public_key, 156 | size=job.size, 157 | memory_buffer=job.memory_buffer, 158 | temporary_directory=job.temporary_directory, 159 | temporary2_directory=temporary2_directory, 160 | destination_directory=destination_directory, 161 | threads=job.threads, 162 | buckets=job.buckets, 163 | bitfield=job.bitfield, 164 | ) 165 | logging.info(f'Starting with plot command: {plot_command}') 166 | 167 | log_file = open(log_file_path, 'a') 168 | logging.info(f'Starting process') 169 | process = start_process(args=plot_command, log_file=log_file) 170 | pid = process.pid 171 | logging.info(f'Started process: {pid}') 172 | 173 | logging.info(f'Setting priority level: {nice_val}') 174 | psutil.Process(pid).nice(nice_val) 175 | logging.info(f'Set priority level') 176 | 177 | work.pid = pid 178 | job.total_running += 1 179 | job.running_work = job.running_work + [pid] 180 | logging.info(f'Job total running: {job.total_running}') 181 | logging.info(f'Job running: {job.running_work}') 182 | 183 | return job, work 184 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Swar's Chia Plot Manager 粉丝版本翻译(已升级 0.1.0) 2 | 3 | 下载或者查看英文请到原地址:https://github.com/swar/Swar-Chia-Plot-Manager 4 | 5 | 这是我为它制作的视频: 6 | - https://www.youtube.com/watch?v=CJcVSzpaXLQ 7 | - https://www.youtube.com/watch?v=ai-GA-9uX2g 8 | - https://www.youtube.com/watch?v=XqcDyg1btKo 9 | - https://www.youtube.com/watch?v=VdU2RaEYKRU 10 | 11 | >请注意,我并不以此牟利,不求任何人的关注,做影片完全是出于锻炼自己的目的,我志远不在此。 12 | 13 | 它是一个 chia 的绘图管理器 14 | 15 | 16 | ![The view of the manager](https://i.imgur.com/SmMDD0Q.png "View") 17 | 18 | 开发版本:v0.0.1 19 | 20 | 这是一个跨平台的 Chia 绘图管理器,可以在主流的操作系统上工作。它不是用来绘图的。绘图还是 chia 做的事情,这个库的目的是管理你的 chia 尽兴绘图,并使用你配置的 config.yaml 置来启动新的绘图。每个人的系统都不尽相同,所以定制是这个库中的一个重要特征(编写大家自己的 config.yaml)。 21 | 22 | 这个库很简单,易于使用,而且可靠,可以保持绘图的生成。 23 | 24 | 这个库已经在 Windows 和 Linux 上进行了测试,我本人(卧底小哥)在 mac 和 windows 上进行了测试 25 | 26 | ## 特点 27 | 28 | 错开你的绘图,这样你的计算机资源就可以避免高峰期。 29 | 允许一个目标目录列表。 30 | 通过交错时间提前启动一个新的绘图,最大限度地利用临时空间。 31 | 同时运行最大数量的绘图,以避免瓶颈或限制资源占用。 32 | 更深入的检测绘制过程。 33 | 34 | ## 支持/问题 35 | 36 | 请不要使用GitHub问题来提问或支持你自己的个人设置,问题应该与代码错误有关,因为它已经被许多人测试过,可以在 Windows、Linux 和 Mac OS 上工作。因此,任何与技术支持、配置设置有关的问题,或者与你自己的个人使用情况有关的问题,都应该在下面的任何一个链接中提问。 37 | 38 | Discord:[https://discord.gg/XyvMzeQpu2](https://discord.gg/XyvMzeQpu2) 39 | 这是官方 Discord 服务器 - Swar's Chia 社区 40 | 41 | 官方Chia Keybase团队:[https://keybase.io/team/chia_network.public](https://keybase.io/team/chia_network.public)频道是#swar 42 | 43 | GitHub讨论区: [https://github.com/swar/Swar-Chia-Plot-Manager/discussions](https://github.com/swar/Swar-Chia-Plot-Manager/discussions) 44 | 45 | 46 | ## 常见的问题 47 | 48 | 49 | **我可以重新加载我的配置吗?** 50 | - 是的,你的配置可以通过`python [manager.py](http://manager.py/) restart`命令来重新加载,或者单独停止并重新启动管理器。请注意,你的任务数将被重置!临时目录2和目标目录的顺序也将被重置。 51 | 52 | - 请注意,如果你改变了任务的任何一个目录,它将扰乱现有的任务,管理器和视图将无法识别旧的任务。如果你在有活动绘图的情况下改变任务目录,请将当前任务的 `max_plots` 改为0,然后用新目录做一个单独的任务。我不建议在计划运行时改变目录。 53 | 54 | **如果我停止绘图器,是否会杀死我的任务?** 55 | - 不会,绘图是在后台启动的,它们不会杀死你现有的绘图。如果你想结束它们,你可以访问PIDs,并手动结束它们。请注意,你还必须删除.tmp文件。我不为你处理这个问题。 56 | 57 | **如果我有一个列表,如何选择`temporary2`和目的地?** 58 | 59 | - 它们是按顺序选择的。如果你有两个目录,第一个绘图会选择第一个,第二个会选择第二个,而第三个会选择第一个,这样循环 1 2 1 2 1 2 1... 60 | 61 | **什么是 temporary2_destination_sync?** 62 | 63 | 一些用户喜欢选择总是拥有相同的`temporary2`和目标目录。启用这个设置将总是让 `temporary2` 作为目的地的磁盘。如果你使用这个设置,你可以使用一个空的 `temporary2` ] 64 | 65 | **对我的设置来说,什么是最好的配置?** 66 | 67 | - 请把这个问题转发给 `Keybase` 或在 github 添加讨论标签。 68 | 69 | 70 | 71 | ## 所有命令 72 | 73 | ##### 命令的使用实例 74 | ```文字 75 | > python3 manager.py start 76 | 77 | > python3 manager.py 重新启动 78 | 79 | > python3 manager.py stop 80 | 81 | > python3 manager.py view 82 | 83 | > python3 manager.py status 84 | 85 | > python3 manager.py analyze_logs 86 | ``` 87 | 88 | ### start 89 | 90 | 这个命令将在后台启动管理器。一旦你启动它,它就会一直运行,除非所有的作业都完成了`max_plots` 或者出现了错误。错误将被记录在一个创建的`debug.log`文件中。 91 | 92 | ### stop 93 | 94 | 这个命令将在后台终止管理器,它不会停止运行中的绘图,只会停止新绘图的创建。 95 | 96 | ### restart 97 | 98 | 该命令将依次运行启动和停止。 99 | 100 | ### view 101 | 102 | 该命令将显示你可以用来跟踪你正在运行的绘图的视图。这将在你的`config.yaml'中定义的每X秒更新一次。 103 | 104 | ### status 105 | 106 | 该命令将对视图进行一次快照,它不会循环。 107 | 108 | ### analyze_logs 109 | 110 | 该命令将分析你的日志文件夹中所有完成的绘图日志,并为你的计算机配置计算适当的权重。只需在你的`config.yaml`中的`progress`部分填入返回的值。这只影响进度条。 111 | 112 | 113 | ## 安装 114 | 115 | 这个库的安装是很简单的。我在下面附上了详细的说明: 116 | 117 | 1、下载并安装Python 3.7或更高版本:[https://www.python.org/](https://www.python.org/) 118 | 119 | 2、`git clone` 命令克隆这个库或者直接网页下载它(download zip)。 120 | 121 | 3、打开 CommandPrompt / PowerShell / Terminal,等任何命令行工具,然后 `cd` 到主库文件夹。 122 | 123 | - 例如:`cd C:\Users\Swar\Documents\Swar-Chia-Plot-Manager` 124 | 125 | 4、可选:为Python创建一个虚拟环境。如果你用Python做其他事情,建议这样做: 126 | 127 | - 创建一个新的 `Python` 环境: `python -m venv venv` 128 | 129 | - 第二个venv可以重命名为你想要的任何东西。作者更喜欢用venv,因为它是一个规范。 130 | 131 | - 激活这个虚拟环境。每次打开新窗口时都必须这样做。 132 | - Windows:`venv\Scripts\activate` 133 | - Linux: `./venv/bin/activate` 或 `source ./venv/bin/activate` 134 | 135 | - 通过看到(venv)的前缀来确认它已经激活了,前缀会根据你给它起的名字(venv)而改变。 136 | 137 | 5、安装所需模块: `pip install -r requirements.txt` 138 | 139 | 6、在同一目录下复制 `config.yaml.default` 并命名为 `config.yaml` 140 | 141 | 7、按照你自己的个人设置编辑和设置`config.yaml` 下面有更多关于这方面的帮助 142 | 143 | - 你还需要添加 `chia_location!` 这应该指向你的chia可执行文件(注意这个不是环境变量,这个工具不需要你配置环境变量) 144 | 145 | 8、运行管理器: `python manager.py start` 146 | 147 | - 这将在后台启动一个进程,根据你输入的设置来管理绘图。 148 | 149 | 9、运行视图: `python manager.py view` 150 | 151 | - 这将循环查看检测屏幕上的正在活动地块的详细信息。 152 | 153 | ## 配置 154 | 155 | 这个库的配置对每个终端用户都是独一无二的 `config.yaml` 文件是配置的所在。 156 | 157 | 这个绘图管理器是基于任务的概念来工作的。每个任务都有自己的设置,你可以对每个任务进行个性化设置,没有哪个是唯一的,所以这将为你提供灵活性。 158 | 159 | ### chia_location 160 | 161 | 这是一个变量,每个人可能都是不一样的,应该包含你的 `chia` 可执行文件的位置。这就是 chia 的可执行文件。 162 | 163 | - Windows的例子:`C:\Users\你的用户名\AppData\Local\chia-blockchain\app-你的chia版本\resources\app.asar.unpacked\daemon\chia.exe` 164 | 165 | - Linux的例子: `/usr/lib/chia-blockchain/resources/app.asar.unpacked/daemon/chia` 166 | 167 | - 另一个Linux例子: `/home/swar/chia-blockchain/venv/bin/chia` 168 | 169 | ### 管理 170 | 171 | 这些是只由绘制管理器使用的配置设置。 172 | 173 | - `check_interval` - 在检查是否有新任务开始之前的等待秒数。 174 | 175 | - `log_level` - 保持在 ` ERROR ` 上,只在有错误时记录。把它改为 `INFO`,以便看到更详细的日志记录。警告:`INFO` 会写下大量的信息。 176 | 177 | ### 日志 178 | 179 | - `folder_path` - 这是你的日志文件的文件夹,用于保存绘图。 180 | 181 | ### 视图 182 | 183 | 这些是视图将使用的设置 184 | 185 | - `check_interval` - 更新视图前的等待秒数。 186 | - `datetime_format` - 视图中希望显示的日期时间格式。格式化见这里:[https://docs.python.org/3/library/datetime.html#strftime-and-strptime-format-codes](https://docs.python.org/3/library/datetime.html#strftime-and-strptime-format-codes) 187 | - `include_seconds_for_phase` - 时间转换格式是否包括秒。 188 | - `include_drive_info` - 是否会显示驱动器相关信息。 189 | - `include_cpu` - 是否显示CPU的相关信息。 190 | - `include_ram` - 是否显示RAM的相关信息。 191 | - `include_plot_stats` - 是否会显示绘图统计相关信息。 192 | ### 通知 193 | 这些是不同的设置,以便在绘图管理器启动和绘图完成时发送通知。 194 | 195 | ### 进度 196 | - `phase_line_end` - 这些设置将用于决定一个阶段在进度条中的结束时间。它应该反映出该阶段结束的线,这样进度计算就可以使用该信息与现有的日志文件来计算进度百分比。 197 | - `phase_weight` - 这些是在进度计算中分配给每个阶段的权重,通常情况下,第1和第3阶段是最长的阶段,所以它们将比其他阶段拥有更多的权重。 198 | 199 | ### 全局 200 | - `max_concurrent` - 你的系统可以运行的最大绘图数量,管理器在一段时间内启动的地块总数不会超过这个数量。 201 | - `max_for_phase_1` - 系统在第一阶段可以运行的最大绘图数量。 202 | - `minimum_minutes_between_job` - 开始一个新的绘图任务前的最小分钟数,这可以防止多个工作在同一时间开始,这将缓解目标磁盘的拥堵,设置为0表示禁用。 203 | 204 | ## 任务 205 | - 这些是每个任务使用的设置,请注意,你可以有多个任务,每个任务都应该是YAML格式的,这样才能正确配置。这里几乎所有的值都将被传递到Chia可执行文件中。 206 | 207 | 点击这里参考更多关于Chia CLI的详情:[https://github.com/Chia-Network/chia-blockchain/wiki/CLI-Commands-Reference](https://github.com/Chia-Network/chia-blockchain/wiki/CLI-Commands-Reference) 208 | 209 | - `name` - 这是你要给的名字。 210 | - `max_plots` - 这是在管理器的一次运行中,任务的最大数量。任何重新启动管理器的操作都会重置这个变量,它在这里只是为了帮助你在这段时间内的绘图。 211 | - [OPTIONAL] `farmer_public_key` - 你的chia耕种公钥。如果没有提供,它将不会把这个变量传给chia执行程序,从而导致你的默认密钥被使用。只有当你在一台没有你的证书的机器上设置了chia时才需要这个。 212 | - [OPTIONAL] `pool_public_key` - 你的池公钥。与上述信息相同。 213 | - `temporary_directory` - 这里应该只传递一个目录。这是将进行绘图的地方。 214 | - [OPTIONAL]`temporary2_directory `- 可以是一个单一的值或一个值的列表。这是一个可选的参数,如果你想使用 Chia 绘图的 temporary2 目录功能,可以使用这个参数。 215 | - `destination_directory` - 可以是一个单一的值或一个值的列表。这是绘图完成后将被转移到的最终目录。如果你提供一个列表,它将逐一循环浏览每个磁盘。 216 | - `size` - 这指的是绘图的k大小。你可以在这里输入32、33、34、35......这样的内容(这取决于你之前的习惯) 217 | - `bitfield` - 这指的是你是否想在你的绘图中使用`bitfield`通常情况下,推荐使用 `true` 218 | - `threads` - 这是将分配给 plot 绘图的线程数。只有第1阶段使用1个以上的线程。(这里尤为注意,这是每个绘图任务的线程,对应 chia 官方的 2 自行改动) 219 | - `buckets` - 要使用的桶的数量。Chia提供的默认值是128。 220 | - `memory_buffer` - 你想分配给进程的内存数量。 221 | - `max_concurrent` - 这个任务在任何时候都要有的最大数量的绘图。 222 | - `max_concurrent_with_start_early` - 这项工作在任何时候拥有的最大绘图数量,包括提前开始的阶段。 223 | - `stagger_minutes` - 每个任务并发之间的交错时间单位 分钟。如果你想让你的 plot 在并发限制允许的情况下立即被启动,你甚至可以把它设置为零(为 0 就是同步开始,没有交错时间,不推荐设置为0,最佳 stagger 一般是平均速度的 1/6为最佳,这是我在 reddit 看到的测试) 224 | - `max_for_phase_1` - 这个任务在第1阶段的最大绘图数量。 225 | - `concurrency_start_early_phase` - 你想提前启动一个绘图的阶段。建议使用4。 226 | - `concurrency_start_early_phase_delay` - 当检测到提前开始阶段时,在新的绘图被启动之前的最大等待分钟数。 227 | 228 | - `temporary2_destination_sync` - 这个字段将始终提交目标目录作为临时2目录。这两个目录将是同步的,因此它们将总是以相同的值提交。 229 | 230 | - `exclude_final_directory` - 是否为收个几跳过最终目录 231 | 232 | - `destination_directory`进行耕作,(这是Chia的一个功能) 233 | - `skip_full_destinations` - 启用该功能时,它将计算所有正在运行的 plot 和未来 plot 的大小,以确定磁盘上是否有足够的空间来启动任务,如果没有,它将跳过该磁盘,转到下一个,一旦所有的空间都满了,它就会停用作业。 234 | - `unix_process_priority` - 仅限UNIX操作系统,这是 plot 生成时将被赋予的优先级。UNIX值必须在-20和19之间。该值越高,进程的优先级越低。 235 | - `windows_process_priority` - 仅限Windows操作系统,这是 plot 在生成时将被赋予的优先级。Windows的数值不同,应该设置为以下数值之一。 236 | - 16384 `below_normal_priority_class`(低于正常优先级)。 237 | - 32 `normal_priority_class`(正常优先级)。 238 | - 32768 "高于正常优先级"。 239 | - 128 "高优先级 "的 240 | - 256 "实时优先级"。 241 | - `enable_cpu_affinity` - 启用或禁用绘图进程的 cpu 亲和性,绘图和收割的系统在排除一个或两个线程的绘图进程时,可能会看到收割机或节点性能的改善。 242 | - `cpu_affinity` - 为绘图进程分配的cpu(或线程)的列表。默认例子假设你有一个超线程的4核CPU(8个逻辑核心)。这个配置将限制绘图进程使用逻辑核心0-5,把逻辑核心6和7留给其他进程(6个使用(限制6个),2个空闲)。 243 | -------------------------------------------------------------------------------- /config.yaml.default: -------------------------------------------------------------------------------- 1 | # This is a single variable that should contain the location of your chia executable file. This is the blockchain executable. 2 | # 3 | # WINDOWS EXAMPLE: C:\Users\Swar\AppData\Local\chia-blockchain\app-1.1.5\resources\app.asar.unpacked\daemon\chia.exe 4 | # LINUX EXAMPLE: /usr/lib/chia-blockchain/resources/app.asar.unpacked/daemon/chia 5 | # LINUX2 EXAMPLE: /home/swar/chia-blockchain/venv/bin/chia 6 | chia_location: 7 | 8 | 9 | manager: 10 | # These are the config settings that will only be used by the plot manager. 11 | # 12 | # check_interval: The number of seconds to wait before checking to see if a new job should start. 13 | # log_level: Keep this on ERROR to only record when there are errors. Change this to INFO in order to see more 14 | # detailed logging. Warning: INFO will write a lot of information. 15 | check_interval: 60 16 | log_level: ERROR 17 | 18 | 19 | log: 20 | # folder_path: This is the folder where your log files for plots will be saved. 21 | folder_path: S:\Chia\Logs\Plotter 22 | 23 | 24 | view: 25 | # These are the settings that will be used by the view. 26 | # 27 | # check_interval: The number of seconds to wait before updating the view. 28 | # datetime_format: The datetime format that you want displayed in the view. See here 29 | # for formatting: https://docs.python.org/3/library/datetime.html#strftime-and-strptime-format-codes 30 | # include_seconds_for_phase: This dictates whether seconds are included in the phase times. 31 | # include_drive_info: This dictates whether the drive information will be showed. 32 | # include_cpu: This dictates whether the CPU information will be showed. 33 | # include_ram: This dictates whether the RAM information will be showed. 34 | # include_plot_stats: This dictates whether the plot stats will be showed. 35 | check_interval: 60 36 | datetime_format: "%Y-%m-%d %H:%M:%S" 37 | include_seconds_for_phase: false 38 | include_drive_info: true 39 | include_cpu: true 40 | include_ram: true 41 | include_plot_stats: true 42 | 43 | 44 | notifications: 45 | # These are different settings in order to notified when the plot manager starts and when a plot has been completed. 46 | 47 | # DISCORD 48 | notify_discord: false 49 | discord_webhook_url: https://discord.com/api/webhooks/0000000000000000/XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX 50 | 51 | # PLAY AUDIO SOUND 52 | notify_sound: false 53 | song: audio.mp3 54 | 55 | # PUSHOVER PUSH SERVICE 56 | notify_pushover: false 57 | pushover_user_key: xx 58 | pushover_api_key: xx 59 | 60 | # TWILIO 61 | notify_twilio: false 62 | twilio_account_sid: xxxxx 63 | twilio_auth_token: xxxxx 64 | twilio_from_phone: +1234657890 65 | twilio_to_phone: +1234657890 66 | 67 | 68 | progress: 69 | # phase_line_end: These are the settings that will be used to dictate when a phase ends in the progress bar. It is 70 | # supposed to reflect the line at which the phase will end so the progress calculations can use that 71 | # information with the existing log file to calculate a progress percent. 72 | # phase_weight: These are the weight to assign to each phase in the progress calculations. Typically, Phase 1 and 3 73 | # are the longest phases so they will hold more weight than the others. 74 | phase1_line_end: 801 75 | phase2_line_end: 834 76 | phase3_line_end: 2474 77 | phase4_line_end: 2620 78 | phase1_weight: 33.4 79 | phase2_weight: 20.43 80 | phase3_weight: 42.29 81 | phase4_weight: 3.88 82 | 83 | 84 | global: 85 | # These are the settings that will be used globally by the plot manager. 86 | # 87 | # max_concurrent: The maximum number of plots that your system can run. The manager will not kick off more than this 88 | # number of plots total over time. 89 | max_concurrent: 10 90 | 91 | 92 | jobs: 93 | # These are the settings that will be used by each job. Please note you can have multiple jobs and each job should be 94 | # in YAML format in order for it to be interpreted correctly. Almost all the values here will be passed into the 95 | # Chia executable file. 96 | # 97 | # Check for more details on the Chia CLI here: https://github.com/Chia-Network/chia-blockchain/wiki/CLI-Commands-Reference 98 | # 99 | # name: This is the name that you want to give to the job. 100 | # max_plots: This is the maximum number of jobs to make in one run of the manager. Any restarts to manager will reset 101 | # this variable. It is only here to help with short term plotting. 102 | # 103 | # [OPTIONAL] farmer_public_key: Your farmer public key. If none is provided, it will not pass in this variable to the 104 | # chia executable which results in your default keys being used. This is only needed if 105 | # you have chia set up on a machine that does not have your credentials. 106 | # [OPTIONAL] pool_public_key: Your pool public key. Same information as the above. 107 | # 108 | # temporary_directory: Only a single directory should be passed into here. This is where the plotting will take place. 109 | # [OPTIONAL] temporary2_directory: Can be a single value or a list of values. This is an optional parameter to use in 110 | # case you want to use the temporary2 directory functionality of Chia plotting. 111 | # destination_directory: Can be a single value or a list of values. This is the final directory where the plot will be 112 | # transferred once it is completed. If you provide a list, it will cycle through each drive 113 | # one by one. 114 | # 115 | # size: This refers to the k size of the plot. You would type in something like 32, 33, 34, 35... in here. 116 | # bitfield: This refers to whether you want to use bitfield or not in your plotting. Typically, you want to keep 117 | # this as true. 118 | # threads: This is the number of threads that will be assigned to the plotter. Only phase 1 uses more than 1 thread. 119 | # buckets: The number of buckets to use. The default provided by Chia is 128. 120 | # memory_buffer: The amount of memory you want to allocate to the process. 121 | # max_concurrent: The maximum number of plots to have for this job at any given time. 122 | # max_concurrent_with_start_early: The maximum number of plots to have for this job at any given time including 123 | # phases that started early. 124 | # stagger_minutes: The amount of minutes to wait before the next job can get kicked off. You can even set this to 125 | # zero if you want your plots to get kicked off immediately when the concurrent limits allow for it. 126 | # max_for_phase_1: The maximum number of plots on phase 1 for this job. 127 | # concurrency_start_early_phase: The phase in which you want to start a plot early. It is recommended to use 4 for 128 | # this field. 129 | # concurrency_start_early_phase_delay: The maximum number of minutes to wait before a new plot gets kicked off when 130 | # the start early phase has been detected. 131 | # temporary2_destination_sync: This field will always submit the destination directory as the temporary2 directory. 132 | # These two directories will be in sync so that they will always be submitted as the 133 | # same value. 134 | - name: micron 135 | max_plots: 999 136 | farmer_public_key: 137 | pool_public_key: 138 | temporary_directory: Z:\Plotter 139 | temporary2_directory: 140 | destination_directory: J:\Plots 141 | size: 32 142 | bitfield: true 143 | threads: 8 144 | buckets: 128 145 | memory_buffer: 4000 146 | max_concurrent: 6 147 | max_concurrent_with_start_early: 7 148 | stagger_minutes: 60 149 | max_for_phase_1: 2 150 | concurrency_start_early_phase: 4 151 | concurrency_start_early_phase_delay: 0 152 | temporary2_destination_sync: false 153 | 154 | - name: inland 155 | max_plots: 999 156 | farmer_public_key: 157 | pool_public_key: 158 | temporary_directory: Y:\Plotter 159 | temporary2_directory: 160 | - J:\Plots 161 | - K:\Plots 162 | destination_directory: 163 | - J:\Plots 164 | - K:\Plots 165 | size: 32 166 | bitfield: true 167 | threads: 8 168 | buckets: 128 169 | memory_buffer: 4000 170 | max_concurrent: 2 171 | max_concurrent_with_start_early: 3 172 | stagger_minutes: 180 173 | max_for_phase_1: 1 174 | concurrency_start_early_phase: 4 175 | concurrency_start_early_phase_delay: 0 176 | temporary2_destination_sync: false 177 | -------------------------------------------------------------------------------- /plotmanager/library/utilities/processes.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | import platform 4 | import psutil 5 | import re 6 | import subprocess 7 | 8 | from copy import deepcopy 9 | from datetime import datetime 10 | 11 | from plotmanager.library.utilities.objects import Work 12 | 13 | 14 | def _contains_in_list(string, lst, case_insensitive=False): 15 | if case_insensitive: 16 | string = string.lower() 17 | for item in lst: 18 | if case_insensitive: 19 | item = item.lower() 20 | if string not in item: 21 | continue 22 | return True 23 | return False 24 | 25 | 26 | def get_manager_processes(): 27 | processes = [] 28 | for process in psutil.process_iter(): 29 | try: 30 | if not re.search(r'^pythonw?(?:\d+\.\d+|\d+)?(?:\.exe)?$', process.name(), flags=re.I): 31 | continue 32 | if not _contains_in_list('python', process.cmdline(), case_insensitive=True) or \ 33 | not _contains_in_list('stateless-manager.py', process.cmdline()): 34 | continue 35 | processes.append(process) 36 | except psutil.NoSuchProcess: 37 | pass 38 | return processes 39 | 40 | 41 | def is_windows(): 42 | return platform.system() == 'Windows' 43 | 44 | 45 | def get_chia_executable_name(): 46 | return f'chia{".exe" if is_windows() else ""}' 47 | 48 | 49 | def get_plot_k_size(commands): 50 | try: 51 | k_index = commands.index('-k') + 1 52 | except ValueError: 53 | return None 54 | return commands[k_index] 55 | 56 | 57 | def get_plot_directories(commands): 58 | try: 59 | temporary_index = commands.index('-t') + 1 60 | destination_index = commands.index('-d') + 1 61 | except ValueError: 62 | return None, None, None 63 | try: 64 | temporary2_index = commands.index('-2') + 1 65 | except ValueError: 66 | temporary2_index = None 67 | temporary_directory = commands[temporary_index] 68 | destination_directory = commands[destination_index] 69 | temporary2_directory = None 70 | if temporary2_index: 71 | temporary2_directory = commands[temporary2_index] 72 | return temporary_directory, temporary2_directory, destination_directory 73 | 74 | 75 | def get_plot_drives(commands, drives=None): 76 | if not drives: 77 | drives = get_system_drives() 78 | temporary_directory, temporary2_directory, destination_directory = get_plot_directories(commands=commands) 79 | temporary_drive = identify_drive(file_path=temporary_directory, drives=drives) 80 | destination_drive = identify_drive(file_path=destination_directory, drives=drives) 81 | temporary2_drive = None 82 | if temporary2_directory: 83 | temporary2_drive = identify_drive(file_path=temporary2_directory, drives=drives) 84 | return temporary_drive, temporary2_drive, destination_drive 85 | 86 | 87 | def get_chia_drives(): 88 | drive_stats = {'temp': {}, 'temp2': {}, 'dest': {}} 89 | chia_executable_name = get_chia_executable_name() 90 | for process in psutil.process_iter(): 91 | try: 92 | if chia_executable_name not in process.name() and 'python' not in process.name().lower(): 93 | continue 94 | except psutil.AccessDenied: 95 | continue 96 | try: 97 | if 'plots' not in process.cmdline() or 'create' not in process.cmdline(): 98 | continue 99 | except psutil.ZombieProcess: 100 | continue 101 | commands = process.cmdline() 102 | temporary_drive, temporary2_drive, destination_drive = get_plot_drives(commands=commands) 103 | if not temporary_drive and not destination_drive: 104 | continue 105 | 106 | if temporary_drive not in drive_stats['temp']: 107 | drive_stats['temp'][temporary_drive] = 0 108 | drive_stats['temp'][temporary_drive] += 1 109 | if destination_drive not in drive_stats['dest']: 110 | drive_stats['dest'][destination_drive] = 0 111 | drive_stats['dest'][destination_drive] += 1 112 | if temporary2_drive: 113 | if temporary2_drive not in drive_stats['temp2']: 114 | drive_stats['temp2'][temporary2_drive] = 0 115 | drive_stats['temp2'][temporary2_drive] += 1 116 | 117 | return drive_stats 118 | 119 | 120 | def get_system_drives(): 121 | drives = [] 122 | for disk in psutil.disk_partitions(): 123 | drive = disk.mountpoint 124 | if is_windows(): 125 | drive = os.path.splitdrive(drive)[0] 126 | drives.append(drive) 127 | drives.sort(reverse=True) 128 | return drives 129 | 130 | 131 | def identify_drive(file_path, drives): 132 | if not file_path: 133 | return None 134 | for drive in drives: 135 | if drive not in file_path: 136 | continue 137 | return drive 138 | return None 139 | 140 | 141 | def get_plot_id(file_path=None, contents=None): 142 | if not contents: 143 | f = open(file_path, 'r') 144 | contents = f.read() 145 | f.close() 146 | 147 | match = re.search(rf'^ID: (.*?)$', contents, flags=re.M) 148 | if match: 149 | return match.groups()[0] 150 | return None 151 | 152 | 153 | def get_temp_size(plot_id, temporary_directory, temporary2_directory): 154 | if not plot_id: 155 | return 0 156 | temp_size = 0 157 | directories = [] 158 | if temporary_directory: 159 | directories += [os.path.join(temporary_directory, file) for file in os.listdir(temporary_directory) if file] 160 | if temporary2_directory: 161 | directories += [os.path.join(temporary2_directory, file) for file in os.listdir(temporary2_directory) if file] 162 | for file_path in directories: 163 | if plot_id not in file_path: 164 | continue 165 | try: 166 | temp_size += os.path.getsize(file_path) 167 | except FileNotFoundError: 168 | pass 169 | return temp_size 170 | 171 | 172 | def get_running_plots(jobs, running_work): 173 | chia_processes = [] 174 | logging.info(f'Getting running plots') 175 | chia_executable_name = get_chia_executable_name() 176 | for process in psutil.process_iter(): 177 | try: 178 | if chia_executable_name not in process.name() and 'python' not in process.name().lower(): 179 | continue 180 | except psutil.AccessDenied: 181 | continue 182 | try: 183 | if 'plots' not in process.cmdline() or 'create' not in process.cmdline(): 184 | continue 185 | except psutil.ZombieProcess: 186 | continue 187 | if process.parent(): 188 | try: 189 | parent_commands = process.parent().cmdline() 190 | if 'plots' in parent_commands and 'create' in parent_commands: 191 | continue 192 | except (psutil.AccessDenied, psutil.ZombieProcess): 193 | pass 194 | logging.info(f'Found chia plotting process: {process.pid}') 195 | datetime_start = datetime.fromtimestamp(process.create_time()) 196 | chia_processes.append([datetime_start, process]) 197 | chia_processes.sort(key=lambda x: (x[0])) 198 | 199 | for datetime_start, process in chia_processes: 200 | logging.info(f'Finding log file for process: {process.pid}') 201 | log_file_path = None 202 | try: 203 | for file in process.open_files(): 204 | if '.mui' == file.path[-4:]: 205 | continue 206 | if file.path[-4:] not in ['.log', '.txt']: 207 | continue 208 | log_file_path = file.path 209 | logging.info(f'Found log file: {log_file_path}') 210 | break 211 | except (psutil.AccessDenied, RuntimeError): 212 | logging.info(f'Failed to find log file: {process.pid}') 213 | 214 | assumed_job = None 215 | logging.info(f'Finding associated job') 216 | 217 | temporary_directory, temporary2_directory, destination_directory = get_plot_directories(commands=process.cmdline()) 218 | for job in jobs: 219 | if temporary_directory != job.temporary_directory: 220 | continue 221 | if destination_directory not in job.destination_directory: 222 | continue 223 | if temporary2_directory: 224 | job_temporary2_directory = job.temporary2_directory 225 | if not isinstance(job.temporary2_directory, list): 226 | job_temporary2_directory = [job.temporary2_directory] 227 | if job.temporary2_destination_sync and temporary2_directory != destination_directory: 228 | continue 229 | if not job.temporary2_destination_sync and temporary2_directory not in job_temporary2_directory: 230 | continue 231 | logging.info(f'Found job: {job.name}') 232 | assumed_job = job 233 | break 234 | 235 | plot_id = None 236 | if log_file_path: 237 | plot_id = get_plot_id(file_path=log_file_path) 238 | 239 | temp_file_size = get_temp_size(plot_id=plot_id, temporary_directory=temporary_directory, 240 | temporary2_directory=temporary2_directory) 241 | 242 | temporary_drive, temporary2_drive, destination_drive = get_plot_drives(commands=process.cmdline()) 243 | k_size = get_plot_k_size(commands=process.cmdline()) 244 | work = deepcopy(Work()) 245 | work.job = assumed_job 246 | work.log_file = log_file_path 247 | work.datetime_start = datetime_start 248 | work.pid = process.pid 249 | work.plot_id = plot_id 250 | work.work_id = '?' 251 | if assumed_job: 252 | work.work_id = assumed_job.current_work_id 253 | assumed_job.current_work_id += 1 254 | assumed_job.total_running += 1 255 | assumed_job.running_work = assumed_job.running_work + [process.pid] 256 | work.temporary_drive = temporary_drive 257 | work.temporary2_drive = temporary2_drive 258 | work.destination_drive = destination_drive 259 | work.temp_file_size = temp_file_size 260 | work.k_size = k_size 261 | 262 | running_work[work.pid] = work 263 | logging.info(f'Finished finding running plots') 264 | 265 | return jobs, running_work 266 | 267 | 268 | def start_process(args, log_file): 269 | kwargs = {} 270 | if is_windows(): 271 | flags = 0 272 | flags |= 0x00000008 273 | kwargs = { 274 | 'creationflags': flags, 275 | } 276 | process = subprocess.Popen( 277 | args=args, 278 | stdout=log_file, 279 | stderr=log_file, 280 | shell=False, 281 | **kwargs, 282 | ) 283 | return process 284 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | The GNU General Public License is a free, copyleft license for 11 | software and other kinds of works. 12 | 13 | The licenses for most software and other practical works are designed 14 | to take away your freedom to share and change the works. By contrast, 15 | the GNU General Public License is intended to guarantee your freedom to 16 | share and change all versions of a program--to make sure it remains free 17 | software for all its users. We, the Free Software Foundation, use the 18 | GNU General Public License for most of our software; it applies also to 19 | any other work released this way by its authors. You can apply it to 20 | your programs, too. 21 | 22 | When we speak of free software, we are referring to freedom, not 23 | price. Our General Public Licenses are designed to make sure that you 24 | have the freedom to distribute copies of free software (and charge for 25 | them if you wish), that you receive source code or can get it if you 26 | want it, that you can change the software or use pieces of it in new 27 | free programs, and that you know you can do these things. 28 | 29 | To protect your rights, we need to prevent others from denying you 30 | these rights or asking you to surrender the rights. Therefore, you have 31 | certain responsibilities if you distribute copies of the software, or if 32 | you modify it: responsibilities to respect the freedom of others. 33 | 34 | For example, if you distribute copies of such a program, whether 35 | gratis or for a fee, you must pass on to the recipients the same 36 | freedoms that you received. You must make sure that they, too, receive 37 | or can get the source code. And you must show them these terms so they 38 | know their rights. 39 | 40 | Developers that use the GNU GPL protect your rights with two steps: 41 | (1) assert copyright on the software, and (2) offer you this License 42 | giving you legal permission to copy, distribute and/or modify it. 43 | 44 | For the developers' and authors' protection, the GPL clearly explains 45 | that there is no warranty for this free software. For both users' and 46 | authors' sake, the GPL requires that modified versions be marked as 47 | changed, so that their problems will not be attributed erroneously to 48 | authors of previous versions. 49 | 50 | Some devices are designed to deny users access to install or run 51 | modified versions of the software inside them, although the manufacturer 52 | can do so. This is fundamentally incompatible with the aim of 53 | protecting users' freedom to change the software. The systematic 54 | pattern of such abuse occurs in the area of products for individuals to 55 | use, which is precisely where it is most unacceptable. Therefore, we 56 | have designed this version of the GPL to prohibit the practice for those 57 | products. If such problems arise substantially in other domains, we 58 | stand ready to extend this provision to those domains in future versions 59 | of the GPL, as needed to protect the freedom of users. 60 | 61 | Finally, every program is threatened constantly by software patents. 62 | States should not allow patents to restrict development and use of 63 | software on general-purpose computers, but in those that do, we wish to 64 | avoid the special danger that patents applied to a free program could 65 | make it effectively proprietary. To prevent this, the GPL assures that 66 | patents cannot be used to render the program non-free. 67 | 68 | The precise terms and conditions for copying, distribution and 69 | modification follow. 70 | 71 | TERMS AND CONDITIONS 72 | 73 | 0. Definitions. 74 | 75 | "This License" refers to version 3 of the GNU General Public License. 76 | 77 | "Copyright" also means copyright-like laws that apply to other kinds of 78 | works, such as semiconductor masks. 79 | 80 | "The Program" refers to any copyrightable work licensed under this 81 | License. Each licensee is addressed as "you". "Licensees" and 82 | "recipients" may be individuals or organizations. 83 | 84 | To "modify" a work means to copy from or adapt all or part of the work 85 | in a fashion requiring copyright permission, other than the making of an 86 | exact copy. The resulting work is called a "modified version" of the 87 | earlier work or a work "based on" the earlier work. 88 | 89 | A "covered work" means either the unmodified Program or a work based 90 | on the Program. 91 | 92 | To "propagate" a work means to do anything with it that, without 93 | permission, would make you directly or secondarily liable for 94 | infringement under applicable copyright law, except executing it on a 95 | computer or modifying a private copy. Propagation includes copying, 96 | distribution (with or without modification), making available to the 97 | public, and in some countries other activities as well. 98 | 99 | To "convey" a work means any kind of propagation that enables other 100 | parties to make or receive copies. Mere interaction with a user through 101 | a computer network, with no transfer of a copy, is not conveying. 102 | 103 | An interactive user interface displays "Appropriate Legal Notices" 104 | to the extent that it includes a convenient and prominently visible 105 | feature that (1) displays an appropriate copyright notice, and (2) 106 | tells the user that there is no warranty for the work (except to the 107 | extent that warranties are provided), that licensees may convey the 108 | work under this License, and how to view a copy of this License. If 109 | the interface presents a list of user commands or options, such as a 110 | menu, a prominent item in the list meets this criterion. 111 | 112 | 1. Source Code. 113 | 114 | The "source code" for a work means the preferred form of the work 115 | for making modifications to it. "Object code" means any non-source 116 | form of a work. 117 | 118 | A "Standard Interface" means an interface that either is an official 119 | standard defined by a recognized standards body, or, in the case of 120 | interfaces specified for a particular programming language, one that 121 | is widely used among developers working in that language. 122 | 123 | The "System Libraries" of an executable work include anything, other 124 | than the work as a whole, that (a) is included in the normal form of 125 | packaging a Major Component, but which is not part of that Major 126 | Component, and (b) serves only to enable use of the work with that 127 | Major Component, or to implement a Standard Interface for which an 128 | implementation is available to the public in source code form. A 129 | "Major Component", in this context, means a major essential component 130 | (kernel, window system, and so on) of the specific operating system 131 | (if any) on which the executable work runs, or a compiler used to 132 | produce the work, or an object code interpreter used to run it. 133 | 134 | The "Corresponding Source" for a work in object code form means all 135 | the source code needed to generate, install, and (for an executable 136 | work) run the object code and to modify the work, including scripts to 137 | control those activities. However, it does not include the work's 138 | System Libraries, or general-purpose tools or generally available free 139 | programs which are used unmodified in performing those activities but 140 | which are not part of the work. For example, Corresponding Source 141 | includes interface definition files associated with source files for 142 | the work, and the source code for shared libraries and dynamically 143 | linked subprograms that the work is specifically designed to require, 144 | such as by intimate data communication or control flow between those 145 | subprograms and other parts of the work. 146 | 147 | The Corresponding Source need not include anything that users 148 | can regenerate automatically from other parts of the Corresponding 149 | Source. 150 | 151 | The Corresponding Source for a work in source code form is that 152 | same work. 153 | 154 | 2. Basic Permissions. 155 | 156 | All rights granted under this License are granted for the term of 157 | copyright on the Program, and are irrevocable provided the stated 158 | conditions are met. This License explicitly affirms your unlimited 159 | permission to run the unmodified Program. The output from running a 160 | covered work is covered by this License only if the output, given its 161 | content, constitutes a covered work. This License acknowledges your 162 | rights of fair use or other equivalent, as provided by copyright law. 163 | 164 | You may make, run and propagate covered works that you do not 165 | convey, without conditions so long as your license otherwise remains 166 | in force. You may convey covered works to others for the sole purpose 167 | of having them make modifications exclusively for you, or provide you 168 | with facilities for running those works, provided that you comply with 169 | the terms of this License in conveying all material for which you do 170 | not control copyright. Those thus making or running the covered works 171 | for you must do so exclusively on your behalf, under your direction 172 | and control, on terms that prohibit them from making any copies of 173 | your copyrighted material outside their relationship with you. 174 | 175 | Conveying under any other circumstances is permitted solely under 176 | the conditions stated below. Sublicensing is not allowed; section 10 177 | makes it unnecessary. 178 | 179 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 180 | 181 | No covered work shall be deemed part of an effective technological 182 | measure under any applicable law fulfilling obligations under article 183 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 184 | similar laws prohibiting or restricting circumvention of such 185 | measures. 186 | 187 | When you convey a covered work, you waive any legal power to forbid 188 | circumvention of technological measures to the extent such circumvention 189 | is effected by exercising rights under this License with respect to 190 | the covered work, and you disclaim any intention to limit operation or 191 | modification of the work as a means of enforcing, against the work's 192 | users, your or third parties' legal rights to forbid circumvention of 193 | technological measures. 194 | 195 | 4. Conveying Verbatim Copies. 196 | 197 | You may convey verbatim copies of the Program's source code as you 198 | receive it, in any medium, provided that you conspicuously and 199 | appropriately publish on each copy an appropriate copyright notice; 200 | keep intact all notices stating that this License and any 201 | non-permissive terms added in accord with section 7 apply to the code; 202 | keep intact all notices of the absence of any warranty; and give all 203 | recipients a copy of this License along with the Program. 204 | 205 | You may charge any price or no price for each copy that you convey, 206 | and you may offer support or warranty protection for a fee. 207 | 208 | 5. Conveying Modified Source Versions. 209 | 210 | You may convey a work based on the Program, or the modifications to 211 | produce it from the Program, in the form of source code under the 212 | terms of section 4, provided that you also meet all of these conditions: 213 | 214 | a) The work must carry prominent notices stating that you modified 215 | it, and giving a relevant date. 216 | 217 | b) The work must carry prominent notices stating that it is 218 | released under this License and any conditions added under section 219 | 7. This requirement modifies the requirement in section 4 to 220 | "keep intact all notices". 221 | 222 | c) You must license the entire work, as a whole, under this 223 | License to anyone who comes into possession of a copy. This 224 | License will therefore apply, along with any applicable section 7 225 | additional terms, to the whole of the work, and all its parts, 226 | regardless of how they are packaged. This License gives no 227 | permission to license the work in any other way, but it does not 228 | invalidate such permission if you have separately received it. 229 | 230 | d) If the work has interactive user interfaces, each must display 231 | Appropriate Legal Notices; however, if the Program has interactive 232 | interfaces that do not display Appropriate Legal Notices, your 233 | work need not make them do so. 234 | 235 | A compilation of a covered work with other separate and independent 236 | works, which are not by their nature extensions of the covered work, 237 | and which are not combined with it such as to form a larger program, 238 | in or on a volume of a storage or distribution medium, is called an 239 | "aggregate" if the compilation and its resulting copyright are not 240 | used to limit the access or legal rights of the compilation's users 241 | beyond what the individual works permit. Inclusion of a covered work 242 | in an aggregate does not cause this License to apply to the other 243 | parts of the aggregate. 244 | 245 | 6. Conveying Non-Source Forms. 246 | 247 | You may convey a covered work in object code form under the terms 248 | of sections 4 and 5, provided that you also convey the 249 | machine-readable Corresponding Source under the terms of this License, 250 | in one of these ways: 251 | 252 | a) Convey the object code in, or embodied in, a physical product 253 | (including a physical distribution medium), accompanied by the 254 | Corresponding Source fixed on a durable physical medium 255 | customarily used for software interchange. 256 | 257 | b) Convey the object code in, or embodied in, a physical product 258 | (including a physical distribution medium), accompanied by a 259 | written offer, valid for at least three years and valid for as 260 | long as you offer spare parts or customer support for that product 261 | model, to give anyone who possesses the object code either (1) a 262 | copy of the Corresponding Source for all the software in the 263 | product that is covered by this License, on a durable physical 264 | medium customarily used for software interchange, for a price no 265 | more than your reasonable cost of physically performing this 266 | conveying of source, or (2) access to copy the 267 | Corresponding Source from a network server at no charge. 268 | 269 | c) Convey individual copies of the object code with a copy of the 270 | written offer to provide the Corresponding Source. This 271 | alternative is allowed only occasionally and noncommercially, and 272 | only if you received the object code with such an offer, in accord 273 | with subsection 6b. 274 | 275 | d) Convey the object code by offering access from a designated 276 | place (gratis or for a charge), and offer equivalent access to the 277 | Corresponding Source in the same way through the same place at no 278 | further charge. You need not require recipients to copy the 279 | Corresponding Source along with the object code. If the place to 280 | copy the object code is a network server, the Corresponding Source 281 | may be on a different server (operated by you or a third party) 282 | that supports equivalent copying facilities, provided you maintain 283 | clear directions next to the object code saying where to find the 284 | Corresponding Source. Regardless of what server hosts the 285 | Corresponding Source, you remain obligated to ensure that it is 286 | available for as long as needed to satisfy these requirements. 287 | 288 | e) Convey the object code using peer-to-peer transmission, provided 289 | you inform other peers where the object code and Corresponding 290 | Source of the work are being offered to the general public at no 291 | charge under subsection 6d. 292 | 293 | A separable portion of the object code, whose source code is excluded 294 | from the Corresponding Source as a System Library, need not be 295 | included in conveying the object code work. 296 | 297 | A "User Product" is either (1) a "consumer product", which means any 298 | tangible personal property which is normally used for personal, family, 299 | or household purposes, or (2) anything designed or sold for incorporation 300 | into a dwelling. In determining whether a product is a consumer product, 301 | doubtful cases shall be resolved in favor of coverage. For a particular 302 | product received by a particular user, "normally used" refers to a 303 | typical or common use of that class of product, regardless of the status 304 | of the particular user or of the way in which the particular user 305 | actually uses, or expects or is expected to use, the product. A product 306 | is a consumer product regardless of whether the product has substantial 307 | commercial, industrial or non-consumer uses, unless such uses represent 308 | the only significant mode of use of the product. 309 | 310 | "Installation Information" for a User Product means any methods, 311 | procedures, authorization keys, or other information required to install 312 | and execute modified versions of a covered work in that User Product from 313 | a modified version of its Corresponding Source. The information must 314 | suffice to ensure that the continued functioning of the modified object 315 | code is in no case prevented or interfered with solely because 316 | modification has been made. 317 | 318 | If you convey an object code work under this section in, or with, or 319 | specifically for use in, a User Product, and the conveying occurs as 320 | part of a transaction in which the right of possession and use of the 321 | User Product is transferred to the recipient in perpetuity or for a 322 | fixed term (regardless of how the transaction is characterized), the 323 | Corresponding Source conveyed under this section must be accompanied 324 | by the Installation Information. But this requirement does not apply 325 | if neither you nor any third party retains the ability to install 326 | modified object code on the User Product (for example, the work has 327 | been installed in ROM). 328 | 329 | The requirement to provide Installation Information does not include a 330 | requirement to continue to provide support service, warranty, or updates 331 | for a work that has been modified or installed by the recipient, or for 332 | the User Product in which it has been modified or installed. Access to a 333 | network may be denied when the modification itself materially and 334 | adversely affects the operation of the network or violates the rules and 335 | protocols for communication across the network. 336 | 337 | Corresponding Source conveyed, and Installation Information provided, 338 | in accord with this section must be in a format that is publicly 339 | documented (and with an implementation available to the public in 340 | source code form), and must require no special password or key for 341 | unpacking, reading or copying. 342 | 343 | 7. Additional Terms. 344 | 345 | "Additional permissions" are terms that supplement the terms of this 346 | License by making exceptions from one or more of its conditions. 347 | Additional permissions that are applicable to the entire Program shall 348 | be treated as though they were included in this License, to the extent 349 | that they are valid under applicable law. If additional permissions 350 | apply only to part of the Program, that part may be used separately 351 | under those permissions, but the entire Program remains governed by 352 | this License without regard to the additional permissions. 353 | 354 | When you convey a copy of a covered work, you may at your option 355 | remove any additional permissions from that copy, or from any part of 356 | it. (Additional permissions may be written to require their own 357 | removal in certain cases when you modify the work.) You may place 358 | additional permissions on material, added by you to a covered work, 359 | for which you have or can give appropriate copyright permission. 360 | 361 | Notwithstanding any other provision of this License, for material you 362 | add to a covered work, you may (if authorized by the copyright holders of 363 | that material) supplement the terms of this License with terms: 364 | 365 | a) Disclaiming warranty or limiting liability differently from the 366 | terms of sections 15 and 16 of this License; or 367 | 368 | b) Requiring preservation of specified reasonable legal notices or 369 | author attributions in that material or in the Appropriate Legal 370 | Notices displayed by works containing it; or 371 | 372 | c) Prohibiting misrepresentation of the origin of that material, or 373 | requiring that modified versions of such material be marked in 374 | reasonable ways as different from the original version; or 375 | 376 | d) Limiting the use for publicity purposes of names of licensors or 377 | authors of the material; or 378 | 379 | e) Declining to grant rights under trademark law for use of some 380 | trade names, trademarks, or service marks; or 381 | 382 | f) Requiring indemnification of licensors and authors of that 383 | material by anyone who conveys the material (or modified versions of 384 | it) with contractual assumptions of liability to the recipient, for 385 | any liability that these contractual assumptions directly impose on 386 | those licensors and authors. 387 | 388 | All other non-permissive additional terms are considered "further 389 | restrictions" within the meaning of section 10. If the Program as you 390 | received it, or any part of it, contains a notice stating that it is 391 | governed by this License along with a term that is a further 392 | restriction, you may remove that term. If a license document contains 393 | a further restriction but permits relicensing or conveying under this 394 | License, you may add to a covered work material governed by the terms 395 | of that license document, provided that the further restriction does 396 | not survive such relicensing or conveying. 397 | 398 | If you add terms to a covered work in accord with this section, you 399 | must place, in the relevant source files, a statement of the 400 | additional terms that apply to those files, or a notice indicating 401 | where to find the applicable terms. 402 | 403 | Additional terms, permissive or non-permissive, may be stated in the 404 | form of a separately written license, or stated as exceptions; 405 | the above requirements apply either way. 406 | 407 | 8. Termination. 408 | 409 | You may not propagate or modify a covered work except as expressly 410 | provided under this License. Any attempt otherwise to propagate or 411 | modify it is void, and will automatically terminate your rights under 412 | this License (including any patent licenses granted under the third 413 | paragraph of section 11). 414 | 415 | However, if you cease all violation of this License, then your 416 | license from a particular copyright holder is reinstated (a) 417 | provisionally, unless and until the copyright holder explicitly and 418 | finally terminates your license, and (b) permanently, if the copyright 419 | holder fails to notify you of the violation by some reasonable means 420 | prior to 60 days after the cessation. 421 | 422 | Moreover, your license from a particular copyright holder is 423 | reinstated permanently if the copyright holder notifies you of the 424 | violation by some reasonable means, this is the first time you have 425 | received notice of violation of this License (for any work) from that 426 | copyright holder, and you cure the violation prior to 30 days after 427 | your receipt of the notice. 428 | 429 | Termination of your rights under this section does not terminate the 430 | licenses of parties who have received copies or rights from you under 431 | this License. If your rights have been terminated and not permanently 432 | reinstated, you do not qualify to receive new licenses for the same 433 | material under section 10. 434 | 435 | 9. Acceptance Not Required for Having Copies. 436 | 437 | You are not required to accept this License in order to receive or 438 | run a copy of the Program. Ancillary propagation of a covered work 439 | occurring solely as a consequence of using peer-to-peer transmission 440 | to receive a copy likewise does not require acceptance. However, 441 | nothing other than this License grants you permission to propagate or 442 | modify any covered work. These actions infringe copyright if you do 443 | not accept this License. Therefore, by modifying or propagating a 444 | covered work, you indicate your acceptance of this License to do so. 445 | 446 | 10. Automatic Licensing of Downstream Recipients. 447 | 448 | Each time you convey a covered work, the recipient automatically 449 | receives a license from the original licensors, to run, modify and 450 | propagate that work, subject to this License. You are not responsible 451 | for enforcing compliance by third parties with this License. 452 | 453 | An "entity transaction" is a transaction transferring control of an 454 | organization, or substantially all assets of one, or subdividing an 455 | organization, or merging organizations. If propagation of a covered 456 | work results from an entity transaction, each party to that 457 | transaction who receives a copy of the work also receives whatever 458 | licenses to the work the party's predecessor in interest had or could 459 | give under the previous paragraph, plus a right to possession of the 460 | Corresponding Source of the work from the predecessor in interest, if 461 | the predecessor has it or can get it with reasonable efforts. 462 | 463 | You may not impose any further restrictions on the exercise of the 464 | rights granted or affirmed under this License. For example, you may 465 | not impose a license fee, royalty, or other charge for exercise of 466 | rights granted under this License, and you may not initiate litigation 467 | (including a cross-claim or counterclaim in a lawsuit) alleging that 468 | any patent claim is infringed by making, using, selling, offering for 469 | sale, or importing the Program or any portion of it. 470 | 471 | 11. Patents. 472 | 473 | A "contributor" is a copyright holder who authorizes use under this 474 | License of the Program or a work on which the Program is based. The 475 | work thus licensed is called the contributor's "contributor version". 476 | 477 | A contributor's "essential patent claims" are all patent claims 478 | owned or controlled by the contributor, whether already acquired or 479 | hereafter acquired, that would be infringed by some manner, permitted 480 | by this License, of making, using, or selling its contributor version, 481 | but do not include claims that would be infringed only as a 482 | consequence of further modification of the contributor version. For 483 | purposes of this definition, "control" includes the right to grant 484 | patent sublicenses in a manner consistent with the requirements of 485 | this License. 486 | 487 | Each contributor grants you a non-exclusive, worldwide, royalty-free 488 | patent license under the contributor's essential patent claims, to 489 | make, use, sell, offer for sale, import and otherwise run, modify and 490 | propagate the contents of its contributor version. 491 | 492 | In the following three paragraphs, a "patent license" is any express 493 | agreement or commitment, however denominated, not to enforce a patent 494 | (such as an express permission to practice a patent or covenant not to 495 | sue for patent infringement). To "grant" such a patent license to a 496 | party means to make such an agreement or commitment not to enforce a 497 | patent against the party. 498 | 499 | If you convey a covered work, knowingly relying on a patent license, 500 | and the Corresponding Source of the work is not available for anyone 501 | to copy, free of charge and under the terms of this License, through a 502 | publicly available network server or other readily accessible means, 503 | then you must either (1) cause the Corresponding Source to be so 504 | available, or (2) arrange to deprive yourself of the benefit of the 505 | patent license for this particular work, or (3) arrange, in a manner 506 | consistent with the requirements of this License, to extend the patent 507 | license to downstream recipients. "Knowingly relying" means you have 508 | actual knowledge that, but for the patent license, your conveying the 509 | covered work in a country, or your recipient's use of the covered work 510 | in a country, would infringe one or more identifiable patents in that 511 | country that you have reason to believe are valid. 512 | 513 | If, pursuant to or in connection with a single transaction or 514 | arrangement, you convey, or propagate by procuring conveyance of, a 515 | covered work, and grant a patent license to some of the parties 516 | receiving the covered work authorizing them to use, propagate, modify 517 | or convey a specific copy of the covered work, then the patent license 518 | you grant is automatically extended to all recipients of the covered 519 | work and works based on it. 520 | 521 | A patent license is "discriminatory" if it does not include within 522 | the scope of its coverage, prohibits the exercise of, or is 523 | conditioned on the non-exercise of one or more of the rights that are 524 | specifically granted under this License. You may not convey a covered 525 | work if you are a party to an arrangement with a third party that is 526 | in the business of distributing software, under which you make payment 527 | to the third party based on the extent of your activity of conveying 528 | the work, and under which the third party grants, to any of the 529 | parties who would receive the covered work from you, a discriminatory 530 | patent license (a) in connection with copies of the covered work 531 | conveyed by you (or copies made from those copies), or (b) primarily 532 | for and in connection with specific products or compilations that 533 | contain the covered work, unless you entered into that arrangement, 534 | or that patent license was granted, prior to 28 March 2007. 535 | 536 | Nothing in this License shall be construed as excluding or limiting 537 | any implied license or other defenses to infringement that may 538 | otherwise be available to you under applicable patent law. 539 | 540 | 12. No Surrender of Others' Freedom. 541 | 542 | If conditions are imposed on you (whether by court order, agreement or 543 | otherwise) that contradict the conditions of this License, they do not 544 | excuse you from the conditions of this License. If you cannot convey a 545 | covered work so as to satisfy simultaneously your obligations under this 546 | License and any other pertinent obligations, then as a consequence you may 547 | not convey it at all. For example, if you agree to terms that obligate you 548 | to collect a royalty for further conveying from those to whom you convey 549 | the Program, the only way you could satisfy both those terms and this 550 | License would be to refrain entirely from conveying the Program. 551 | 552 | 13. Use with the GNU Affero General Public License. 553 | 554 | Notwithstanding any other provision of this License, you have 555 | permission to link or combine any covered work with a work licensed 556 | under version 3 of the GNU Affero General Public License into a single 557 | combined work, and to convey the resulting work. The terms of this 558 | License will continue to apply to the part which is the covered work, 559 | but the special requirements of the GNU Affero General Public License, 560 | section 13, concerning interaction through a network will apply to the 561 | combination as such. 562 | 563 | 14. Revised Versions of this License. 564 | 565 | The Free Software Foundation may publish revised and/or new versions of 566 | the GNU General Public License from time to time. Such new versions will 567 | be similar in spirit to the present version, but may differ in detail to 568 | address new problems or concerns. 569 | 570 | Each version is given a distinguishing version number. If the 571 | Program specifies that a certain numbered version of the GNU General 572 | Public License "or any later version" applies to it, you have the 573 | option of following the terms and conditions either of that numbered 574 | version or of any later version published by the Free Software 575 | Foundation. If the Program does not specify a version number of the 576 | GNU General Public License, you may choose any version ever published 577 | by the Free Software Foundation. 578 | 579 | If the Program specifies that a proxy can decide which future 580 | versions of the GNU General Public License can be used, that proxy's 581 | public statement of acceptance of a version permanently authorizes you 582 | to choose that version for the Program. 583 | 584 | Later license versions may give you additional or different 585 | permissions. However, no additional obligations are imposed on any 586 | author or copyright holder as a result of your choosing to follow a 587 | later version. 588 | 589 | 15. Disclaimer of Warranty. 590 | 591 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 592 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 593 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 594 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 595 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 596 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 597 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 598 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 599 | 600 | 16. Limitation of Liability. 601 | 602 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 603 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 604 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 605 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 606 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 607 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 608 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 609 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 610 | SUCH DAMAGES. 611 | 612 | 17. Interpretation of Sections 15 and 16. 613 | 614 | If the disclaimer of warranty and limitation of liability provided 615 | above cannot be given local legal effect according to their terms, 616 | reviewing courts shall apply local law that most closely approximates 617 | an absolute waiver of all civil liability in connection with the 618 | Program, unless a warranty or assumption of liability accompanies a 619 | copy of the Program in return for a fee. 620 | 621 | END OF TERMS AND CONDITIONS 622 | 623 | How to Apply These Terms to Your New Programs 624 | 625 | If you develop a new program, and you want it to be of the greatest 626 | possible use to the public, the best way to achieve this is to make it 627 | free software which everyone can redistribute and change under these terms. 628 | 629 | To do so, attach the following notices to the program. It is safest 630 | to attach them to the start of each source file to most effectively 631 | state the exclusion of warranty; and each file should have at least 632 | the "copyright" line and a pointer to where the full notice is found. 633 | 634 | 635 | Copyright (C) 636 | 637 | This program is free software: you can redistribute it and/or modify 638 | it under the terms of the GNU General Public License as published by 639 | the Free Software Foundation, either version 3 of the License, or 640 | (at your option) any later version. 641 | 642 | This program is distributed in the hope that it will be useful, 643 | but WITHOUT ANY WARRANTY; without even the implied warranty of 644 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 645 | GNU General Public License for more details. 646 | 647 | You should have received a copy of the GNU General Public License 648 | along with this program. If not, see . 649 | 650 | Also add information on how to contact you by electronic and paper mail. 651 | 652 | If the program does terminal interaction, make it output a short 653 | notice like this when it starts in an interactive mode: 654 | 655 | Copyright (C) 656 | This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 657 | This is free software, and you are welcome to redistribute it 658 | under certain conditions; type `show c' for details. 659 | 660 | The hypothetical commands `show w' and `show c' should show the appropriate 661 | parts of the General Public License. Of course, your program's commands 662 | might be different; for a GUI interface, you would use an "about box". 663 | 664 | You should also get your employer (if you work as a programmer) or school, 665 | if any, to sign a "copyright disclaimer" for the program, if necessary. 666 | For more information on this, and how to apply and follow the GNU GPL, see 667 | . 668 | 669 | The GNU General Public License does not permit incorporating your program 670 | into proprietary programs. If your program is a subroutine library, you 671 | may consider it more useful to permit linking proprietary applications with 672 | the library. If this is what you want to do, use the GNU Lesser General 673 | Public License instead of this License. But first, please read 674 | . 675 | --------------------------------------------------------------------------------