Package psychopy :: Module info
[frames] | no frames]

Source Code for Module psychopy.info

  1  # -*- coding: utf-8 -*- 
  2  """Fetching data about the system""" 
  3  # Part of the PsychoPy library 
  4  # Copyright (C) 2010 Jonathan Peirce 
  5  # Distributed under the terms of the GNU General Public License (GPL). 
  6   
  7  import sys, os, time, platform 
  8   
  9  from psychopy import visual# imports for RuntimeInfo() 
 10  from psychopy.core import shellCall 
 11  from psychopy.ext import rush 
 12  from psychopy import __version__ as psychopyVersion 
 13  from pyglet.gl import gl_info 
 14  import numpy, scipy, matplotlib, pyglet 
 15  try: import ctypes 
 16  except: pass 
 17  try: import hashlib # python 2.5 
 18  except: import sha 
 19  import random 
 20   
 21   
22 -class RunTimeInfo(dict):
23 """Returns a snapshot of your configuration at run-time, for immediate or archival use. 24 25 Returns a dict-like object with info about PsychoPy, your experiment script, the system & OS, 26 your window and monitor settings (if any), python & packages, and openGL. 27 28 If you want to skip testing the refresh rate, use 'refreshTest=None' 29 30 Example usage: see runtimeInfo.py in coder demos. 31 32 :Author: 33 - 2010 written by Jeremy Gray, with input from Jon Peirce and Alex Holcombe 34 """
35 - def __init__(self, author=None, version=None, win=None, refreshTest='grating', 36 userProcsDetailed=False, verbose=False, randomSeed=None ):
37 """ 38 :Parameters: 39 40 win : *None*, False, :class:`~psychopy.visual.Window` instance 41 what window to use for refresh rate testing (if any) and settings. None -> temporary window using 42 defaults; False -> no window created, used, nor profiled; a Window() instance you have already created 43 44 author : *None*, string 45 None = try to autodetect first __author__ in sys.argv[0]; string = user-supplied author info (of an experiment) 46 47 version : *None*, string 48 None = try to autodetect first __version__ in sys.argv[0]; string = user-supplied version info (of an experiment) 49 50 verbose : *False*, True; how much detail to assess 51 52 refreshTest : None, False, True, *'grating'* 53 True or 'grating' = assess refresh average, median, and SD of 60 win.flip()s, using visual.getMsPerFrame() 54 'grating' = show a visual during the assessment; True = assess without a visual 55 56 userProcsDetailed: *False*, True 57 get details about concurrent user's processses (command, process-ID) 58 59 randomSeed: *None* 60 a way for the user to record, and optionally set, a random seed for making reproducible random sequences 61 'set:XYZ' will both record the seed, 'XYZ', and set it: random.seed('XYZ'); numpy.random.seed() is NOT set 62 None defaults to python default; 63 'time' = use time.time() as the seed, as obtained during RunTimeInfo() 64 randomSeed='set:time' will give a new random seq every time the script is run, with the seed recorded. 65 66 :Returns: 67 a flat dict (but with several groups based on key names): 68 69 psychopy : version, rush() availability 70 psychopyVersion, psychopyHaveExtRush 71 72 experiment : author, version, directory, name, current time-stamp, 73 SHA1 digest, VCS info (if any, svn or hg only), 74 experimentAuthor, experimentVersion, ... 75 76 system : hostname, platform, user login, count of users, user process info (count, cmd + pid), flagged processes 77 systemHostname, systemPlatform, ... 78 79 window : (see output; many details about the refresh rate, window, and monitor; units are noted) 80 windowWinType, windowWaitBlanking, ...windowRefreshTimeSD_ms, ... windowMonitor.<details>, ... 81 82 python : version of python, versions of key packages (numpy, scipy, matplotlib, pyglet, pygame) 83 pythonVersion, pythonScipyVersion, ... 84 85 openGL : version, vendor, rendering engine, plus info on whether several extensions are present 86 openGLVersion, ..., openGLextGL_EXT_framebuffer_object, ... 87 """ 88 89 dict.__init__(self) # this will cause an object to be created with all the same methods as a dict 90 91 self['psychopyVersion'] = psychopyVersion 92 self['psychopyHaveExtRush'] = rush(False) # NB: this looks weird, but avoids setting high-priority incidentally 93 94 self._setExperimentInfo(author, version, verbose, randomSeed) 95 self._setSystemUserInfo() 96 self._setCurrentProcessInfo(verbose, userProcsDetailed) 97 98 # need a window for frame-timing, and some openGL drivers want a window open 99 if win == None: # make a temporary window, later close it 100 win = visual.Window(fullscr=True, monitor="testMonitor") 101 refreshTest = 'grating' 102 usingTempWin = True 103 else: # either False, or we were passed a window instance, use it for timing and profile it: 104 usingTempWin = False 105 if win: 106 self._setWindowInfo(win, verbose, refreshTest, usingTempWin) 107 108 self['pythonVersion'] = sys.version.split()[0] 109 if verbose: 110 self._setPythonInfo() 111 if win: self._setOpenGLInfo() 112 if usingTempWin: 113 win.close() # close after doing openGL
114
115 - def _setExperimentInfo(self, author, version, verbose, randomSeedFlag=None):
116 # try to auto-detect __author__ and __version__ in sys.argv[0] (= the users's script) 117 if not author or not version: 118 f = open(sys.argv[0],'r') 119 lines = f.read() 120 f.close() 121 if not author and lines.find('__author__')>-1: 122 linespl = lines.splitlines() 123 while linespl[0].find('__author__') == -1: 124 linespl.pop(0) 125 auth = linespl[0] 126 if len(auth) and auth.find('=') > 0: 127 try: 128 author = str(eval(auth[auth.find('=')+1 :])) 129 except: 130 pass 131 if not version and lines.find('__version__')>-1: 132 linespl = lines.splitlines() 133 while linespl[0].find('__version__') == -1: 134 linespl.pop(0) 135 ver = linespl[0] 136 if len(ver) and ver.find('=') > 0: 137 try: 138 version = str(eval(ver[ver.find('=')+1 :])) 139 except: 140 pass 141 142 if author or verbose: 143 self['experimentAuthor'] = author 144 if version or verbose: 145 self['experimentAuthVersion'] = version 146 147 # script identity & integrity information: 148 self['experimentScript'] = os.path.basename(sys.argv[0]) # file name 149 scriptDir = os.path.dirname(os.path.abspath(sys.argv[0])) 150 self['experimentScript.directory'] = scriptDir 151 # sha1 digest, text-format compatibility 152 self['experimentScript.digestSHA1'] = _getSha1hexDigest(os.path.abspath(sys.argv[0])) 153 # subversion revision? 154 try: 155 svnrev, last, url = _getSvnVersion(os.path.abspath(sys.argv[0])) # svn revision 156 if svnrev: # or verbose: 157 self['experimentScript.svnRevision'] = svnrev 158 self['experimentScript.svnRevLast'] = last 159 self['experimentScript.svnRevURL'] = url 160 except: 161 pass 162 # mercurical revision? 163 try: 164 hgChangeSet = _getHgVersion(os.path.abspath(sys.argv[0])) 165 if hgChangeSet: # or verbose: 166 self['experimentScript.hgChangeSet'] = hgChangeSet 167 except: 168 pass 169 170 # when was this run? 171 self['experimentRunTime.epoch'] = time.time() # basis for default random.seed() 172 self['experimentRunTime'] = time.ctime(self['experimentRunTime.epoch'])+' '+time.tzname[time.daylight] # a "right now" time-stamp 173 174 # random.seed -- record the value, and initialize random.seed() if 'set:' 175 if randomSeedFlag: 176 randomSeedFlag = str(randomSeedFlag) 177 while randomSeedFlag.find('set: ') == 0: 178 randomSeedFlag = randomSeedFlag.replace('set: ','set:',1) # spaces between set: and value could be confusing after deleting 'set:' 179 randomSeed = randomSeedFlag.replace('set:','',1).strip() 180 if randomSeed in ['time']: 181 randomSeed = self['experimentRunTime.epoch'] 182 self['experimentRandomSeed.string'] = randomSeed 183 if randomSeedFlag.find('set:') == 0: 184 random.seed(self['experimentRandomSeed.string']) # seed it 185 self['experimentRandomSeed.isSet'] = True 186 else: 187 self['experimentRandomSeed.isSet'] = False 188 else: 189 self['experimentRandomSeed.string'] = None 190 self['experimentRandomSeed.isSet'] = False
191
192 - def _setSystemUserInfo(self):
193 # machine name 194 self['systemHostName'] = platform.node() 195 196 # platform name, etc 197 if sys.platform in ['darwin']: 198 OSXver, junk, architecture = platform.mac_ver() 199 platInfo = 'darwin '+OSXver+' '+architecture 200 # powerSource = ... 201 elif sys.platform in ['linux2']: 202 platInfo = 'linux2 '+platform.release() 203 # powerSource = ... 204 elif sys.platform in ['win32']: 205 platInfo = 'windowsversion='+repr(sys.getwindowsversion()) 206 # powerSource = ... 207 else: 208 platInfo = ' [?]' 209 # powerSource = ... 210 self['systemPlatform'] = platInfo 211 #self['systemPowerSource'] = powerSource 212 213 # count all unique people (user IDs logged in), and find current user name & UID 214 self['systemUser'],self['systemUserID'] = _getUserNameUID() 215 try: 216 users = shellCall("who -q").splitlines()[0].split() 217 self['systemUsersCount'] = len(set(users)) 218 except: 219 self['systemUsersCount'] = False 220 221 # when last rebooted? 222 try: 223 lastboot = shellCall("who -b").split() 224 self['systemRebooted'] = ' '.join(lastboot[2:]) 225 except: # windows 226 sysInfo = shellCall('systeminfo').splitlines() 227 lastboot = [line for line in sysInfo if line.find("System Up Time") == 0 or line.find("System Boot Time") == 0] 228 lastboot += ['[?]'] # put something in the list just in case 229 self['systemRebooted'] = lastboot[0].strip() 230 231 # is R available (for stats)? 232 try: 233 Rver,err = shellCall("R --version",stderr=True) 234 Rversion = Rver.splitlines()[0] 235 if Rversion.find('R version') == 0: 236 self['systemRavailable'] = Rversion.strip() 237 else: raise 238 except: 239 self['systemRavailable'] = False 240 241 """try: 242 import rpy2 243 self['systemRpy2'] = rpy2.__version__ 244 except: 245 self['systemRpy2'] = False 246 247 # openssl version--maybe redundant with python distribution info? 248 # for a sha1 digest, python's hashlib is better than a shell call to openssl 249 try: 250 self['systemOpenSSLVersion'],err = shellCall('openssl version',stderr=True) 251 if err: 252 raise 253 except: 254 self['systemOpenSSLVersion'] = None 255 """
256
257 - def _setCurrentProcessInfo(self, verbose=False, userProcsDetailed=False):
258 # what other processes are currently active for this user? 259 profileInfo = '' 260 appFlagList = [# flag these apps if active, case-insensitive match: 261 'Firefox','Safari','Explorer','Netscape', 'Opera', # web browsers can burn CPU cycles 262 'BitTorrent', 'iTunes', # but also matches iTunesHelper (add to ignore-list) 263 'mdimport', # can have high CPU 264 'Office', 'KeyNote', 'Pages', 'LaunchCFMApp', # productivity; on mac, MS Office (Word etc) can be launched by 'LaunchCFMApp' 265 'VirtualBox','VBoxClient', # virtual machine as host or client 266 'Parallels', 'Coherence', 'prl_client_app','prl_tools_service', 267 'VMware'] # just a guess 268 appIgnoreList = [# always ignore these, exact match: 269 'ps','login','-tcsh','bash', 'iTunesHelper'] 270 271 # assess concurrently active processes owner by the current user: 272 try: 273 # ps = process status, -c to avoid full path (potentially having spaces) & args, -U for user 274 if sys.platform in ['darwin']: 275 proc = shellCall("ps -c -U "+os.environ['USER']) 276 cmdStr = 'COMMAND' 277 elif sys.platform in ['linux2']: 278 proc = shellCall("ps -c -U "+os.environ['USER']) 279 cmdStr = 'CMD' 280 elif sys.platform in ['win32']: 281 proc, err = shellCall("tasklist", stderr=True) # "tasklist /m" gives modules as well 282 if err: 283 print 'tasklist error:', err 284 raise 285 else: # guess about freebsd based on darwin... 286 proc,err = shellCall("ps -U "+os.environ['USER'],stderr=True) 287 if err: raise 288 cmdStr = 'COMMAND' # or 'CMD'? 289 systemProcPsu = [] 290 systemProcPsuFlagged = [] 291 systemUserProcFlaggedPID = [] 292 procLines = proc.splitlines() 293 headerLine = procLines.pop(0) # column labels 294 if sys.platform not in ['win32']: 295 cmd = headerLine.split().index(cmdStr) # columns and column labels can vary across platforms 296 pid = headerLine.split().index('PID') # process id's extracted in case you want to os.kill() them from psychopy 297 else: # this works for win XP, for output from 'tasklist' 298 procLines.pop(0) # blank 299 procLines.pop(0) # ===== 300 pid = -5 # pid next after command, which can have 301 cmd = 0 # command is first, but can have white space, so end up taking line[0:pid] 302 for p in procLines: 303 pr = p.split() # info fields for this process 304 if pr[cmd] in appIgnoreList: 305 continue 306 if sys.platform in ['win32']: #allow for spaces in app names 307 systemProcPsu.append([' '.join(pr[cmd:pid]),pr[pid]]) # later just count these unless want details 308 else: 309 systemProcPsu.append([' '.join(pr[cmd:]),pr[pid]]) # 310 matchingApp = [a for a in appFlagList if p.lower().find(a.lower())>-1] 311 for app in matchingApp: 312 systemProcPsuFlagged.append([app, pr[pid]]) 313 systemUserProcFlaggedPID.append(pr[pid]) 314 self['systemUserProcCount'] = len(systemProcPsu) 315 self['systemUserProcFlagged'] = systemProcPsuFlagged 316 317 if verbose and userProcsDetailed: 318 self['systemUserProcCmdPid'] = systemProcPsu 319 self['systemUserProcFlaggedPID'] = systemUserProcFlaggedPID 320 except: 321 if verbose: 322 self['systemUserProcCmdPid'] = None 323 self['systemUserProcFlagged'] = None
324
325 - def _setWindowInfo(self, win, verbose=False, refreshTest='grating', usingTempWin=True):
326 """find and store info about the window: refresh rate, configuration info 327 """ 328 329 if refreshTest in ['grating', True]: 330 msPFavg, msPFstd, msPFmd6 = visual.getMsPerFrame(win, nFrames=120, showVisual=bool(refreshTest=='grating')) 331 self['windowRefreshTimeAvg_ms'] = msPFavg 332 self['windowRefreshTimeMedian_ms'] = msPFmd6 333 self['windowRefreshTimeSD_ms'] = msPFstd 334 if usingTempWin: 335 return 336 337 # These 'configuration lists' control what attributes are reported. 338 # All desired attributes/properties need a legal internal name, e.g., win.winType. 339 # If an attr is callable, its gets called with no arguments, e.g., win.monitor.getWidth() 340 winAttrList = ['winType', '_isFullScr', 'units', 'monitor', 'pos', 'screen', 'rgb', 'size'] 341 winAttrListVerbose = ['allowGUI', 'useNativeGamma', 'recordFrameIntervals','waitBlanking', '_haveShaders', '_refreshThreshold'] 342 if verbose: winAttrList += winAttrListVerbose 343 344 monAttrList = ['name', 'getDistance', 'getWidth', 'currentCalibName'] 345 monAttrListVerbose = ['_gammaInterpolator', '_gammaInterpolator2'] 346 if verbose: monAttrList += monAttrListVerbose 347 if 'monitor' in winAttrList: # replace 'monitor' with all desired monitor.<attribute> 348 i = winAttrList.index('monitor') # retain list-position info, put monitor stuff there 349 del(winAttrList[i]) 350 for monAttr in monAttrList: 351 winAttrList.insert(i, 'monitor.' + monAttr) 352 i += 1 353 for winAttr in winAttrList: 354 try: 355 attrValue = eval('win.'+winAttr) 356 except AttributeError: 357 log.warning('AttributeError in RuntimeInfo._setWindowInfo(): Window instance has no attribute', winAttr) 358 continue 359 if hasattr(attrValue, '__call__'): 360 try: 361 a = attrValue() 362 attrValue = a 363 except: 364 print 'Warning: could not get a value from win.'+winAttr+'() (expects arguments?)' 365 continue 366 while winAttr[0]=='_': 367 winAttr = winAttr[1:] 368 winAttr = winAttr[0].capitalize()+winAttr[1:] 369 winAttr = winAttr.replace('Monitor._','Monitor.') 370 if winAttr in ['Pos','Size']: 371 winAttr += '_pix' 372 if winAttr in ['Monitor.getWidth','Monitor.getDistance']: 373 winAttr += '_cm' 374 if winAttr in ['RefreshThreshold']: 375 winAttr += '_sec' 376 self['window'+winAttr] = attrValue
377
378 - def _setPythonInfo(self):
379 # External python packages: 380 self['pythonNumpyVersion'] = numpy.__version__ 381 self['pythonScipyVersion'] = scipy.__version__ 382 self['pythonMatplotlibVersion'] = matplotlib.__version__ 383 self['pythonPygletVersion'] = pyglet.__version__ 384 try: from pygame import __version__ as pygameVersion 385 except: pygameVersion = '(no pygame)' 386 self['pythonPygameVersion'] = pygameVersion 387 388 # Python gory details: 389 self['pythonFullVersion'] = sys.version.replace('\n',' ') 390 self['pythonExecutable'] = sys.executable
391
392 - def _setOpenGLInfo(self):
393 # OpenGL info: 394 self['openGLVendor'] = gl_info.get_vendor() 395 self['openGLRenderingEngine'] = gl_info.get_renderer() 396 self['openGLVersion'] = gl_info.get_version() 397 GLextensionsOfInterest=['GL_ARB_multitexture', 'GL_EXT_framebuffer_object','GL_ARB_fragment_program', 398 'GL_ARB_shader_objects','GL_ARB_vertex_shader', 'GL_ARB_texture_non_power_of_two','GL_ARB_texture_float'] 399 400 for ext in GLextensionsOfInterest: 401 self['openGLext.'+ext] = bool(gl_info.have_extension(ext))
402
403 - def __repr__(self):
404 """ Return a string that is a legal python (dict), and close to YAML, .ini, and configObj syntax 405 """ 406 info = '{\n#[ PsychoPy2 RuntimeInfoStart ]\n' 407 sections = ['PsychoPy', 'Experiment', 'System', 'Window', 'Python', 'OpenGL'] 408 for sect in sections: 409 info += ' #[[ %s ]] #---------\n' % (sect) 410 sectKeys = [k for k in self.keys() if k.lower().find(sect.lower()) == 0] 411 # get keys for items matching this section label; use reverse-alpha order if easier to read: 412 sectKeys.sort(key=str.lower, reverse=bool(sect in ['PsychoPy', 'Window', 'Python', 'OpenGL'])) 413 for k in sectKeys: 414 selfk = self[k] # alter a copy for display purposes 415 try: 416 if type(selfk) == type('abc'): 417 selfk = selfk.replace('"','').replace('\n',' ') 418 elif k.find('_ms')> -1: #type(selfk) == type(0.123): 419 selfk = "%.3f" % selfk 420 elif k.find('_sec')> -1: 421 selfk = "%.4f" % selfk 422 elif k.find('_cm')>-1: 423 selfk = "%.1f" % selfk 424 except: 425 pass 426 if k in ['systemUserProcFlagged','systemUserProcCmdPid'] and selfk is not None and len(selfk): # then strcat unique proc names 427 prSet = [] 428 for pr in self[k]: # str -> list of lists 429 if pr[0].find(' ')>-1: # add single quotes around file names that contain spaces 430 pr[0] = "'"+pr[0]+"'" 431 prSet += [pr[0]] # first item in sublist is proc name (CMD) 432 selfk = ' '.join(list(set(prSet))) 433 if k not in ['systemUserProcFlaggedPID']: # suppress display PID info -- useful at run-time, never useful in an archive 434 #if type(selfk) == type('abc'): 435 info += ' "%s": "%s",\n' % (k, selfk) 436 #else: 437 # info += ' "%s": %s,\n' % (k, selfk) 438 info += '#[ PsychoPy2 RuntimeInfoEnd ]\n}\n' 439 return info
440
441 - def __str__(self):
442 """ Return a string intended for printing to a log file 443 """ 444 infoLines = self.__repr__() 445 info = infoLines.splitlines()[1:-1] # remove enclosing braces from repr 446 for i,line in enumerate(info): 447 if line.find('openGLext')>-1: # swap order for OpenGL extensions -- much easier to read 448 tmp = line.split(':') 449 info[i] = ': '.join([' '+tmp[1].replace(',',''),tmp[0].replace(' ','')+',']) 450 info[i] = info[i].rstrip(',') 451 info = '\n'.join(info).replace('"','')+'\n' 452 return info
453
454 - def _type(self):
455 # for debugging 456 sk = self.keys() 457 sk.sort() 458 for k in sk: 459 print k,type(self[k]),self[k]
460
461 -def _getSvnVersion(file):
462 """Tries to discover the svn version (revision #) for a file. 463 464 Not thoroughly tested; completely untested on Windows Vista, Win 7, FreeBSD 465 466 :Author: 467 - 2010 written by Jeremy Gray 468 """ 469 if not (os.path.exists(file) and os.path.isdir(os.path.join(os.path.dirname(file),'.svn'))): 470 return None, None, None 471 svnRev, svnLastChangedRev, svnUrl = None, None, None 472 if sys.platform in ['darwin', 'linux2', 'freebsd']: 473 svninfo,stderr = shellCall('svn info "'+file+'"', stderr=True) # expects a filename, not dir 474 for line in svninfo.splitlines(): 475 if line.find('URL:') == 0: 476 svnUrl = line.split()[1] 477 elif line.find('Revision: ') == 0: 478 svnRev = line.split()[1] 479 elif line.find('Last Changed Rev') == 0: 480 svnLastChangedRev = line.split()[3] 481 else: # worked for me on Win XP sp2 with TortoiseSVN (SubWCRev.exe) 482 stdout,stderr = shellCall('subwcrev "'+file+'"', stderr=True) 483 for line in stdout.splitlines(): 484 if line.find('Last committed at revision') == 0: 485 svnRev = line.split()[4] 486 elif line.find('Updated to revision') == 0: 487 svnLastChangedRev = line.split()[3] 488 return svnRev, svnLastChangedRev, svnUrl
489
490 -def _getHgVersion(file):
491 """Tries to discover the mercurial (hg) parent and id of a file. 492 493 Not thoroughly tested; completely untested on Windows Vista, Win 7, FreeBSD 494 495 :Author: 496 - 2010 written by Jeremy Gray 497 """ 498 if not os.path.exists(file) or not os.path.isdir(os.path.join(os.path.dirname(file),'.hg')): 499 return None 500 try: 501 hgParentLines,err = shellCall('hg parents "'+file+'"', stderr=True) 502 changeset = hgParentLines.splitlines()[0].split()[-1] 503 except: 504 changeset = '' 505 #else: changeset = hgParentLines.splitlines()[0].split()[-1] 506 try: 507 hgID,err = shellCall('hg id -nibt "'+os.path.dirname(file)+'"', stderr=True) 508 except: 509 if err: hgID = '' 510 511 if len(hgID) or len(changeset): 512 return hgID.strip()+' | parent: '+changeset.strip() 513 else: 514 return None
515
516 -def _getUserNameUID():
517 """Return user name, UID: -1=undefined, 0=assume full root, >499=assume non-root; but its >999 on debian 518 519 :Author: 520 - 2010 written by Jeremy Gray 521 """ 522 try: 523 user = os.environ['USER'] 524 except KeyError: 525 user = os.environ['USERNAME'] 526 uid = '-1' 527 try: 528 if sys.platform not in ['win32']: 529 uid = os.popen('id -u').read() 530 else: 531 try: 532 uid = '1000' 533 if ctypes.windll.shell32.IsUserAnAdmin(): 534 uid = '0' 535 except: 536 raise 537 except: 538 pass 539 return str(user), int(uid)
540
541 -def _getSha1hexDigest(str):
542 """Returns base64 / hex encoded sha1 digest of a file or string, using hashlib.sha1() if available 543 544 :Author: 545 - 2010 written by Jeremy Gray 546 547 >>> _getSha1hexDigest('1') 548 '356a192b7913b04c54574d18c28d46e6395428ab' 549 """ 550 try: 551 sha1 = hashlib.sha1() 552 except ImportError: 553 sha1 = sha.new() # deprecated, here for python 2.4 554 if os.path.isfile(str): 555 f = open(str,'r') 556 sha1.update(f.read()) 557 f.close() 558 else: 559 sha1.update(str) 560 return sha1.hexdigest()
561