├── 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 | 
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 |
--------------------------------------------------------------------------------