123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938 |
- #!/usr/bin/env python
- # -*- coding: utf-8 -*-
- # Automatic assignment handler for Mlucas.
- # This handles LL and PRP testing (first-time and double-check), i.e. all the worktypes supported by the program.
- # EWM: adapted from https://github.com/MarkRose/primetools/blob/master/mfloop.py by teknohog and Mark Rose, with help rom Gord Palameta.
- # 2020: support for computer registration and assignment-progress via direct Primenet-v5-API calls by Loïc Le Loarer <loic@le-loarer.org>.
- # 2021: support for p-1 assignment fetch (Pfactor= and Pminus1= formats) and results reporting (worktyp = PM1) added by EWM for Mlucas v20 release.
- # This script is intended to be run alongside Mlucas - use it to register your computer (if you've not previously done so)
- # and then reinvoke in periodic-update mode to automatically fetch work from the Primenet server, report latest results and
- # report the status of currently-in-progress assignments to the server, which you can view in a convenient dashboard form via
- # login to the server and clicking Account/Team Info --> My Account --> CPUs. (Or directly via URL: https://www.mersenne.org/cpus/)
- ################################################################################
- # #
- # (C) 2017-2020 by Ernst W. Mayer. #
- # #
- # This program is free software; you can redistribute it and/or modify it #
- # under the terms of the GNU General Public License as published by the #
- # Free Software Foundation; either version 2 of the License, or (at your #
- # option) any later version. #
- # #
- # This program is distributed in the hope that it will be useful, but WITHOUT #
- # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or #
- # FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for #
- # more details. #
- # #
- # You should have received a copy of the GNU General Public License along #
- # with this program; see the file GPL.txt. If not, you may view one at #
- # http://www.fsf.org/licenses/licenses.html, or obtain one by writing to the #
- # Free Software Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA #
- # 02111-1307, USA. #
- # #
- ################################################################################
- from __future__ import division, print_function
- import sys
- import os.path
- import re
- from time import sleep
- from optparse import OptionParser, OptionGroup
- from hashlib import sha256
- import json
- import platform
- # More python3-backward-incompatibility-breakage-related foo - thanks to Gord Palameta for the workaround:
- try:
- # Python3
- import http.cookiejar as cookiejar
- from urllib.error import URLError, HTTPError
- from urllib.parse import urlencode
- from urllib.request import build_opener, install_opener, urlopen
- from urllib.request import HTTPCookieProcessor
- except ImportError:
- # Python2
- import cookielib as cookiejar
- from urllib2 import URLError, HTTPError
- from urllib import urlencode
- from urllib2 import build_opener, install_opener, urlopen
- from urllib2 import HTTPCookieProcessor
- try:
- from configparser import ConfigParser, Error as ConfigParserError
- except ImportError:
- from ConfigParser import ConfigParser, Error as ConfigParserError # ver. < 3.0
- from collections import namedtuple
- if sys.version_info[:2] >= (3,7):
- # If is OK to use dict in 3.7+ because insertion order is garantied to be preserved
- # Since it is also faster, it is better to use raw dict()
- OrderedDict = dict
- else:
- try:
- from collections import OrderedDict
- except ImportError:
- # For python2.6 and before which don't have OrderedDict
- try:
- from ordereddict import OrderedDict
- except ImportError:
- # Tests will not work correctly but it doesn't affect the functionnality
- OrderedDict = dict
- primenet_v5_burl = "http://v5.mersenne.org/v5server/?"
- primenet_v5_bargs = OrderedDict((("px", "GIMPS"), ("v", 0.95)))
- primenet_baseurl = "https://www.mersenne.org/"
- primenet_login = False
- class primenet_api:
- ERROR_OK = 0
- ERROR_SERVER_BUSY = 3
- ERROR_INVALID_VERSION = 4
- ERROR_INVALID_TRANSACTION = 5
- ERROR_INVALID_PARAMETER = 7 # Returned for length, type, or character invalidations.
- ERROR_ACCESS_DENIED = 9
- ERROR_DATABASE_FULL_OR_BROKEN = 13
- # Account related errors:
- ERROR_INVALID_USER = 21
- # Computer cpu/software info related errors:
- ERROR_OBSOLETE_CLIENT = 31
- ERROR_UNREGISTERED_CPU = 30
- ERROR_STALE_CPU_INFO = 32
- ERROR_CPU_IDENTITY_MISMATCH = 33
- ERROR_CPU_CONFIGURATION_MISMATCH = 34
- # Work assignment related errors:
- ERROR_NO_ASSIGNMENT = 40
- ERROR_INVALID_ASSIGNMENT_KEY = 43
- ERROR_INVALID_ASSIGNMENT_TYPE = 44
- ERROR_INVALID_RESULT_TYPE = 45
- ERROR_INVALID_WORK_TYPE = 46
- ERROR_WORK_NO_LONGER_NEEDED = 47
- PRIMENET_AR_NO_RESULT = 0 # No result, just sending done msg
- PRIMENET_AR_TF_FACTOR = 1 # Trial factoring, factor found
- PRIMENET_AR_P1_FACTOR = 2 # P-1, factor found
- PRIMENET_AR_ECM_FACTOR = 3 # ECM, factor found
- PRIMENET_AR_TF_NOFACTOR = 4 # Trial Factoring no factor found
- PRIMENET_AR_P1_NOFACTOR = 5 # P-1 Factoring no factor found
- PRIMENET_AR_ECM_NOFACTOR = 6 # ECM Factoring no factor found
- PRIMENET_AR_LL_RESULT = 100 # LL result, not prime
- PRIMENET_AR_LL_PRIME = 101 # LL result, Mersenne prime
- PRIMENET_AR_PRP_RESULT = 150 # PRP result, not prime
- PRIMENET_AR_PRP_PRIME = 151 # PRP result, probably prime
- def debug_print(text, file=sys.stdout):
- if options.debug or file == sys.stderr:
- caller_name = sys._getframe(1).f_code.co_name
- if caller_name == '<module>':
- caller_name = 'main loop'
- caller_string = caller_name + ": "
- print(progname + ": " + caller_string + str(text), file=file)
- file.flush()
- def greplike(pattern, l):
- output = []
- for line in l:
- s = pattern.search(line)
- if s:
- output.append(s.group(0))
- return output
- def num_to_fetch(l, targetsize):
- num_existing = len(l)
- num_needed = targetsize - num_existing
- return max(num_needed, 0)
- def readonly_list_file(filename, mode="r"):
- # Used when there is no intention to write the file back, so don't
- # check or write lockfiles. Also returns a single string, no list.
- try:
- with open(filename, mode=mode) as File:
- contents = File.readlines()
- File.close()
- return [ x.rstrip() for x in contents ]
- except (IOError,OSError):
- return []
- def read_list_file(filename, mode="r"):
- return readonly_list_file(filename, mode=mode)
- def write_list_file(filename, l, mode="w"):
- # A "null append" is meaningful, as we can call this to clear the
- # lockfile. In this case the main file need not be touched.
- if not ( "a" in mode and len(l) == 0):
- newline = b'\n' if 'b' in mode else '\n'
- content = newline.join(l) + newline
- File = open(filename, mode)
- File.write(content)
- File.close()
- def primenet_fetch(num_to_get):
- if not primenet_login:
- return []
- # As of early 2018, here is the full list of assignment-type codes supported by the Primenet server; Mlucas
- # v20 (and thus this script) supports only the subset of these indicated by an asterisk in the left column.
- # The Pminus1 worktype is supported only as split from an LL|PRP assignment needing p-1 done, hence the **.
- # Supported assignment types may be specified via either their PrimeNet number code or the listed Mnemonic:
- # Worktype:
- # Code Mnemonic Description
- # ---- ----------------- -----------------------
- # 0 Whatever makes the most sense
- # 1 Trial factoring to low limits
- # 2 Trial factoring
- # 3 Pfactor P-1 factoring
- # ** 4 Pminus1 P-1 factoring
- # 5 ECM for first factor on Mersenne numbers
- # 6 ECM on Fermat numbers
- # 8 ECM on mersenne cofactors
- # *100 SmallestAvail Smallest available first-time tests
- # *101 DoubleCheck Double-checking
- # *102 WorldRecord World record primality tests
- # *104 100Mdigit 100M digit number to LL test (not recommended)
- # *150 SmallestAvailPRP First time PRP tests (Gerbicz)
- # *151 DoubleCheckPRP Doublecheck PRP tests (Gerbicz)
- # *152 WorldRecordPRP World record sized numbers to PRP test (Gerbicz)
- # *153 100MdigitPRP 100M digit number to PRP test (Gerbicz)
- # 160 PRP on Mersenne cofactors
- # 161 PRP double-checks on Mersenne cofactors
- # Convert mnemonic-form worktypes to corresponding numeric value, check worktype value vs supported ones:
- if options.worktype == "SmallestAvail":
- options.worktype = "100"
- elif options.worktype == "DoubleCheck":
- options.worktype = "101"
- elif options.worktype == "WorldRecord":
- options.worktype = "102"
- elif options.worktype == "100Mdigit":
- options.worktype = "104"
- if options.worktype == "SmallestAvailPRP":
- options.worktype = "150"
- elif options.worktype == "DoubleCheckPRP":
- options.worktype = "151"
- elif options.worktype == "WorldRecordPRP":
- options.worktype = "152"
- elif options.worktype == "100MdigitPRP":
- options.worktype = "153"
- supported = set(['100','101','102','104','150','151','152','153'])
- if not options.worktype in supported:
- debug_print("Unsupported/unrecognized worktype = " + options.worktype)
- return []
- assignment = OrderedDict((
- ("cores","1"),
- ("num_to_get", num_to_get),
- ("pref", options.worktype),
- ("exp_lo", ""),
- ("exp_hi", ""),
- ("B1", "Get Assignments")
- ))
- try:
- openurl = primenet_baseurl + "manual_assignment/?" + urlencode(assignment)
- debug_print("Fetching work via URL = "+openurl)
- r = primenet.open(openurl)
- return greplike(workpattern, [ line.decode('utf-8','replace') for line in r.readlines() ] )
- except URLError:
- debug_print("URL open error at primenet_fetch")
- return []
- def get_assignment(progress):
- w = read_list_file(workfile)
- tasks = greplike(workpattern, w)
- (percent, time_left) = None, None
- if progress is not None and type(progress) == tuple and len(progress) == 2:
- (percent, time_left) = progress # unpack update_progress output
- num_cache = int(options.num_cache)
- if percent is not None and percent >= int(options.percent_limit):
- num_cache += 1
- debug_print("Progress of current assignment is {0:.2f} and bigger than limit ({1}), so num_cache is increased by one to {2}".format(percent, options.percent_limit, num_cache))
- elif time_left is not None and time_left <= max(3*options.timeout, 24*3600):
- # use else if here is important,
- # time_left and percent increase are exclusive (don't want to do += 2)
- num_cache += 1
- debug_print("Time_left is {0} and smaller than limit ({1}), so num_cache is increased by one to {2}".format(time_left, max(3*options.timeout, 24*3600), num_cache))
- num_to_get = num_to_fetch(tasks, num_cache)
- if num_to_get < 1:
- debug_print(workfile + " already has " + str(len(tasks)) + " >= " + str(num_cache) + " entries, not getting new work")
- return 0
- debug_print("Fetching " + str(num_to_get) + " assignments")
- new_tasks = primenet_fetch(num_to_get)
- num_fetched = len(new_tasks)
- if num_fetched > 0:
- debug_print("Fetched {0} assignments:".format(num_fetched))
- for new_task in new_tasks:
- debug_print("{0}".format(new_task))
- write_list_file(workfile, new_tasks, "a")
- if num_fetched < num_to_get:
- debug_print("Error: Failed to obtain requested number of new assignments, " + str(num_to_get) + " requested, " + str(num_fetched) + " successfully retrieved")
- return num_fetched
- def mersenne_find(line, complete=True):
- # Pre-v19 old-style HRF-formatted result used "Program:..."; starting w/v19 JSON-formatted result uses "program",
- return re.search("[Pp]rogram", line)
- try:
- from statistics import median_low
- except ImportError:
- def median_low(mylist):
- sorts = sorted(mylist)
- length = len(sorts)
- return sorts[(length-1)//2]
- def parse_stat_file(p):
- statfile = 'p' + str(p) + '.stat'
- w = readonly_list_file(statfile) # appended line by line, no lock needed
- found = 0
- regex = re.compile("Iter# = (.+?) .*?(\d+\.\d+) (m?sec)/iter")
- list_usec_per_iter = []
- # get the 5 most recent Iter line
- for line in reversed(w):
- res = regex.search(line)
- if res:
- found += 1
- # keep the last iteration to compute the percent of progress
- if found == 1:
- iteration = int(res.group(1))
- usec_per_iter = float(res.group(2))
- unit = res.group(3)
- if unit == "sec":
- usec_per_iter *= 1000
- list_usec_per_iter.append(usec_per_iter)
- if found == 5: break
- if found == 0: return 0, None # iteration is 0, but don't know the estimated speed yet
- # take the media of the last grepped lines
- usec_per_iter = median_low(list_usec_per_iter)
- return iteration, usec_per_iter
- def parse_v5_resp(r):
- ans = dict()
- for line in r.splitlines():
- if line == "==END==": break
- option,_,value = line.partition("=")
- ans[option]=value
- return ans
- def send_request(guid, args):
- args["g"] = guid
- # to mimic mprime, it is necessary to add safe='"{}:,' argument to urlencode, in
- # particular to encode JSON in result submission. But safe is not supported by python2...
- url_args = urlencode(args)
- # Only really usefull for t = "uc", not for "ap", is it for "ar" ?
- url_args += "&ss=19191919&sh=ABCDABCDABCDABCDABCDABCDABCDABCD"
- try:
- # don't need to use primenet opener because this API doesn't have cookies
- r = urlopen(primenet_v5_burl+url_args)
- except HTTPError as e:
- debug_print("ERROR receiving answer to request: "+str(primenet_v5_burl+url_args), file=sys.stderr)
- debug_print(e, file=sys.stderr)
- return None
- except URLError as e:
- debug_print("ERROR connecting to server for request: "+str(primenet_v5_burl+url_args), file=sys.stderr)
- debug_print(e, file=sys.stderr)
- return None
- return parse_v5_resp(r.read().decode("utf-8","replace"))
- from random import getrandbits
- def create_new_guid():
- guid = hex(getrandbits(128))
- if guid[:2] == '0x': guid = guid[2:] # remove the 0x prefix
- if guid[-1] == 'L': guid = guid[:-1] # remove trailling 'L' in python2
- # add missing 0 to the beginning"
- guid = (32-len(guid))*"0" + guid
- return guid
- def register_instance(guid):
- # register the instance to server, guid is the instance identifier
- if options.username is None or options.hostname is None:
- parser.error("To register the instance, --username and --hostname are required")
- hardware_id = sha256(options.cpu_model.encode("utf-8")).hexdigest()[:32] # similar as mprime
- args = primenet_v5_bargs.copy()
- args["t"] = "uc" # update compute command
- args["a"] = "Linux64,Mlucas,v19" #
- if config.has_option("primenet", "sw_version"):
- args["a"] = config.get("primenet", "sw_version")
- args["wg"] = "" # only filled on Windows by mprime
- args["hd"] = hardware_id # 32 hex char (128 bits)
- args["c"] = options.cpu_model[:64] # CPU model (len between 8 and 64)
- args["f"] = options.features[:64] # CPU option (like asimd, max len 64)
- args["L1"] = options.L1 # L1 cache size in KBytes
- args["L2"] = options.L2 # L2 cache size in KBytes
- # if smaller or equal to 256,
- # server refuses to gives LL assignment
- args["np"] = options.np # number of cores
- args["hp"] = options.hp # number of hyperthreading cores
- args["m"] = options.memory # number of megabytes of physical memory
- args["s"] = options.frequency # CPU frequency
- args["h"] = 24 # pretend to run 24h/day
- args["r"] = 1000 # pretend to run at 100%
- args["u"] = options.username #
- args["cn"] = options.hostname[:20] # truncate to 20 char max
- if guid is None:
- guid = create_new_guid()
- result = send_request(guid, args)
- if result is None:
- parser.error("Error while registering on mersenne.org")
- elif int(result["pnErrorResult"]) != 0:
- parser.error("Error while registering on mersenne.org\nReason: "+result["pnErrorDetail"])
- config_write(config, guid=guid)
- print("GUID {guid} correctly registered with the following features:".format(guid=guid))
- print("Username: {0}".format(options.username))
- print("Hostname: {0}".format(options.hostname))
- print("CPU model: {0}".format(options.cpu_model))
- print("CPU features: {0}".format(options.features))
- print("CPU L1 cache size: {0}kB".format(options.L1))
- print("CPU L2 cache size: {0}kB".format(options.L2))
- print("CPU cores: {0}".format(options.np))
- print("CPU thread per core: {0}".format(options.hp))
- print("CPU frequency: {0}MHz".format(options.frequency))
- print("Memory size: {0}MB".format(options.memory))
- print("If you want to change the value, please rerun with the corresponding options or edit the local.ini file and rerun with --register option")
- print("You can see the result in this page:")
- print("https://www.mersenne.org/editcpu/?g={guid}".format(guid=guid))
- return
- def config_read():
- config = ConfigParser(dict_type=OrderedDict)
- try:
- config.read([localfile])
- except ConfigParserError as e:
- debug_print("ERROR reading {0} file:".format(localfile), file=sys.stderr)
- debug_print(e, file=sys.stderr)
- if not config.has_section("primenet"):
- # Create the section to avoid having to test for it later
- config.add_section("primenet")
- return config
- def get_guid(config):
- try:
- return config.get("primenet", "guid")
- except ConfigParserError:
- return None
- def config_write(config, guid=None):
- # generate a new local.ini file
- if guid is not None: # update the guid if necessary
- config.set("primenet", "guid", guid)
- with open(localfile, "w") as configfile:
- config.write(configfile)
- def merge_config_and_options(config, options):
- # getattr and setattr allow access to the options.xxxx values by name
- # which allow to copy all of them programmatically instead of having
- # one line per attribute. Only the attr_to_copy list need to be updated
- # when adding an option you want to copy from argument options to local.ini config.
- attr_to_copy = ["username", "password", "worktype", "num_cache", "percent_limit",
- "hostname", "cpu_model", "features", "frequency", "memory", "L1", "L2", "np", "hp"]
- updated = False
- for attr in attr_to_copy:
- # if "attr" has its default value in options, copy it from config
- attr_val = getattr(options, attr)
- if attr_val == parser.defaults[attr] \
- and config.has_option("primenet", attr):
- # If no option is given and the option exists in local.ini, take it from local.ini
- new_val = config.get("primenet", attr)
- # config file values are always str()
- # they need to be converted to the expected type from options
- if attr_val is not None:
- new_val = type(attr_val)(new_val)
- setattr(options, attr, new_val)
- elif attr_val is not None and (not config.has_option("primenet", attr) \
- or config.get("primenet", attr) != str(attr_val)):
- # If an option is given (even default value) and it is not already
- # identical in local.ini, update local.ini
- debug_print("update local.ini with {0}={1}".format(attr, attr_val))
- config.set("primenet", attr, str(attr_val))
- updated = True
- return updated
- Assignment = namedtuple('Assignment', "id p is_prp iteration usec_per_iter")
- def update_progress():
- w = readonly_list_file(workfile)
- tasks = greplike(workpattern, w)
- if not len(tasks): return # don't update if no worktodo
- config_updated = False
- # Treat the first assignment. Only this one is used to save the usec_per_iter
- # The idea is that the first assignment is having a .stat file with correct values
- # Most of the time, a later assignment would not have a .stat file to obtain information,
- # but if it has, it may come from an other computer if the user moved the files, and so
- # it doesn't have revelant values for speed estimation.
- # Using usec_per_iter from one p to another is a good estimation if both p are close enougth
- # if there is big gap, it will be other or under estimated.
- # Any idea for a better estimation of assignment duration when only p and type (LL or PRP) is known ?
- assignment = get_progress_assignment(tasks[0])
- usec_per_iter = assignment.usec_per_iter
- if usec_per_iter is not None:
- config.set("primenet", "usec_per_iter", "{0:.2f}".format(usec_per_iter))
- config_updated = True
- elif config.has_option("primenet", "usec_per_iter"):
- # If not speed available, get it from the local.ini file
- usec_per_iter = float(config.get("primenet", "usec_per_iter"))
- percent, time_left = compute_progress(assignment.p, assignment.iteration, usec_per_iter)
- debug_print("p:{0} is {1:.2f}% done".format(assignment.p, percent))
- if time_left is None:
- debug_print("Finish cannot be estimated")
- else:
- debug_print("Finish estimated in {0:.1f} days (used {1:.1f} msec/iter estimation)".format(time_left/3600/24, usec_per_iter))
- send_progress(assignment.id, assignment.is_prp, percent, time_left)
- # Do the other assignment accumulating the time_lefts
- cur_time_left = time_left
- for task in tasks[1:]:
- assignment = get_progress_assignment(task)
- percent, time_left = compute_progress(assignment.p, assignment.iteration, usec_per_iter)
- debug_print("p:{0} is {1:.2f}% done".format(assignment.p, percent))
- if time_left is None:
- debug_print("Finish cannot be estimated")
- else:
- cur_time_left += time_left
- debug_print("Finish estimated in {0:.1f} days (used {1:.1f} msec/iter estimation)".format(cur_time_left/3600/24, usec_per_iter))
- send_progress(assignment.id, assignment.is_prp, percent, cur_time_left)
- config_write(config)
- return percent, cur_time_left
- def get_progress_assignment(task):
- found = workpattern.search(task)
- if not found:
- # TODO: test this error
- debug_print("ERROR: Unable to extract valid Primenet assignment ID from entry in " + workfile + ": " + str(tasks[0]), file=sys.stderr)
- return
- assignment_id = found.group(2)
- is_prp = found.group(1) == "PRP"
- debug_print("type = {0}, assignment_id = {1}".format(found.group(1), assignment_id))
- found = task.split(",")
- idx = 3 if is_prp else 1
- if len(found) <= idx:
- debug_print("Unable to extract valid exponent substring from entry in " + workfile + ": " + str(task))
- return None, None
- # Extract the subfield containing the exponent, whose position depends on the assignment type:
- p = int(found[idx])
- iteration, usec_per_iter = parse_stat_file(p)
- return Assignment(assignment_id, p, is_prp, iteration, usec_per_iter)
- def compute_progress(p, iteration, usec_per_iter):
- percent = 100*float(iteration)/float(p)
- if usec_per_iter is None:
- return percent, None
- iteration_left = p - iteration
- time_left = int(usec_per_iter * iteration_left / 1000)
- return percent, time_left
- def send_progress(assignment_id, is_prp, percent, time_left, retry_count=0):
- guid = get_guid(config)
- if guid is None:
- debug_print("Cannot update, the registration is not done", file=sys.stderr)
- debug_print("Call primenet.py with --register option", file=sys.stderr)
- return
- if retry_count > 5: return
- # Assignment Progress fields:
- # g= the machine's GUID (32 chars, assigned by Primenet on 1st-contact from a given machine, stored in 'guid=' entry of local.ini file of rundir)
- #
- args=primenet_v5_bargs.copy()
- args["t"] = "ap" # update compute command
- # k= the assignment ID (32 chars, follows '=' in Primenet-geerated workfile entries)
- args["k"] = assignment_id
- # p= progress in %-done, 4-char format = xy.z
- args["p"] = "{0:.1f}".format(percent)
- # d= when the client is expected to check in again (in seconds ... )
- args["d"] = options.timeout if options.timeout else 24*3600
- # e= the ETA of completion in seconds, if unknown, just put 1 week
- args["e"] = time_left if time_left is not None else 7*24*3600
- # c= the worker thread of the machine ... always sets = 0 for now, elaborate later if desired
- args["c"] = 0
- # stage= LL in this case, although an LL test may be doing TF or P-1 work first so it's possible to be something besides LL
- if not is_prp:
- args["stage"] = "LL"
- retry = False
- result = send_request(guid, args)
- if result is None:
- debug_print("ERROR while updating on mersenne.org", file=sys.stderr)
- # Try again
- retry = True
- else:
- rc = int(result["pnErrorResult"])
- if rc == primenet_api.ERROR_OK:
- debug_print("Update correctly send to server")
- elif rc == primenet_api.ERROR_STALE_CPU_INFO:
- debug_print("STALE CPU INFO ERROR: re-send computer update")
- # rerun --register
- register_instance(guid)
- retry = True
- elif rc == primenet_api.ERROR_UNREGISTERED_CPU:
- debug_print("UNREGISTERED CPU ERROR: pick a new GUID and register again")
- # corrupted GUI: change GUID, and rerun --register
- register_instance(None)
- retry = True
- elif rc == primenet_api.ERROR_SERVER_BUSY:
- retry = True
- else:
- # TODO: treat more errors correctly in all send_request callers
- # primenet_api.ERROR_INVALID_ASSIGNMENT_KEY
- # primenet_api.ERROR_WORK_NO_LONGER_NEEDED
- # drop the assignment
- debug_print("ERROR while updating on mersenne.org", file=sys.stderr)
- debug_print("Code: "+str(rc), file=sys.stderr)
- debug_print("Reason: "+result["pnErrorDetail"], file=sys.stderr)
- if retry:
- return send_progress(assignment_id, is_prp, percent, time_left, retry_count+1)
- return
- def submit_one_line(sendline):
- """Submit one line"""
- try:
- ar = json.loads(sendline)
- is_json = True
- except json.decoder.JSONDecodeError:
- is_json = False
- guid = get_guid(config)
- if guid is not None and is_json:
- # If registered and the line is a JSON, submit using the v API
- # The result will be attributed to the registered computer
- sent = submit_one_line_v5(sendline, guid, ar)
- else:
- # The result will be attributed to "Manual testing"
- sent = submit_one_line_manually(sendline)
- return sent
- def get_result_type(ar):
- """Extract result type from JSON result"""
- # Cf. The Primenet API forum thread [https://mersenneforum.org/showthread.php?t=23992] for lists of codes:
- if ar['worktype'] == 'LL':
- if ar['status'] == 'P':
- return primenet_api.PRIMENET_AR_LL_PRIME
- else:
- return primenet_api.PRIMENET_AR_LL_RESULT
- elif ar['worktype'].startswith('PRP'):
- if ar['status'] == 'P':
- return primenet_api.PRIMENET_AR_PRP_PRIME
- else:
- return primenet_api.PRIMENET_AR_PRP_RESULT
- elif ar['worktype'] == 'PM1':
- if ar['status'] == 'F':
- return primenet_api.PRIMENET_AR_P1_FACTOR
- else:
- return primenet_api.PRIMENET_AR_P1_NOFACTOR
- else:
- raise ValueError("This is a bug in primenet.py, Unsupported worktype {0}".format(ar['worktype']))
- def submit_one_line_v5(sendline, guid, ar):
- """Submit one result line using V5 API, will be attributed to the computed identified by guid"""
- """Return False if the submission should be retried"""
- # JSON is required because assignment_id is necessary in that case
- # and it is not present in old output format.
- debug_print("Submitting using V5 API\n" + sendline)
- aid = ar['aid']
- result_type = get_result_type(ar)
- args = primenet_v5_bargs.copy()
- args["t"] = "ar" # assignment result
- args["k"] = ar['aid'] if 'aid' in ar else 0 # assignment id
- args["m"] = sendline # message is the complete JSON string
- args["r"] = result_type # result type
- args["d"] = 1 # done: 0 for no closing is used for partial results
- args["n"] = ar['exponent']
- if result_type in (primenet_api.PRIMENET_AR_LL_RESULT, primenet_api.PRIMENET_AR_LL_PRIME):
- if result_type == primenet_api.PRIMENET_AR_LL_RESULT:
- args["rd"] = ar['res64']
- if 'shift-count' in ar:
- args['sc'] = ar['shift-count']
- if 'error-code' in ar:
- args["ec"] = ar['error-code']
- elif result_type in (primenet_api.PRIMENET_AR_PRP_RESULT, primenet_api.PRIMENET_AR_PRP_PRIME):
- args.update((("A", 1), ("b", 2), ("c", -1)))
- if result_type == primenet_api.PRIMENET_AR_PRP_RESULT:
- args["rd"] = ar['res64']
- if 'error-code' in ar:
- args["ec"] = ar['error-code']
- if 'known-factors' in ar:
- args['nkf'] = len(ar['known-factors'])
- args["base"] = ar['worktype'][4:] # worktype == PRP-base
- if 'residue-type' in ar:
- args["rt"] = ar['residue-type']
- if 'shift-count' in ar:
- args['sc'] = ar['shift-count']
- if 'errors' in ar:
- args['gbz'] = 1
- elif result_type in frozenset([primenet_api.PRIMENET_AR_P1_FACTOR, primenet_api.PRIMENET_AR_P1_NOFACTOR]):
- tasks = readonly_list_file(workfile)
- if result_type == primenet_api.PRIMENET_AR_P1_NOFACTOR:
- args["d"] = 0
- args.update((("A", 1), ("b", 2), ("c", -1)))
- args['B1'] = ar['B1']
- if 'B2' in ar:
- args['B2'] = ar['B2']
- if result_type == primenet_api.PRIMENET_AR_P1_FACTOR:
- args["f"] = ar['factors'][0]
- args['fftlen'] = ar['fft-length']
- result = send_request(guid, args)
- if result is None:
- debug_print("ERROR while submitting result on mersenne.org: assignment_id={0}".format(aid), file=sys.stderr)
- # if this happens, the submission can be retried
- # since no answer has been received from the server
- return False
- elif int(result["pnErrorResult"]) == primenet_api.ERROR_OK:
- debug_print("Result correctly send to server: assignment_id={0}".format(aid))
- if result["pnErrorDetail"] != "SUCCESS":
- debug_print("server message: "+result["pnErrorDetail"])
- else: # non zero ERROR code
- debug_print("ERROR while submitting result on mersenne.org: assignment_id={0}".format(aid), file=sys.stderr)
- if int(result["pnErrorResult"]) is primenet_api.ERROR_UNREGISTERED_CPU:
- # should register again and retry
- debug_print("ERROR UNREGISTERED CPU: Please remove guid line from local.ini, run with --register and retry", file=sys.stderr)
- return False
- elif int(result["pnErrorResult"]) is primenet_api.ERROR_INVALID_PARAMETER:
- debug_print("INVALID PARAMETER: this is a bug in primenet.py, please notify the author", file=sys.stderr)
- debug_print("Reason: "+result["pnErrorDetail"], file=sys.stderr)
- return False
- else:
- # In all other error case, the submission must not be retried
- debug_print("Reason: "+result["pnErrorDetail"], file=sys.stderr)
- return True
- return True
- def submit_one_line_manually(sendline):
- """Submit results using manual testing, will be attributed to "Manual Testing" in mersenne.org"""
- debug_print("Submitting using manual results\n" + sendline)
- try:
- post_data = urlencode({"data": sendline}).encode('utf-8')
- r = primenet.open(primenet_baseurl + "manual_result/default.php", post_data)
- res = r.read()
- if b"Error" in res:
- res_str = res.decode("utf-8", "replace")
- ibeg = res_str.find("Error")
- iend = res_str.find("</div>", ibeg)
- print("Submission failed: '{0}'".format(res_str[ibeg:iend]))
- elif b"Accepted" in res:
- pass
- else:
- print("submit_work: Submission of results line '" + sendline + "' failed for reasons unknown - please try manual resubmission.")
- except URLError:
- debug_print("URL open ERROR")
- return True # EWM: Append entire results_send rather than just sent to avoid resubmitting
- # bad results (e.g. previously-submitted duplicates) every time the script executes.
- def submit_work():
- results_send = read_list_file(sentfile)
- # Only submit completed work, i.e. the exponent must not exist in worktodo file any more
- results = readonly_list_file(resultsfile) # appended line by line, no lock needed
- # EWM: Note that read_list_file does not need the file(s) to exist - nonexistent files simply yield 0-length rs-array entries.
- results = filter(mersenne_find, results) # remove nonsubmittable lines from list of possibles
- results_send = [line for line in results if line not in results_send] # if a line was previously submitted, discard
- # Only for new results, to be appended to results_sent
- sent = []
- if len(results_send) == 0:
- debug_print("No complete results found to send.")
- return
- # EWM: Switch to one-result-line-at-a-time submission to support error-message-on-submit handling:
- for sendline in results_send:
- is_sent = submit_one_line(sendline)
- if is_sent:
- sent.append(sendline)
- write_list_file(sentfile, sent, "a")
- #######################################################################################################
- #
- # Start main program here
- #
- #######################################################################################################
- parser = OptionParser(version="primenet.py 19.1", description=\
- """This program is used to fill worktodo.ini with assignments and send the results for Mlucas
- program. It also saves its configuration to local.ini file, so it is necessary to gives the arguments only the first time you call it. Arguments are recovered for local.ini if not given.
- If --register is given, it registers the current Mlucas instance to mersenne.org (see all the options identify your CPU correctly). Registering is optionnal, but if registered, the progress can be sent and your CPU monitored on your account on the website.
- Then, without --register, it fetches assignment and send results to mersenne.org using manual assignment process on a "timeout" basic, or only once if timeout=0.
- """
- )
- # options not saved to local.ini
- parser.add_option("-d", "--debug", action="count", dest="debug", default=False, help="Display debugging info")
- parser.add_option("-w", "--workdir", dest="workdir", default=".", help="Working directory with worktodo.ini and results.txt from mlucas, and local.ini created by this program. Default current directory")
- # all other options are saved to local.ini (except --register)
- parser.add_option("-u", "--username", dest="username", help="Primenet user name")
- parser.add_option("-p", "--password", dest="password", help="Primenet password")
- # -t is reserved for timeout, instead use -T for assignment-type preference:
- parser.add_option("-T", "--worktype", dest="worktype", default="101", help="Worktype code, default is 101 for double-check LL, alternatively 100 (smallest available first-time LL), 102 (world-record-sized first-time LL), 104 (100M digit number to LL test - not recommended), 150 (smallest available first-time PRP), 151 (double-check PRP), 152 (world-record-sized first-time PRP), 153 (100M digit number to PRP test - not recommended)")
- parser.add_option("-n", "--num_cache", dest="num_cache", type="int", default=2, help="Number of assignments to cache, default: %default")
- parser.add_option("-L", "--percent_limit", dest="percent_limit", type="int", default=90, help="Add one to num_cache when current assignment is already done at this percentage, default: %default")
- parser.add_option("-t", "--timeout", dest="timeout", type="int", default=60*60*6, help="Seconds to wait between network updates, default %default [6 hours]. Use 0 for a single update without looping.")
- group = OptionGroup(parser, "Registering Options: send to mersenne.org when registering, visible in CPUs in the website.")
- group.add_option("-r", "--register", action="store_true", dest="register", default=False, help="Register to mersenne.org, this allows sending regular updates and follow the progress on the website.")
- group.add_option("-H", "--hostname", dest="hostname", default=platform.node()[:20], help="Hostname name for mersenne.org, default: %default")
- # TODO: add detection for most parameter, including automatic change of the hardware
- group.add_option("-c", "--cpu_model", dest="cpu_model", default="cpu.unknown", help="CPU model, defautl: %default")
- group.add_option("--features", dest="features", default="", help="CPU features, default '%default'")
- group.add_option("--frequency", dest="frequency", type="int", default=100, help="CPU frequency in MHz, default: %default")
- group.add_option("-m", "--memory", dest="memory", type="int", default=0, help="memory size in MB, default: %default")
- group.add_option("--L1", dest="L1", type="int", default=8, help="L1 cache size, default: %default")
- group.add_option("--L2", dest="L2", type="int", default=512, help="L2 cache size, default: %default")
- group.add_option("--np", dest="np", type="int", default=1, help="number of processors, default: %default")
- group.add_option("--hp", dest="hp", type="int", default=0, help="number of hyperthreading cores (0 is unknown), default: %default")
- parser.add_option_group(group)
- (options, args) = parser.parse_args()
- progname = os.path.basename(sys.argv[0])
- workdir = os.path.expanduser(options.workdir)
- localfile = os.path.join(workdir, "local.ini")
- workfile = os.path.join(workdir, "worktodo.ini")
- resultsfile = os.path.join(workdir, "results.txt")
- # A cumulative backup
- sentfile = os.path.join(workdir, "results_sent.txt")
- # Good refs re. Python regexp: https://www.geeksforgeeks.org/pattern-matching-python-regex/, https://www.python-course.eu/re.php
- # pre-v19 only handled LL-test assignments starting with either DoubleCheck or Test, followed by =, and ending with 3 ,number pairs:
- #
- # workpattern = r"(DoubleCheck|Test)=.*(,[0-9]+){3}"
- #
- # v19 we add PRP-test support - both first-time and DC of these start with PRP=, the DCs tack on 2 more ,number pairs representing
- # the PRP base to use and the PRP test-type (the latter is a bit complex to explain here). Sample of the 4 worktypes supported by v19:
- #
- # Test=7A30B8B6C0FC79C534A271D9561F7DCC,89459323,76,1
- # DoubleCheck=92458E009609BD9E10577F83C2E9639C,50549549,73,1
- # PRP=BC914675C81023F252E92CF034BEFF6C,1,2,96364649,-1,76,0
- # PRP=51D650F0A3566D6C256B1679C178163E,1,2,81348457,-1,75,0,3,1
- #
- # and the obvious regexp pattern-modification is
- #
- # workpattern = r"(DoubleCheck|Test|PRP)=.*(,[0-9]+){3}"
- #
- # Here is where we get to the kind of complication the late baseball-philosopher Yogi Berra captured via his aphorism,
- # "In theory, theory and practice are the same. In practice, they're different". Namely, while the above regexp pattern
- # should work on all 4 assignment patterns, since each has a string of at least 3 comma-separated nonnegative ints somewhere
- # between the 32-hexchar assignment ID and end of the line, said pattern failed on the 3rd of the above 4 assignments,
- # apparently because when the regexp is done via the 'greplike' below, the (,[0-9]+){3} part of the pattern gets implicitly
- # tiled to the end of the input line. Assignment # 3 above happens to have a negative number among the final 3, thus the
- # grep fails. This weird behavior is not reproducible running Python in console mode:
- #
- # >>> import re
- # >>> s1 = "DoubleCheck=92458E009609BD9E10577F83C2E9639C,50549549,73,1"
- # >>> s2 = "Test=7A30B8B6C0FC79C534A271D9561F7DCC,89459323,76,1"
- # >>> s3 = "PRP=BC914675C81023F252E92CF034BEFF6C,1,2,96364649,-1,76,0"
- # >>> s4 = "PRP=51D650F0A3566D6C256B1679C178163E,1,2,81348457,-1,75,0,3,1"
- # >>> print re.search(r"(DoubleCheck|Test|PRP)=.*(,[0-9]+){3}" , s1)
- # <_sre.SRE_Match object at 0x1004bd250>
- # >>> print re.search(r"(DoubleCheck|Test|PRP)=.*(,[0-9]+){3}" , s2)
- # <_sre.SRE_Match object at 0x1004bd250>
- # >>> print re.search(r"(DoubleCheck|Test|PRP)=.*(,[0-9]+){3}" , s3)
- # <_sre.SRE_Match object at 0x1004bd250>
- # >>> print re.search(r"(DoubleCheck|Test|PRP)=.*(,[0-9]+){3}" , s4)
- # <_sre.SRE_Match object at 0x1004bd250>
- #
- # Anyhow, based on that I modified the grep pattern to work around the weirdness, by appending .* to the pattern, thus
- # changing things to "look for 3 comma-separated nonnegative ints somewhere in the assignment, followed by anything",
- # also now to specifically look for a 32-hexchar assignment ID preceding such a triplet, and to allow whitespace around
- # the =. The latter bit is not needed based on current server assignment format, just a personal aesthetic bias of mine:
- #
- workpattern = re.compile("(DoubleCheck|Test|PRP)\s*=\s*([0-9A-F]{32})(,[0-9]+){3}.*")
- # mersenne.org limit is about 4 KB; stay on the safe side
- sendlimit = 3000 # TODO: enforce this limit
- # adapted from http://stackoverflow.com/questions/923296/keeping-a-session-in-python-while-making-http-requests
- primenet_cj = cookiejar.CookieJar()
- primenet = build_opener(HTTPCookieProcessor(primenet_cj))
- # If debug is requested
- if options.debug > 1:
- # if urllib_debug is not present, don't try to activate the debugging
- try:
- import urllib_debug
- except ImportError:
- options.debug = 1
- if options.debug == 3:
- debug_print("Enable testing url request and responses")
- from urllib_debug import TestHTTPHandler, TestHTTPSHandler
- primenet = build_opener(HTTPCookieProcessor(primenet_cj), TestHTTPHandler, TestHTTPSHandler)
- my_opener = build_opener(TestHTTPHandler, TestHTTPSHandler)
- install_opener(my_opener)
- from random import seed
- seed(3)
- elif options.debug == 2:
- debug_print("Enable spying url request and responses")
- from urllib_debug import SpyHTTPHandler, SpyHTTPSHandler
- primenet = build_opener(HTTPCookieProcessor(primenet_cj), SpyHTTPHandler, SpyHTTPSHandler)
- my_opener = build_opener(SpyHTTPHandler, SpyHTTPSHandler)
- install_opener(my_opener)
- # load local.ini and update options
- config = config_read()
- config_updated = merge_config_and_options(config, options)
- # check options after merging so that if local.ini file is changed by hand,
- # values are also checked
- # TODO: check that input char are ascii or at least supported by the server
- if not (8 <= len(options.cpu_model) <= 64):
- parser.error("cpu_model must be between 8 and 64 characters")
- if options.hostname is not None and len(options.hostname) > 20:
- parser.error("hostname must be less than 21 characters")
- if options.features is not None and len(options.features) > 64:
- parser.error("features must be less than 64 characters")
- # write back local.ini if necessary
- if config_updated:
- debug_print("write local.ini")
- config_write(config)
- if options.register:
- # if guid already exist, recover it, this way, one can (re)register to change
- # the CPU model (changing instance name can only be done in the website)
- guid = get_guid(config)
- register_instance(guid)
- sys.exit(0)
- if options.username is None or options.password is None:
- parser.error("Username and password must be given")
- while True:
- # Log in to primenet
- try:
- login_data = OrderedDict((
- ("user_login", options.username),
- ("user_password", options.password),
- ))
- # TODO: login only if necessary:
- # TODO: when configuration has been changed to test the password
- # TODO: when getting assignments is necessary
- # TODO: on a monthly basis ?
- # This makes a POST instead of GET
- data = urlencode(login_data).encode('utf-8')
- r = primenet.open(primenet_baseurl + "default.php", data)
- if not (options.username + "<br>logged in").encode('utf-8') in r.read():
- primenet_login = False
- debug_print("ERROR: Login failed.")
- else:
- primenet_login = True
- except URLError:
- debug_print("Primenet URL open ERROR")
- if primenet_login:
- submit_work()
- progress = update_progress()
- got = get_assignment(progress)
- if got > 0:
- debug_print("Redo progress update to update the just obtained assignment")
- # Since assignment are obtain by manual assignment, it is important to update them
- # to mark them as belonging to the current computer.
- update_progress()
- if options.timeout <= 0:
- break
- try:
- sleep(options.timeout)
- except KeyboardInterrupt:
- break
- sys.exit(0)
- # vim: noexpandtab ts=4 sts=0 sw=0
|