2018-10-21 18:14:20 +02:00
|
|
|
"""
|
2019-11-03 16:11:52 +01:00
|
|
|
Startup operations.
|
2018-10-21 18:14:20 +02:00
|
|
|
"""
|
|
|
|
# pylint: disable=too-many-branches,too-many-statements
|
2018-04-07 10:05:31 +02:00
|
|
|
|
2019-10-18 16:52:44 +02:00
|
|
|
import logging
|
2013-06-22 16:55:15 +02:00
|
|
|
import os
|
2014-01-20 21:25:02 +01:00
|
|
|
import platform
|
2018-10-21 18:14:20 +02:00
|
|
|
import sys
|
2019-10-18 16:52:44 +02:00
|
|
|
import time
|
2014-01-20 21:25:02 +01:00
|
|
|
from distutils.version import StrictVersion
|
2013-06-22 16:55:15 +02:00
|
|
|
|
2018-10-21 18:14:20 +02:00
|
|
|
import defaults
|
|
|
|
import helper_random
|
2017-01-11 17:00:00 +01:00
|
|
|
import paths
|
|
|
|
import state
|
2018-10-21 18:14:20 +02:00
|
|
|
from bmconfigparser import BMConfigParser
|
2013-07-05 20:08:19 +02:00
|
|
|
|
2019-10-18 16:52:44 +02:00
|
|
|
try:
|
|
|
|
from plugins.plugin import get_plugin
|
|
|
|
except ImportError:
|
|
|
|
get_plugin = None
|
|
|
|
|
|
|
|
|
|
|
|
logger = logging.getLogger('default')
|
2019-11-03 16:11:52 +01:00
|
|
|
|
2018-03-03 16:31:49 +01:00
|
|
|
# The user may de-select Portable Mode in the settings if they want
|
|
|
|
# the config files to stay in the application data folder.
|
2018-04-07 15:50:29 +02:00
|
|
|
StoreConfigFilesInSameDirectoryAsProgramByDefault = False
|
2018-04-07 10:05:31 +02:00
|
|
|
|
2013-06-21 00:55:04 +02:00
|
|
|
|
|
|
|
def loadConfig():
|
2018-10-21 18:14:20 +02:00
|
|
|
"""Load the config"""
|
2018-03-03 16:59:53 +01:00
|
|
|
config = BMConfigParser()
|
2017-01-11 17:00:00 +01:00
|
|
|
if state.appdata:
|
2018-03-03 16:59:53 +01:00
|
|
|
config.read(state.appdata + 'keys.dat')
|
2018-04-07 10:05:31 +02:00
|
|
|
# state.appdata must have been specified as a startup option.
|
2018-03-03 16:59:53 +01:00
|
|
|
needToCreateKeysFile = config.safeGet(
|
2018-03-03 16:31:49 +01:00
|
|
|
'bitmessagesettings', 'settingsversion') is None
|
|
|
|
if not needToCreateKeysFile:
|
2019-10-18 16:52:44 +02:00
|
|
|
logger.info(
|
2018-03-03 16:31:49 +01:00
|
|
|
'Loading config files from directory specified'
|
2019-10-18 16:52:44 +02:00
|
|
|
' on startup: %s', state.appdata)
|
2013-08-26 00:55:53 +02:00
|
|
|
else:
|
2018-03-03 16:59:53 +01:00
|
|
|
config.read(paths.lookupExeFolder() + 'keys.dat')
|
2013-08-26 00:55:53 +02:00
|
|
|
try:
|
2018-03-03 16:59:53 +01:00
|
|
|
config.get('bitmessagesettings', 'settingsversion')
|
2019-10-18 16:52:44 +02:00
|
|
|
logger.info('Loading config files from same directory as program.')
|
2013-08-26 00:55:53 +02:00
|
|
|
needToCreateKeysFile = False
|
2017-01-11 17:00:00 +01:00
|
|
|
state.appdata = paths.lookupExeFolder()
|
2018-03-03 16:31:49 +01:00
|
|
|
except:
|
2018-04-07 10:05:31 +02:00
|
|
|
# Could not load the keys.dat file in the program directory.
|
|
|
|
# Perhaps it is in the appdata directory.
|
2017-01-11 17:00:00 +01:00
|
|
|
state.appdata = paths.lookupAppdataFolder()
|
2018-03-03 16:59:53 +01:00
|
|
|
config.read(state.appdata + 'keys.dat')
|
|
|
|
needToCreateKeysFile = config.safeGet(
|
2018-03-03 16:31:49 +01:00
|
|
|
'bitmessagesettings', 'settingsversion') is None
|
|
|
|
if not needToCreateKeysFile:
|
2019-10-18 16:52:44 +02:00
|
|
|
logger.info(
|
|
|
|
'Loading existing config files from %s', state.appdata)
|
2013-08-26 00:55:53 +02:00
|
|
|
|
|
|
|
if needToCreateKeysFile:
|
2018-03-03 16:31:49 +01:00
|
|
|
|
2013-08-26 00:55:53 +02:00
|
|
|
# This appears to be the first time running the program; there is
|
|
|
|
# no config file (or it cannot be accessed). Create config file.
|
2018-03-03 16:59:53 +01:00
|
|
|
config.add_section('bitmessagesettings')
|
|
|
|
config.set('bitmessagesettings', 'settingsversion', '10')
|
|
|
|
config.set('bitmessagesettings', 'port', '8444')
|
|
|
|
config.set('bitmessagesettings', 'timeformat', '%%c')
|
|
|
|
config.set('bitmessagesettings', 'blackwhitelist', 'black')
|
|
|
|
config.set('bitmessagesettings', 'startonlogon', 'false')
|
2013-08-26 00:55:53 +02:00
|
|
|
if 'linux' in sys.platform:
|
2018-03-03 16:59:53 +01:00
|
|
|
config.set('bitmessagesettings', 'minimizetotray', 'false')
|
2018-03-03 16:31:49 +01:00
|
|
|
# This isn't implimented yet and when True on
|
|
|
|
# Ubuntu causes Bitmessage to disappear while
|
|
|
|
# running when minimized.
|
2013-08-26 00:55:53 +02:00
|
|
|
else:
|
2018-03-03 16:59:53 +01:00
|
|
|
config.set('bitmessagesettings', 'minimizetotray', 'true')
|
|
|
|
config.set('bitmessagesettings', 'showtraynotifications', 'true')
|
|
|
|
config.set('bitmessagesettings', 'startintray', 'false')
|
|
|
|
config.set('bitmessagesettings', 'socksproxytype', 'none')
|
|
|
|
config.set('bitmessagesettings', 'sockshostname', 'localhost')
|
|
|
|
config.set('bitmessagesettings', 'socksport', '9050')
|
|
|
|
config.set('bitmessagesettings', 'socksauthentication', 'false')
|
|
|
|
config.set('bitmessagesettings', 'socksusername', '')
|
|
|
|
config.set('bitmessagesettings', 'sockspassword', '')
|
|
|
|
config.set('bitmessagesettings', 'keysencrypted', 'false')
|
|
|
|
config.set('bitmessagesettings', 'messagesencrypted', 'false')
|
|
|
|
config.set(
|
2018-03-03 16:31:49 +01:00
|
|
|
'bitmessagesettings', 'defaultnoncetrialsperbyte',
|
|
|
|
str(defaults.networkDefaultProofOfWorkNonceTrialsPerByte))
|
2018-03-03 16:59:53 +01:00
|
|
|
config.set(
|
2018-03-03 16:31:49 +01:00
|
|
|
'bitmessagesettings', 'defaultpayloadlengthextrabytes',
|
|
|
|
str(defaults.networkDefaultPayloadLengthExtraBytes))
|
2018-03-03 16:59:53 +01:00
|
|
|
config.set('bitmessagesettings', 'minimizeonclose', 'false')
|
|
|
|
config.set('bitmessagesettings', 'dontconnect', 'true')
|
|
|
|
config.set('bitmessagesettings', 'replybelow', 'False')
|
|
|
|
config.set('bitmessagesettings', 'maxdownloadrate', '0')
|
|
|
|
config.set('bitmessagesettings', 'maxuploadrate', '0')
|
2018-04-30 16:55:10 +02:00
|
|
|
|
2018-03-03 16:31:49 +01:00
|
|
|
# UI setting to stop trying to send messages after X days/months
|
2018-03-03 16:59:53 +01:00
|
|
|
config.set('bitmessagesettings', 'stopresendingafterxdays', '')
|
|
|
|
config.set('bitmessagesettings', 'stopresendingafterxmonths', '')
|
2013-08-28 04:29:39 +02:00
|
|
|
|
|
|
|
# Are you hoping to add a new option to the keys.dat file? You're in
|
|
|
|
# the right place for adding it to users who install the software for
|
|
|
|
# the first time. But you must also add it to the keys.dat file of
|
2018-03-03 16:31:49 +01:00
|
|
|
# existing users. To do that, search the class_sqlThread.py file
|
|
|
|
# for the text: "right above this line!"
|
2013-08-28 04:29:39 +02:00
|
|
|
|
2018-04-07 15:50:29 +02:00
|
|
|
if StoreConfigFilesInSameDirectoryAsProgramByDefault:
|
2013-08-26 00:55:53 +02:00
|
|
|
# Just use the same directory as the program and forget about
|
|
|
|
# the appdata folder
|
2017-01-11 17:00:00 +01:00
|
|
|
state.appdata = ''
|
2019-10-18 16:52:44 +02:00
|
|
|
logger.info(
|
|
|
|
'Creating new config files in same directory as program.')
|
2013-08-26 00:55:53 +02:00
|
|
|
else:
|
2019-10-18 16:52:44 +02:00
|
|
|
logger.info('Creating new config files in %s', state.appdata)
|
2017-01-11 17:00:00 +01:00
|
|
|
if not os.path.exists(state.appdata):
|
|
|
|
os.makedirs(state.appdata)
|
2013-08-26 00:55:53 +02:00
|
|
|
if not sys.platform.startswith('win'):
|
|
|
|
os.umask(0o077)
|
2018-03-03 16:59:53 +01:00
|
|
|
config.save()
|
2018-03-03 16:31:49 +01:00
|
|
|
else:
|
|
|
|
updateConfig()
|
2014-01-20 21:25:02 +01:00
|
|
|
|
2018-06-27 23:54:49 +02:00
|
|
|
|
2018-03-03 16:31:49 +01:00
|
|
|
def updateConfig():
|
2018-10-21 18:14:20 +02:00
|
|
|
"""Save the config"""
|
2018-03-03 16:59:53 +01:00
|
|
|
config = BMConfigParser()
|
|
|
|
settingsversion = config.getint('bitmessagesettings', 'settingsversion')
|
2018-03-03 16:31:49 +01:00
|
|
|
if settingsversion == 1:
|
2018-03-03 16:59:53 +01:00
|
|
|
config.set('bitmessagesettings', 'socksproxytype', 'none')
|
|
|
|
config.set('bitmessagesettings', 'sockshostname', 'localhost')
|
|
|
|
config.set('bitmessagesettings', 'socksport', '9050')
|
|
|
|
config.set('bitmessagesettings', 'socksauthentication', 'false')
|
|
|
|
config.set('bitmessagesettings', 'socksusername', '')
|
|
|
|
config.set('bitmessagesettings', 'sockspassword', '')
|
|
|
|
config.set('bitmessagesettings', 'sockslisten', 'false')
|
|
|
|
config.set('bitmessagesettings', 'keysencrypted', 'false')
|
|
|
|
config.set('bitmessagesettings', 'messagesencrypted', 'false')
|
2018-03-03 16:31:49 +01:00
|
|
|
settingsversion = 2
|
|
|
|
# let class_sqlThread update SQL and continue
|
|
|
|
elif settingsversion == 4:
|
2018-03-03 16:59:53 +01:00
|
|
|
config.set(
|
2018-03-03 16:31:49 +01:00
|
|
|
'bitmessagesettings', 'defaultnoncetrialsperbyte',
|
|
|
|
str(defaults.networkDefaultProofOfWorkNonceTrialsPerByte))
|
2018-03-03 16:59:53 +01:00
|
|
|
config.set(
|
2018-03-03 16:31:49 +01:00
|
|
|
'bitmessagesettings', 'defaultpayloadlengthextrabytes',
|
|
|
|
str(defaults.networkDefaultPayloadLengthExtraBytes))
|
|
|
|
settingsversion = 5
|
|
|
|
|
|
|
|
if settingsversion == 5:
|
2018-03-03 16:59:53 +01:00
|
|
|
config.set(
|
2018-03-03 16:31:49 +01:00
|
|
|
'bitmessagesettings', 'maxacceptablenoncetrialsperbyte', '0')
|
2018-03-03 16:59:53 +01:00
|
|
|
config.set(
|
2018-03-03 16:31:49 +01:00
|
|
|
'bitmessagesettings', 'maxacceptablepayloadlengthextrabytes', '0')
|
|
|
|
settingsversion = 7
|
|
|
|
|
2018-03-03 16:59:53 +01:00
|
|
|
if not config.has_option('bitmessagesettings', 'sockslisten'):
|
|
|
|
config.set('bitmessagesettings', 'sockslisten', 'false')
|
2018-03-03 16:31:49 +01:00
|
|
|
|
2018-03-03 16:59:53 +01:00
|
|
|
if not config.has_option('bitmessagesettings', 'userlocale'):
|
|
|
|
config.set('bitmessagesettings', 'userlocale', 'system')
|
2018-03-03 16:31:49 +01:00
|
|
|
|
2018-03-03 16:59:53 +01:00
|
|
|
if not config.has_option('bitmessagesettings', 'sendoutgoingconnections'):
|
|
|
|
config.set('bitmessagesettings', 'sendoutgoingconnections', 'True')
|
2018-03-03 16:31:49 +01:00
|
|
|
|
2018-03-03 16:59:53 +01:00
|
|
|
if not config.has_option('bitmessagesettings', 'useidenticons'):
|
|
|
|
config.set('bitmessagesettings', 'useidenticons', 'True')
|
|
|
|
if not config.has_option('bitmessagesettings', 'identiconsuffix'):
|
2018-03-03 16:31:49 +01:00
|
|
|
# acts as a salt
|
2018-03-03 16:59:53 +01:00
|
|
|
config.set(
|
2018-10-21 18:14:20 +02:00
|
|
|
'bitmessagesettings', 'identiconsuffix', ''.join(
|
|
|
|
helper_random.randomchoice("123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz")
|
|
|
|
for x in range(12)
|
|
|
|
)
|
|
|
|
) # a twelve character pseudo-password to salt the identicons
|
2018-03-03 16:31:49 +01:00
|
|
|
|
|
|
|
# Add settings to support no longer resending messages after
|
|
|
|
# a certain period of time even if we never get an ack
|
|
|
|
if settingsversion == 7:
|
2018-03-03 16:59:53 +01:00
|
|
|
config.set('bitmessagesettings', 'stopresendingafterxdays', '')
|
|
|
|
config.set('bitmessagesettings', 'stopresendingafterxmonths', '')
|
2018-03-03 16:31:49 +01:00
|
|
|
settingsversion = 8
|
|
|
|
|
|
|
|
# With the change to protocol version 3, reset the user-settable
|
|
|
|
# difficulties to 1
|
|
|
|
if settingsversion == 8:
|
2018-03-03 16:59:53 +01:00
|
|
|
config.set(
|
2018-03-03 16:31:49 +01:00
|
|
|
'bitmessagesettings', 'defaultnoncetrialsperbyte',
|
|
|
|
str(defaults.networkDefaultProofOfWorkNonceTrialsPerByte))
|
2018-03-03 16:59:53 +01:00
|
|
|
config.set(
|
2018-03-03 16:31:49 +01:00
|
|
|
'bitmessagesettings', 'defaultpayloadlengthextrabytes',
|
|
|
|
str(defaults.networkDefaultPayloadLengthExtraBytes))
|
|
|
|
previousTotalDifficulty = int(
|
2018-03-03 16:59:53 +01:00
|
|
|
config.getint(
|
2018-03-03 16:31:49 +01:00
|
|
|
'bitmessagesettings', 'maxacceptablenoncetrialsperbyte')
|
|
|
|
) / 320
|
|
|
|
previousSmallMessageDifficulty = int(
|
2018-03-03 16:59:53 +01:00
|
|
|
config.getint(
|
2018-03-03 16:31:49 +01:00
|
|
|
'bitmessagesettings', 'maxacceptablepayloadlengthextrabytes')
|
|
|
|
) / 14000
|
2018-03-03 16:59:53 +01:00
|
|
|
config.set(
|
2018-03-03 16:31:49 +01:00
|
|
|
'bitmessagesettings', 'maxacceptablenoncetrialsperbyte',
|
|
|
|
str(previousTotalDifficulty * 1000))
|
2018-03-03 16:59:53 +01:00
|
|
|
config.set(
|
2018-03-03 16:31:49 +01:00
|
|
|
'bitmessagesettings', 'maxacceptablepayloadlengthextrabytes',
|
|
|
|
str(previousSmallMessageDifficulty * 1000))
|
|
|
|
settingsversion = 9
|
|
|
|
|
|
|
|
# Adjust the required POW values for each of this user's addresses
|
|
|
|
# to conform to protocol v3 norms.
|
|
|
|
if settingsversion == 9:
|
2018-03-03 16:59:53 +01:00
|
|
|
for addressInKeysFile in config.addresses():
|
2018-03-03 16:31:49 +01:00
|
|
|
try:
|
|
|
|
previousTotalDifficulty = float(
|
2018-03-03 16:59:53 +01:00
|
|
|
config.getint(
|
2018-03-03 16:31:49 +01:00
|
|
|
addressInKeysFile, 'noncetrialsperbyte')) / 320
|
|
|
|
previousSmallMessageDifficulty = float(
|
2018-03-03 16:59:53 +01:00
|
|
|
config.getint(
|
2018-03-03 16:31:49 +01:00
|
|
|
addressInKeysFile, 'payloadlengthextrabytes')) / 14000
|
|
|
|
if previousTotalDifficulty <= 2:
|
|
|
|
previousTotalDifficulty = 1
|
|
|
|
if previousSmallMessageDifficulty < 1:
|
|
|
|
previousSmallMessageDifficulty = 1
|
2018-03-03 16:59:53 +01:00
|
|
|
config.set(
|
2018-03-03 16:31:49 +01:00
|
|
|
addressInKeysFile, 'noncetrialsperbyte',
|
|
|
|
str(int(previousTotalDifficulty * 1000)))
|
2018-03-03 16:59:53 +01:00
|
|
|
config.set(
|
2018-03-03 16:31:49 +01:00
|
|
|
addressInKeysFile, 'payloadlengthextrabytes',
|
|
|
|
str(int(previousSmallMessageDifficulty * 1000)))
|
|
|
|
except Exception:
|
|
|
|
continue
|
2018-03-03 16:59:53 +01:00
|
|
|
config.set('bitmessagesettings', 'maxdownloadrate', '0')
|
|
|
|
config.set('bitmessagesettings', 'maxuploadrate', '0')
|
2018-03-03 16:31:49 +01:00
|
|
|
settingsversion = 10
|
|
|
|
|
|
|
|
# sanity check
|
2018-03-03 16:59:53 +01:00
|
|
|
if config.safeGetInt(
|
2018-03-03 16:31:49 +01:00
|
|
|
'bitmessagesettings', 'maxacceptablenoncetrialsperbyte') == 0:
|
2018-03-03 16:59:53 +01:00
|
|
|
config.set(
|
2018-03-03 16:31:49 +01:00
|
|
|
'bitmessagesettings', 'maxacceptablenoncetrialsperbyte',
|
2018-03-03 16:59:53 +01:00
|
|
|
str(defaults.ridiculousDifficulty *
|
|
|
|
defaults.networkDefaultProofOfWorkNonceTrialsPerByte)
|
2018-03-03 16:31:49 +01:00
|
|
|
)
|
2018-10-21 18:14:20 +02:00
|
|
|
if config.safeGetInt('bitmessagesettings', 'maxacceptablepayloadlengthextrabytes') == 0:
|
2018-03-03 16:59:53 +01:00
|
|
|
config.set(
|
2018-03-03 16:31:49 +01:00
|
|
|
'bitmessagesettings', 'maxacceptablepayloadlengthextrabytes',
|
2018-03-03 16:59:53 +01:00
|
|
|
str(defaults.ridiculousDifficulty *
|
|
|
|
defaults.networkDefaultPayloadLengthExtraBytes)
|
2018-03-03 16:31:49 +01:00
|
|
|
)
|
|
|
|
|
2018-03-03 16:59:53 +01:00
|
|
|
if not config.has_option('bitmessagesettings', 'onionhostname'):
|
|
|
|
config.set('bitmessagesettings', 'onionhostname', '')
|
|
|
|
if not config.has_option('bitmessagesettings', 'onionport'):
|
|
|
|
config.set('bitmessagesettings', 'onionport', '8444')
|
|
|
|
if not config.has_option('bitmessagesettings', 'onionbindip'):
|
|
|
|
config.set('bitmessagesettings', 'onionbindip', '127.0.0.1')
|
|
|
|
if not config.has_option('bitmessagesettings', 'smtpdeliver'):
|
|
|
|
config.set('bitmessagesettings', 'smtpdeliver', '')
|
|
|
|
if not config.has_option(
|
2018-03-03 16:31:49 +01:00
|
|
|
'bitmessagesettings', 'hidetrayconnectionnotifications'):
|
2018-03-03 16:59:53 +01:00
|
|
|
config.set(
|
2018-03-03 16:31:49 +01:00
|
|
|
'bitmessagesettings', 'hidetrayconnectionnotifications', 'false')
|
2018-03-03 16:59:53 +01:00
|
|
|
if config.safeGetInt('bitmessagesettings', 'maxoutboundconnections') < 1:
|
|
|
|
config.set('bitmessagesettings', 'maxoutboundconnections', '8')
|
2019-10-18 16:52:44 +02:00
|
|
|
logger.warning('Your maximum outbound connections must be a number.')
|
2018-03-03 16:31:49 +01:00
|
|
|
|
|
|
|
# TTL is now user-specifiable. Let's add an option to save
|
|
|
|
# whatever the user selects.
|
2018-03-03 16:59:53 +01:00
|
|
|
if not config.has_option('bitmessagesettings', 'ttl'):
|
|
|
|
config.set('bitmessagesettings', 'ttl', '367200')
|
2018-03-03 16:31:49 +01:00
|
|
|
|
2018-03-03 16:59:53 +01:00
|
|
|
config.set('bitmessagesettings', 'settingsversion', str(settingsversion))
|
|
|
|
config.save()
|
2018-03-03 16:31:49 +01:00
|
|
|
|
|
|
|
|
2014-01-20 21:25:02 +01:00
|
|
|
def isOurOperatingSystemLimitedToHavingVeryFewHalfOpenConnections():
|
2018-10-21 18:14:20 +02:00
|
|
|
"""Check for (mainly XP and Vista) limitations"""
|
2014-01-20 21:25:02 +01:00
|
|
|
try:
|
2018-04-07 10:05:31 +02:00
|
|
|
if sys.platform[0:3] == "win":
|
|
|
|
VER_THIS = StrictVersion(platform.version())
|
2018-03-03 16:31:49 +01:00
|
|
|
return (
|
|
|
|
StrictVersion("5.1.2600") <= VER_THIS and
|
|
|
|
StrictVersion("6.0.6000") >= VER_THIS
|
|
|
|
)
|
2014-01-20 21:25:02 +01:00
|
|
|
return False
|
2018-04-07 10:05:31 +02:00
|
|
|
except Exception:
|
2018-03-03 16:31:49 +01:00
|
|
|
pass
|
2019-10-18 16:52:44 +02:00
|
|
|
|
|
|
|
|
|
|
|
def start_proxyconfig():
|
|
|
|
"""Check socksproxytype and start any proxy configuration plugin"""
|
|
|
|
if not get_plugin:
|
|
|
|
return
|
|
|
|
config = BMConfigParser()
|
|
|
|
proxy_type = config.safeGet('bitmessagesettings', 'socksproxytype')
|
|
|
|
if proxy_type and proxy_type not in ('none', 'SOCKS4a', 'SOCKS5'):
|
|
|
|
try:
|
|
|
|
proxyconfig_start = time.time()
|
|
|
|
if not get_plugin('proxyconfig', name=proxy_type)(config):
|
|
|
|
raise TypeError()
|
|
|
|
except TypeError:
|
|
|
|
# cannot import shutdown here ):
|
|
|
|
logger.error(
|
|
|
|
'Failed to run proxy config plugin %s',
|
|
|
|
proxy_type, exc_info=True)
|
|
|
|
os._exit(0) # pylint: disable=protected-access
|
|
|
|
else:
|
|
|
|
logger.info(
|
|
|
|
'Started proxy config plugin %s in %s sec',
|
|
|
|
proxy_type, time.time() - proxyconfig_start)
|