mirror of
git://xwords.git.sourceforge.net/gitroot/xwords/xwords
synced 2025-01-06 05:24:46 +01:00
4996d3fe89
Remove legacy relay-inspired logic around comms addressing. Now when a device creates a game it's required to provide its "self address," and if it's a client, the address of the host (which it presumably got through the invitation in response to which the game is being created.) Then as registration messages come in from clients, the host gathers their addresses as always.
1033 lines
38 KiB
Python
Executable file
1033 lines
38 KiB
Python
Executable file
#!/usr/bin/env python3
|
|
|
|
import re, os, sys, shutil, threading, requests, json, glob
|
|
import argparse, datetime, random, signal, subprocess, time
|
|
from shutil import rmtree
|
|
|
|
# LOGDIR=./$(basename $0)_logs
|
|
# APP_NEW=""
|
|
# DO_CLEAN=""
|
|
# APP_NEW_PARAMS=""
|
|
# NGAMES = 1
|
|
g_UDP_PCT_START = 100
|
|
gDeadLaunches = 0
|
|
# UDP_PCT_INCR=10
|
|
# UPGRADE_ODDS=""
|
|
# NROOMS=""
|
|
# HOST=""
|
|
# PORT=""
|
|
# TIMEOUT=""
|
|
# SAVE_GOOD=""
|
|
# MINDEVS=""
|
|
# MAXDEVS=""
|
|
# ONEPER=""
|
|
# RESIGN_PCT=0
|
|
g_DROP_N=0
|
|
# MINRUN=2 # seconds
|
|
# ONE_PER_ROOM="" # don't run more than one device at a time per room
|
|
# USE_GTK=""
|
|
# UNDO_PCT=0
|
|
# ALL_VIA_RQ=${ALL_VIA_RQ:-FALSE}
|
|
# SEED=""
|
|
# BOARD_SIZES_OLD=(15)
|
|
# BOARD_SIZES_NEW=(15)
|
|
g_NAMES = [None, 'Brynn', 'Ariela', 'Kati', 'Eric']
|
|
# SEND_CHAT=''
|
|
# CORE_COUNT=$(ls core.* 2>/dev/null | wc -l)
|
|
# DUP_PACKETS=''
|
|
# HTTP_PCT=0
|
|
|
|
# declare -A PIDS
|
|
# declare -A APPS
|
|
# declare -A NEW_ARGS
|
|
# declare -a ARGS
|
|
# declare -A ARGS_DEVID
|
|
# declare -A ROOMS
|
|
# declare -A FILES
|
|
# declare -A LOGS
|
|
# declare -A MINEND
|
|
# ROOM_PIDS = {}
|
|
# declare -a APPS_OLD=()
|
|
# declare -a DICTS= # wants to be =() too?
|
|
# declare -A CHECKED_ROOMS
|
|
|
|
# function cleanup() {
|
|
# APP="$(basename $APP_NEW)"
|
|
# while pidof $APP; do
|
|
# echo "killing existing $APP instances..."
|
|
# killall -9 $APP
|
|
# sleep 1
|
|
# done
|
|
# echo "cleaning everything up...."
|
|
# if [ -d $LOGDIR ]; then
|
|
# mv $LOGDIR /tmp/${LOGDIR}_$$
|
|
# fi
|
|
# if [ -e $(dirname $0)/../../relay/xwrelay.log ]; then
|
|
# mkdir -p /tmp/${LOGDIR}_$$
|
|
# mv $(dirname $0)/../../relay/xwrelay.log /tmp/${LOGDIR}_$$
|
|
# fi
|
|
|
|
# echo "DELETE FROM games WHERE room LIKE 'ROOM_%';" | psql -q -t xwgames
|
|
# echo "DELETE FROM msgs WHERE NOT devid in (SELECT unnest(devids) from games);" | psql -q -t xwgames
|
|
# }
|
|
|
|
# function connName() {
|
|
# LOG=$1
|
|
# grep -a 'got_connect_cmd: connName' $LOG | \
|
|
# tail -n 1 | \
|
|
# sed 's,^.*connName: \"\(.*\)\" (reconnect=.)$,\1,'
|
|
# }
|
|
|
|
# function check_room() {
|
|
# ROOM=$1
|
|
# if [ -z ${CHECKED_ROOMS[$ROOM]:-""} ]; then
|
|
# NUM=$(echo "SELECT COUNT(*) FROM games "\
|
|
# "WHERE NOT dead "\
|
|
# "AND ntotal!=sum_array(nperdevice) "\
|
|
# "AND ntotal != -sum_array(nperdevice) "\
|
|
# "AND room='$ROOM'" |
|
|
# psql -q -t xwgames)
|
|
# NUM=$((NUM+0))
|
|
# if [ "$NUM" -gt 0 ]; then
|
|
# echo "$ROOM in the DB has unconsummated games. Remove them."
|
|
# exit 1
|
|
# else
|
|
# CHECKED_ROOMS[$ROOM]=1
|
|
# fi
|
|
# fi
|
|
# }
|
|
|
|
# print_cmdline() {
|
|
# local COUNTER=$1
|
|
# local LOG=${LOGS[$COUNTER]}
|
|
# echo -n "New cmdline: " >> $LOG
|
|
# echo "${APPS[$COUNTER]} ${NEW_ARGS[$COUNTER]} ${ARGS[$COUNTER]}" >> $LOG
|
|
# }
|
|
|
|
def pick_ndevs(args):
|
|
RNUM = random.randint(0, 99)
|
|
if RNUM > 95 and args.MAXDEVS >= 4:
|
|
NDEVS = 4
|
|
elif RNUM > 90 and args.MAXDEVS >= 3:
|
|
NDEVS = 3
|
|
else:
|
|
NDEVS = 2
|
|
if NDEVS < args.MINDEVS:
|
|
NDEVS = args.MINDEVS
|
|
return NDEVS
|
|
|
|
# # Given a device count, figure out how many local players per device.
|
|
# # "1 1" would be a two-device game with 1 each. "1 2 1" a
|
|
# # three-device game with four players total
|
|
def figure_locals(args, NDEVS):
|
|
NPLAYERS = pick_ndevs(args)
|
|
if NPLAYERS < NDEVS: NPLAYERS = NDEVS
|
|
|
|
EXTRAS = 0
|
|
if not args.ONEPER:
|
|
EXTRAS = NPLAYERS - NDEVS
|
|
|
|
LOCALS = []
|
|
for IGNORE in range(NDEVS):
|
|
COUNT = 1
|
|
if EXTRAS > 0:
|
|
EXTRA = random.randint(0, EXTRAS)
|
|
if EXTRA > 0:
|
|
COUNT += EXTRA
|
|
EXTRAS -= EXTRA
|
|
LOCALS.append(COUNT)
|
|
assert 0 < sum(LOCALS) <= 4
|
|
return LOCALS
|
|
|
|
def player_params(args, NLOCALS, NPLAYERS, NAME_INDX):
|
|
assert 0 < NPLAYERS <= 4
|
|
NREMOTES = NPLAYERS - NLOCALS
|
|
PARAMS = []
|
|
while NLOCALS > 0 or NREMOTES > 0:
|
|
if 0 == random.randint(0, 2) and 0 < NLOCALS:
|
|
PARAMS += ['--robot', g_NAMES[NAME_INDX]]
|
|
if not args.IQS_SAME:
|
|
PARAMS += ['--robot-iq', str(random.randint(1,100))]
|
|
NLOCALS -= 1
|
|
NAME_INDX += 1
|
|
elif 0 < NREMOTES:
|
|
PARAMS += ['--remote-player']
|
|
NREMOTES -= 1
|
|
return PARAMS
|
|
|
|
def logReaderStub(dev): dev.logReaderMain()
|
|
|
|
class Device():
|
|
sHasLDevIDMap = {}
|
|
# sConnNamePat = re.compile('.*got_connect_cmd: connName: "([^"]+)".*$')
|
|
sGameOverPat = re.compile('^\[(\#\d|Winner)\] (.*): (\d+)')
|
|
sTilesLeftPoolPat = re.compile('.*pool_r.*Tiles: (\d+) tiles left in pool')
|
|
sTilesLeftTrayPat = re.compile('.*player \d+ now has (\d+) tiles')
|
|
sRelayIDPat = re.compile('.*UPDATE games.*seed=(\d+),.*relayid=\'([^\']+)\'.*')
|
|
sDevIDPat = re.compile('.*storing new devid: ([\da-fA-F]+).*')
|
|
sMQTTDevIDPat = re.compile('.*getMQTTDevID.*: generated id: ([\d[A-F]+).*')
|
|
sConnPat = re.compile('.*linux_util_informMissing\(isServer.*nMissing=0\).*')
|
|
|
|
sScoresDup = []
|
|
sScoresReg = []
|
|
|
|
def __init__(self, args, game, indx, params, room, peers, order,
|
|
db, log, script, nInGame, inDupMode, usePublic):
|
|
self.game = game
|
|
self.indx = indx
|
|
self.args = args
|
|
self.pid = 0
|
|
self.gameOver = False
|
|
self.params = params
|
|
self.room = room
|
|
self.order = order
|
|
self.db = db
|
|
self.logPath = log
|
|
self.script = script
|
|
self.nInGame = nInGame
|
|
self.inDupMode = inDupMode
|
|
self.usePublic = usePublic
|
|
# runtime stuff; init now
|
|
self.app = args.APP_OLD
|
|
self.proc = None
|
|
self.peers = peers
|
|
self.devID = ''
|
|
self.launchCount = 0
|
|
self.allDone = False # when true, can be killed
|
|
self.nTilesLeftPool = None
|
|
self.nTilesLeftTray = None
|
|
self.relayID = None
|
|
self.inviteeDevID = None
|
|
self.inviteeDevIDs = [] # only servers use this
|
|
self.inviteeMQTTDevID = None
|
|
self.inviteeMQTTDevIDs = []
|
|
self.connected = False
|
|
self.relaySeed = 0
|
|
self.locked = False
|
|
|
|
self.setApp(args.START_PCT)
|
|
|
|
with open(self.logPath, "w") as log:
|
|
log.write('New cmdline: ' + self.app + ' ' + (' '.join([str(p) for p in self.params])))
|
|
log.write(os.linesep)
|
|
|
|
def setApp(self, pct):
|
|
if self.app == self.args.APP_OLD and not self.app == self.args.APP_NEW:
|
|
if os.path.exists(self.script) and pct > random.randint(0, 99):
|
|
print('launch(): upgrading {} from {} to {}' \
|
|
.format(self.devName(), self.app, self.args.APP_NEW))
|
|
self.app = self.args.APP_NEW
|
|
# nuke script to force regeneration
|
|
os.unlink(self.script)
|
|
|
|
def devName(self):
|
|
return 'dev_' + str(self.indx)
|
|
|
|
def logReaderMain(self):
|
|
assert self and self.proc
|
|
# print('logReaderMain called; opening:', self.logPath)
|
|
stdout, stderr = self.proc.communicate()
|
|
nLines = 0
|
|
with open(self.logPath, 'a') as log:
|
|
for line in stderr.splitlines():
|
|
nLines += 1
|
|
log.write(line + os.linesep)
|
|
|
|
self.locked = True
|
|
|
|
# check for game over
|
|
if not self.gameOver:
|
|
match = Device.sGameOverPat.match(line)
|
|
if match:
|
|
self.gameOver = True
|
|
score = int(match.group(3))
|
|
if self.inDupMode:
|
|
Device.sScoresDup.append(score)
|
|
else:
|
|
Device.sScoresReg.append(score)
|
|
|
|
# Check every line for tiles left in pool
|
|
match = Device.sTilesLeftPoolPat.match(line)
|
|
if match: self.nTilesLeftPool = int(match.group(1))
|
|
|
|
# Check every line for tiles left in tray
|
|
match = Device.sTilesLeftTrayPat.match(line)
|
|
if match: self.nTilesLeftTray = int(match.group(1))
|
|
|
|
if not self.relayID:
|
|
match = Device.sRelayIDPat.match(line)
|
|
if match:
|
|
self.relaySeed = int(match.group(1))
|
|
self.relayID = match.group(2)
|
|
|
|
if self.args.WITH_RELAY and not self.inviteeDevID:
|
|
match = Device.sDevIDPat.match(line)
|
|
if match: self.inviteeDevID = int(match.group(1), 16)
|
|
|
|
if self.args.WITH_MQTT and not self.inviteeMQTTDevID:
|
|
match = Device.sMQTTDevIDPat.match(line)
|
|
if match:
|
|
self.inviteeMQTTDevID = int(match.group(1), 16)
|
|
# print('read mqtt devid: {:16X}'.format(self.inviteeMQTTDevID))
|
|
|
|
if not self.connected:
|
|
match = Device.sConnPat.match(line)
|
|
if match: self.connected = True
|
|
|
|
self.locked = False
|
|
|
|
# print('logReaderMain done, wrote lines:', nLines, 'to', self.logPath);
|
|
|
|
def checkScript(self):
|
|
if not os.path.exists(self.script):
|
|
args = ['exec'] # without exec means terminate() won't work
|
|
if self.args.VALGRIND:
|
|
args += ['valgrind']
|
|
# args += ['--leak-check=full']
|
|
# args += ['--track-origins=yes']
|
|
args += [self.app] + [str(p) for p in self.params]
|
|
if self.devID: args.extend( ' '.split(self.devID))
|
|
args += [ '$*' ]
|
|
with open( self.script, 'w' ) as fil:
|
|
fil.write( "#!/bin/sh\n" )
|
|
fil.write( ' '.join(args) + '\n' )
|
|
os.chmod(self.script, 0o755)
|
|
|
|
def launch(self):
|
|
self.setApp(self.args.UPGRADE_PCT)
|
|
self.checkScript()
|
|
self.launchCount += 1
|
|
args = [ self.script, '--close-stdin' ]
|
|
|
|
# If I'm an unconnected server and I know a client's relayid,
|
|
# append it so invitation can happen. When more than one
|
|
# device will be invited, the invitations must always go in
|
|
# the same order so channels will be assigned consistently. So
|
|
# keep them in an array as they're encountered, and use in
|
|
# that order
|
|
if self.args.WITH_RELAY:
|
|
if not self.usePublic and self.order == 1 and self.inviteeDevID and not self.connected:
|
|
for peer in self.peers:
|
|
if peer.inviteeDevID and not peer == self:
|
|
if not peer.inviteeDevID in self.inviteeDevIDs:
|
|
self.inviteeDevIDs.append(peer.inviteeDevID)
|
|
|
|
if self.inviteeDevIDs:
|
|
args += [ '--force-invite' ]
|
|
for inviteeDevID in self.inviteeDevIDs:
|
|
args += ['--invitee-relayid', str(inviteeDevID)]
|
|
|
|
if self.args.WITH_MQTT:
|
|
if self.order == 1 and not self.connected:
|
|
for peer in self.peers:
|
|
if peer.inviteeMQTTDevID and not peer == self:
|
|
if not peer.inviteeMQTTDevID in self.inviteeMQTTDevIDs:
|
|
self.inviteeMQTTDevIDs.append(peer.inviteeMQTTDevID)
|
|
if self.inviteeMQTTDevIDs:
|
|
args += [ '--force-invite' ]
|
|
for idid in self.inviteeMQTTDevIDs:
|
|
asHexStr = '{:16X}'.format(idid)
|
|
args += ['--invitee-mqtt-devid', asHexStr]
|
|
|
|
self.proc = subprocess.Popen(args, stdout = subprocess.DEVNULL,
|
|
stderr = subprocess.PIPE, universal_newlines = True)
|
|
self.pid = self.proc.pid
|
|
self.minEnd = datetime.datetime.now() + datetime.timedelta(seconds = self.args.MINRUN)
|
|
|
|
# Now start a thread to read stdio
|
|
self.reader = threading.Thread(target = logReaderStub, args=(self,))
|
|
self.reader.isDaemon = True
|
|
self.reader.start()
|
|
|
|
def running(self):
|
|
return self.proc and not self.proc.poll()
|
|
|
|
def minTimeExpired(self):
|
|
assert self.proc
|
|
return self.minEnd < datetime.datetime.now()
|
|
|
|
def kill(self):
|
|
if self.proc.poll() is None:
|
|
self.proc.terminate()
|
|
self.proc.wait()
|
|
assert self.proc.poll() is not None
|
|
|
|
self.reader.join()
|
|
self.reader = None
|
|
else:
|
|
print('NOT killing')
|
|
self.proc = None
|
|
self.check_game_over()
|
|
|
|
def handleAllDone(self):
|
|
global gDeadLaunches
|
|
if self.allDone:
|
|
self.moveFiles()
|
|
self.send_dead()
|
|
gDeadLaunches += self.launchCount
|
|
return self.allDone
|
|
|
|
def moveFiles(self):
|
|
assert not self.running()
|
|
for fil in [ self.logPath, self.db, self.script ]:
|
|
shutil.move(fil, self.args.LOGDIR + '/done')
|
|
|
|
def send_dead(self):
|
|
if self.args.WITH_RELAY:
|
|
JSON = json.dumps([{'relayID': self.relayID, 'seed': self.relaySeed}])
|
|
url = 'http://%s/xw4/relay.py/kill' % (self.args.HOST)
|
|
params = {'params' : JSON}
|
|
try:
|
|
req = requests.get(url, params = params) # failing
|
|
except requests.exceptions.ConnectionError:
|
|
print('got exception sending to', url, params, '; is relay.py running as apache module?')
|
|
|
|
def getTilesCount(self):
|
|
assert not self.locked
|
|
return {'index': self.indx,
|
|
'nTilesLeftPool': self.nTilesLeftPool,
|
|
'nTilesLeftTray': self.nTilesLeftTray,
|
|
'launchCount': self.launchCount,
|
|
'game': self.game,
|
|
}
|
|
|
|
def update_ldevid(self):
|
|
if not self.app in Device.sHasLDevIDMap:
|
|
hasLDevID = False
|
|
proc = subprocess.Popen([self.app, '--help'], stderr=subprocess.PIPE)
|
|
# output, err, = proc.communicate()
|
|
for line in proc.stderr.readlines():
|
|
if b'--ldevid' in line:
|
|
hasLDevID = True
|
|
break
|
|
# print('found --ldevid:', hasLDevID);
|
|
Device.sHasLDevIDMap[self.app] = hasLDevID
|
|
|
|
if Device.sHasLDevIDMap[self.app]:
|
|
RNUM = random.randint(0, 99)
|
|
if not self.devID:
|
|
if RNUM < 30:
|
|
self.devID = '--ldevid LINUX_TEST_%.5d_' % (self.indx)
|
|
elif RNUM < 10:
|
|
self.devID += 'x'
|
|
|
|
def check_game_over(self):
|
|
if self.gameOver and not self.allDone:
|
|
allDone = True
|
|
for dev in self.peers:
|
|
if dev == self: continue
|
|
if not dev.gameOver:
|
|
allDone = False
|
|
break
|
|
|
|
if allDone:
|
|
for dev in self.peers:
|
|
assert self.game == dev.game
|
|
dev.allDone = True
|
|
|
|
def makeSMSPhoneNo( game, dev ):
|
|
return '{:03d}{:03d}'.format( game, dev )
|
|
|
|
def build_cmds(args):
|
|
devs = []
|
|
COUNTER = 0
|
|
|
|
for GAME in range(1, args.NGAMES + 1):
|
|
peers = set()
|
|
ROOM = 'ROOM_%.3d' % (GAME % args.NROOMS)
|
|
NDEVS = pick_ndevs(args)
|
|
LOCALS = figure_locals(args, NDEVS) # as array
|
|
NPLAYERS = sum(LOCALS)
|
|
assert(len(LOCALS) == NDEVS)
|
|
DICT = args.DICTS[GAME % len(args.DICTS)]
|
|
# make one in three games public
|
|
usePublic = args.WITH_RELAY and random.randint(0, 3) == 0
|
|
useDupeMode = random.randint(0, 100) < args.DUP_PCT
|
|
if args.PHONIES == -1: phonies = GAME % 3
|
|
else: phonies = args.PHONIES
|
|
DEV = 0
|
|
for NLOCALS in LOCALS:
|
|
DEV += 1
|
|
DB = '{}/{:02d}_{:02d}_DB.sql3'.format(args.LOGDIR, GAME, DEV)
|
|
LOG = '{}/{:02d}_{:02d}_LOG.txt'.format(args.LOGDIR, GAME, DEV)
|
|
SCRIPT = '{}/start_{:02d}_{:02d}.sh'.format(args.LOGDIR, GAME, DEV)
|
|
|
|
PARAMS = player_params(args, NLOCALS, NPLAYERS, DEV)
|
|
if not args.USE_GTK: PARAMS += ['--curses']
|
|
PARAMS += ['--board-size', '15', '--sort-tiles']
|
|
if not useDupeMode: PARAMS += ['--trade-pct', args.TRADE_PCT]
|
|
|
|
# We SHOULD support having both SMS and relay working...
|
|
if args.WITH_RELAY:
|
|
PARAMS += [ '--relay-port', args.PORT, '--room', ROOM, '--host', args.HOST]
|
|
if random.randint(0, 100) < g_UDP_PCT_START:
|
|
PARAMS += ['--use-udp']
|
|
if args.WITH_SMS:
|
|
PARAMS += [ '--sms-number', makeSMSPhoneNo(GAME, DEV) ]
|
|
if args.SMS_FAIL_PCT > 0:
|
|
PARAMS += [ '--sms-fail-pct', args.SMS_FAIL_PCT ]
|
|
if DEV == 1:
|
|
PARAMS += [ '--force-invite' ]
|
|
for dev in range(2, NDEVS + 1):
|
|
PARAMS += [ '--invitee-sms-number', makeSMSPhoneNo(GAME, dev) ]
|
|
|
|
PARAMS += [ '--mqtt-port', args.MQTT_PORT, '--mqtt-host', args.MQTT_HOST ]
|
|
if args.WITH_MQTT:
|
|
PARAMS += [ '--with-mqtt' ]
|
|
if DEV == 1:
|
|
PARAMS += [ '--force-invite' ]
|
|
else:
|
|
PARAMS += [ '--without-mqtt' ]
|
|
|
|
if args.UNDO_PCT > 0:
|
|
PARAMS += ['--undo-pct', args.UNDO_PCT]
|
|
PARAMS += [ '--game-dict', DICT]
|
|
# Removing --slow-robot for now. With it on (and
|
|
# successfully passed through to the curses client, which
|
|
# hasn't always been happening), 20% of games
|
|
# stall. PENDING...
|
|
# PARAMS += ['--slow-robot', '1:3']
|
|
PARAMS += ['--skip-confirm']
|
|
PARAMS += ['--db', DB]
|
|
|
|
PARAMS += ['--drop-nth-packet', g_DROP_N]
|
|
if random.randint(0, 100) < args.HTTP_PCT:
|
|
PARAMS += ['--use-http']
|
|
|
|
PARAMS += ['--split-packets', '2']
|
|
if args.SEND_CHAT:
|
|
PARAMS += ['--send-chat', args.SEND_CHAT]
|
|
|
|
if args.DUP_PACKETS:
|
|
PARAMS += ['--dup-packets']
|
|
# PARAMS += ['--my-port', '1024']
|
|
# PARAMS += ['--savefail-pct', 10]
|
|
|
|
# With the --seed param passed, games with more than 2
|
|
# devices don't get going. No idea why. This param is NOT
|
|
# passed in the old bash version of this script, so fixing
|
|
# it isn't a priority.
|
|
# PARAMS += ['--seed', args.SEED]
|
|
|
|
if DEV == 1 or usePublic: PARAMS += ['--force-game']
|
|
if DEV == 1:
|
|
PARAMS += ['--server', '--phonies', phonies ]
|
|
if 0 == args.TRAYSIZE: traySize = random.randint(7, 9)
|
|
else: traySize = args.TRAYSIZE
|
|
PARAMS += ['--tray-size', traySize] # randint() is *inclusive*
|
|
# IFF there are any non-1 player counts, tell inviter which
|
|
if sum(LOCALS) > NDEVS:
|
|
PARAMS += ['--invitee-counts', ":".join(str(n) for n in LOCALS[1:])]
|
|
else:
|
|
PARAMS += ['--force-channel', DEV - 1]
|
|
if args.PHONY_PCT and phonies == 2: PARAMS += [ '--make-phony-pct', args.PHONY_PCT ]
|
|
|
|
if useDupeMode: PARAMS += ['--duplicate-mode']
|
|
if usePublic: PARAMS += ['--make-public', '--join-public']
|
|
|
|
PARAMS += ['--board-size', args.BOARD_SIZE]
|
|
|
|
# print('PARAMS:', PARAMS)
|
|
|
|
dev = Device( args, GAME, COUNTER, PARAMS, ROOM, peers,
|
|
DEV, DB, LOG, SCRIPT, len(LOCALS), useDupeMode, usePublic )
|
|
peers.add(dev)
|
|
dev.update_ldevid()
|
|
devs.append(dev)
|
|
|
|
COUNTER += 1
|
|
return devs
|
|
|
|
def summarizeTileCounts(devs, endTime, state, changeSecs):
|
|
global gDeadLaunches
|
|
shouldGoOn = True
|
|
data = [dev.getTilesCount() for dev in devs]
|
|
dupModeFlags = [dev.inDupMode for dev in devs]
|
|
nDevs = len(data)
|
|
totalTilesStd = 0
|
|
totalTilesDup = 0
|
|
colWidth = max(2, len(str(nDevs)))
|
|
headWidth = 0
|
|
fmtData = [{'head' : 'dev', },
|
|
{'head' : 'launches', },
|
|
{'head' : 'tls left', },
|
|
]
|
|
for datum in fmtData:
|
|
headWidth = max(headWidth, len(datum['head']))
|
|
datum['data'] = []
|
|
|
|
# Group devices by game
|
|
games = []
|
|
joinChars = []
|
|
prev = -1
|
|
for datum, inDupMode in zip(data, dupModeFlags):
|
|
gameNo = datum['game']
|
|
if gameNo != prev:
|
|
games.append([])
|
|
if inDupMode: joinChars.append('.')
|
|
else: joinChars.append('+')
|
|
prev = gameNo
|
|
games[-1].append('{:0{width}d}'.format(datum['index'], width=colWidth))
|
|
|
|
fmtData[0]['data'] = []
|
|
for game, joinChar in zip(games, joinChars):
|
|
fmtData[0]['data'].append( joinChar.join(game) )
|
|
|
|
nLaunches = gDeadLaunches
|
|
for datum, inDupMode in zip(data, dupModeFlags):
|
|
launchCount = datum['launchCount']
|
|
nLaunches += launchCount
|
|
fmtData[1]['data'].append('{:{width}d}'.format(launchCount, width=colWidth))
|
|
|
|
# Format tiles left. It's the number in the bag/pool until
|
|
# that drops to 0, then the number in the tray preceeded by
|
|
# '+'. Only the pool number is included in the totalTiles sum.
|
|
nTilesPool = datum['nTilesLeftPool']
|
|
nTilesTray = datum['nTilesLeftTray']
|
|
if nTilesPool is None and nTilesTray is None:
|
|
txt = ('-' * colWidth)
|
|
elif int(nTilesPool) == 0 and not nTilesTray is None:
|
|
txt = '{:+{width}d}'.format(nTilesTray, width=colWidth-1)
|
|
else:
|
|
txt = '{:{width}d}'.format(nTilesPool, width=colWidth)
|
|
if inDupMode: totalTilesDup += int(nTilesPool)
|
|
else: totalTilesStd += int(nTilesPool)
|
|
fmtData[2]['data'].append(txt)
|
|
|
|
print('')
|
|
if totalTilesDup: dupDetails = ' (std: {}, dup: {})'.format(totalTilesStd, totalTilesDup)
|
|
else: dupDetails = ''
|
|
# here
|
|
print('devs left: {nDevs}; bag tiles left: {total}{details}; total launches: {nLaunches}; {now}/{endTime}' \
|
|
.format(nDevs=nDevs, total=totalTilesStd + totalTilesDup, details=dupDetails, \
|
|
nLaunches=nLaunches, now=datetime.datetime.now(), endTime=endTime ))
|
|
fmt = '{head:>%d} {data}' % headWidth
|
|
for datum in fmtData: datum['data'] = ' '.join(datum['data'])
|
|
for datum in fmtData:
|
|
print(fmt.format(**datum))
|
|
|
|
# Now let's see if things are stuck: if the tile string hasn't
|
|
# changed in two minutes bail. Note that the count of tiles left
|
|
# isn't enough because it's zero for a long time as devices are
|
|
# using up what's left in their trays and getting killed.
|
|
now = datetime.datetime.now()
|
|
tilesStr = fmtData[2]['data']
|
|
if not 'tilesStr' in state or state['tilesStr'] != tilesStr:
|
|
state['lastChange'] = now
|
|
state['tilesStr'] = tilesStr
|
|
|
|
return now - state['lastChange'] < datetime.timedelta(seconds = changeSecs)
|
|
|
|
def countCores(args):
|
|
count = 0
|
|
if args.CORE_PAT:
|
|
count = len( glob.glob(args.CORE_PAT) )
|
|
return count
|
|
|
|
gDone = False
|
|
|
|
def run_cmds(args, devs):
|
|
nCores = countCores(args)
|
|
endTime = datetime.datetime.now() + datetime.timedelta(minutes = args.TIMEOUT_MINS)
|
|
printState = {}
|
|
lastPrint = datetime.datetime.now()
|
|
|
|
while len(devs) > 0 and not gDone:
|
|
if countCores(args) > nCores:
|
|
print('core file count increased; exiting')
|
|
break
|
|
now = datetime.datetime.now()
|
|
if now > endTime:
|
|
print('outta time; outta here')
|
|
break
|
|
|
|
# print stats every 5 seconds
|
|
if now - lastPrint > datetime.timedelta(seconds = 5):
|
|
lastPrint = now
|
|
if not summarizeTileCounts(devs, endTime, printState, args.NO_CHANGE_SECS):
|
|
print('no change in too long; exiting')
|
|
break
|
|
|
|
dev = random.choice(devs)
|
|
if not dev.running():
|
|
if dev.handleAllDone():
|
|
devs.remove(dev)
|
|
else:
|
|
dev.launch()
|
|
elif dev.minTimeExpired():
|
|
dev.kill()
|
|
if dev.handleAllDone():
|
|
devs.remove(dev)
|
|
else:
|
|
time.sleep(1.0)
|
|
print('.', end='', flush=True)
|
|
|
|
# if we get here via a break, kill any remaining games
|
|
if devs:
|
|
print('stopping {} remaining games'.format(len(devs)))
|
|
for dev in devs:
|
|
if dev.running(): dev.kill()
|
|
|
|
# run_via_rq() {
|
|
# # launch then kill all games to give chance to hook up
|
|
# for KEY in ${!ARGS[*]}; do
|
|
# echo "launching $KEY"
|
|
# launch $KEY &
|
|
# PID=$!
|
|
# sleep 1
|
|
# kill $PID
|
|
# wait $PID
|
|
# # add_pipe $KEY
|
|
# done
|
|
|
|
# echo "now running via rq"
|
|
# # then run them
|
|
# while :; do
|
|
# COUNT=${#ARGS[*]}
|
|
# [ 0 -ge $COUNT ] && break
|
|
|
|
# INDX=$(($RANDOM%COUNT))
|
|
# KEYS=( ${!ARGS[*]} )
|
|
# KEY=${KEYS[$INDX]}
|
|
# CMD=${ARGS[$KEY]}
|
|
|
|
# RELAYID=$(./scripts/relayID.sh --short ${LOGS[$KEY]})
|
|
# MSG_COUNT=$(../relay/rq -a $HOST -m $RELAYID 2>/dev/null | sed 's,^.*-- ,,')
|
|
# if [ $MSG_COUNT -gt 0 ]; then
|
|
# launch $KEY &
|
|
# PID=$!
|
|
# sleep 2
|
|
# kill $PID || /bin/true
|
|
# wait $PID
|
|
# fi
|
|
# [ "$DROP_N" -ge 0 ] && increment_drop $KEY
|
|
# check_game $KEY
|
|
# done
|
|
# } # run_via_rq
|
|
|
|
# function getArg() {
|
|
# [ 1 -lt "$#" ] || usage "$1 requires an argument"
|
|
# echo $2
|
|
# }
|
|
|
|
def log_scores( devs ):
|
|
if len(Device.sScoresReg) > 0:
|
|
print( "average score for regular games:",
|
|
sum(Device.sScoresReg) // len(Device.sScoresReg) )
|
|
if len(Device.sScoresDup) > 0:
|
|
print( "average score for dup games:",
|
|
sum(Device.sScoresDup) // len(Device.sScoresDup) )
|
|
|
|
def mkParser():
|
|
parser = argparse.ArgumentParser()
|
|
parser.add_argument('--send-chat', dest = 'SEND_CHAT', type = str, default = None,
|
|
help = 'the message to send')
|
|
|
|
parser.add_argument('--app-new', dest = 'APP_NEW', default = './obj_linux_memdbg/xwords',
|
|
help = 'the app we\'ll use')
|
|
parser.add_argument('--app-old', dest = 'APP_OLD', default = './obj_linux_memdbg/xwords',
|
|
help = 'the app we\'ll upgrade from')
|
|
parser.add_argument('--start-pct', dest = 'START_PCT', default = 50, type = int,
|
|
help = 'odds of starting with the new app, 0 <= n < 100')
|
|
parser.add_argument('--upgrade-pct', dest = 'UPGRADE_PCT', default = 20, type = int,
|
|
help = 'odds of upgrading at any launch, 0 <= n < 100')
|
|
|
|
parser.add_argument('--num-games', dest = 'NGAMES', type = int, default = 1, help = 'number of games')
|
|
parser.add_argument('--num-rooms', dest = 'NROOMS', type = int, default = 0,
|
|
help = 'number of roooms (default to --num-games)')
|
|
parser.add_argument('--timeout-mins', dest = 'TIMEOUT_MINS', default = 10000, type = int,
|
|
help = 'minutes after which to timeout')
|
|
parser.add_argument('--nochange-secs', dest = 'NO_CHANGE_SECS', default = 30, type = int,
|
|
help = 'seconds without change after which to timeout')
|
|
parser.add_argument('--log-root', dest='LOGROOT', default = '.', help = 'where logfiles go')
|
|
parser.add_argument('--dup-packets', dest = 'DUP_PACKETS', default = False,
|
|
help = 'send all packet twice')
|
|
parser.add_argument('--phonies', dest = 'PHONIES', default = -1, type = int,
|
|
help = '0 (ignore), 1 (warn)) or 2 (lose turn); default is pick at random')
|
|
parser.add_argument('--make-phony-pct', dest = 'PHONY_PCT', default = 20, type = int,
|
|
help = 'how often a robot should play a phony (only applies when --phonies==2')
|
|
parser.add_argument('--use-gtk', dest = 'USE_GTK', default = False, action = 'store_true',
|
|
help = 'run games using gtk instead of ncurses')
|
|
|
|
parser.add_argument('--dup-pct', dest = 'DUP_PCT', default = 0, type = int,
|
|
help = 'this fraction played in duplicate mode')
|
|
|
|
# #
|
|
# # echo " [--clean-start] \\" >&2
|
|
parser.add_argument('--game-dict', dest = 'DICTS', action = 'append', default = [])
|
|
# # echo " [--help] \\" >&2
|
|
parser.add_argument('--host', dest = 'HOST', default = 'localhost',
|
|
help = 'relay hostname')
|
|
# # echo " [--max-devs <int>] \\" >&2
|
|
parser.add_argument('--min-devs', dest = 'MINDEVS', type = int, default = 2,
|
|
help = 'No game will have fewer devices than this')
|
|
parser.add_argument('--max-devs', dest = 'MAXDEVS', type = int, default = 4,
|
|
help = 'No game will have more devices than this')
|
|
|
|
parser.add_argument('--robots-all-same-iq', dest = 'IQS_SAME', default = False,
|
|
action = 'store_true', help = 'give all robots the same IQ')
|
|
|
|
parser.add_argument('--min-run', dest = 'MINRUN', type = int, default = 2,
|
|
help = 'Keep each run alive at least this many seconds')
|
|
# # echo " [--new-app <path/to/app] \\" >&2
|
|
# # echo " [--new-app-args [arg*]] # passed only to new app \\" >&2
|
|
# # echo " [--num-rooms <int>] \\" >&2
|
|
# # echo " [--old-app <path/to/app]* \\" >&2
|
|
parser.add_argument('--one-per', dest = 'ONEPER', default = False,
|
|
action = 'store_true', help = 'force one player per device')
|
|
parser.add_argument('--port', dest = 'PORT', default = 10997, type = int, \
|
|
help = 'Port relay\'s on')
|
|
parser.add_argument('--resign-pct', dest = 'RESIGN_PCT', default = 0, type = int, \
|
|
help = 'Odds of resigning [0..100]')
|
|
parser.add_argument('--seed', type = int, dest = 'SEED',
|
|
default = random.randint(1, 1000000000))
|
|
# # echo " [--send-chat <interval-in-seconds> \\" >&2
|
|
# # echo " [--udp-incr <pct>] \\" >&2
|
|
# # echo " [--udp-start <pct>] # default: $UDP_PCT_START \\" >&2
|
|
# # echo " [--undo-pct <int>] \\" >&2
|
|
parser.add_argument('--http-pct', dest = 'HTTP_PCT', default = 0, type = int,
|
|
help = 'pct of games to be using web api')
|
|
|
|
parser.add_argument('--undo-pct', dest = 'UNDO_PCT', default = 0, type = int)
|
|
parser.add_argument('--trade-pct', dest = 'TRADE_PCT', default = 10, type = int)
|
|
|
|
parser.add_argument('--with-sms', dest = 'WITH_SMS', action = 'store_true')
|
|
parser.add_argument('--without-sms', dest = 'WITH_SMS', default = False, action = 'store_false')
|
|
parser.add_argument('--sms-fail-pct', dest = 'SMS_FAIL_PCT', default = 0, type = int)
|
|
|
|
parser.add_argument('--with-mqtt', dest = 'WITH_MQTT', default = True, action = 'store_true')
|
|
parser.add_argument('--without-mqtt', dest = 'WITH_MQTT', action = 'store_false')
|
|
parser.add_argument('--mqtt-port', dest = 'MQTT_PORT', default = 1883 )
|
|
parser.add_argument('--mqtt-host', dest = 'MQTT_HOST', default = 'localhost' )
|
|
|
|
parser.add_argument('--with-relay', dest = 'WITH_RELAY', action = 'store_true')
|
|
parser.add_argument('--without-relay', dest = 'WITH_RELAY', default = False, action = 'store_false')
|
|
parser.add_argument('--force-tray', dest = 'TRAYSIZE', default = 0, type = int,
|
|
help = 'Always this many tiles per tray')
|
|
|
|
parser.add_argument('--board-size', dest = 'BOARD_SIZE', type = int, default = 15,
|
|
help = 'Use <n>x<n> size board')
|
|
|
|
parser.add_argument('--core-pat', dest = 'CORE_PAT', default = os.environ.get('DISCON_COREPAT'),
|
|
help = "pattern for core files that should stop the script " \
|
|
+ "(default from env $DISCON_COREPAT)" )
|
|
|
|
parser.add_argument('--with-valgrind', dest = 'VALGRIND', default = False,
|
|
action = 'store_true')
|
|
|
|
return parser
|
|
|
|
# #######################################################
|
|
# ##################### MAIN begins #####################
|
|
# #######################################################
|
|
|
|
def parseArgs():
|
|
args = mkParser().parse_args()
|
|
assignDefaults(args)
|
|
print(args)
|
|
return args
|
|
# print(options)
|
|
|
|
# while [ "$#" -gt 0 ]; do
|
|
# case $1 in
|
|
# --udp-start)
|
|
# UDP_PCT_START=$(getArg $*)
|
|
# shift
|
|
# ;;
|
|
# --udp-incr)
|
|
# UDP_PCT_INCR=$(getArg $*)
|
|
# shift
|
|
# ;;
|
|
# --clean-start)
|
|
# DO_CLEAN=1
|
|
# ;;
|
|
# --num-games)
|
|
# NGAMES=$(getArg $*)
|
|
# shift
|
|
# ;;
|
|
# --num-rooms)
|
|
# NROOMS=$(getArg $*)
|
|
# shift
|
|
# ;;
|
|
# --old-app)
|
|
# APPS_OLD[${#APPS_OLD[@]}]=$(getArg $*)
|
|
# shift
|
|
# ;;
|
|
# --log-root)
|
|
# [ -d $2 ] || usage "$1: no such directory $2"
|
|
# LOGDIR=$2/$(basename $0)_logs
|
|
# shift
|
|
# ;;
|
|
# --dup-packets)
|
|
# DUP_PACKETS=1
|
|
# ;;
|
|
# --new-app)
|
|
# APP_NEW=$(getArg $*)
|
|
# shift
|
|
# ;;
|
|
# --new-app-args)
|
|
# APP_NEW_PARAMS="${2}"
|
|
# echo "got $APP_NEW_PARAMS"
|
|
# shift
|
|
# ;;
|
|
# --game-dict)
|
|
# DICTS[${#DICTS[@]}]=$(getArg $*)
|
|
# shift
|
|
# ;;
|
|
# --min-devs)
|
|
# MINDEVS=$(getArg $*)
|
|
# shift
|
|
# ;;
|
|
# --max-devs)
|
|
# MAXDEVS=$(getArg $*)
|
|
# shift
|
|
# ;;
|
|
# --min-run)
|
|
# MINRUN=$(getArg $*)
|
|
# [ $MINRUN -ge 2 -a $MINRUN -le 60 ] || usage "$1: n must be 2 <= n <= 60"
|
|
# shift
|
|
# ;;
|
|
# --one-per)
|
|
# ONEPER=TRUE
|
|
# ;;
|
|
# --host)
|
|
# HOST=$(getArg $*)
|
|
# shift
|
|
# ;;
|
|
# --port)
|
|
# PORT=$(getArg $*)
|
|
# shift
|
|
# ;;
|
|
# --seed)
|
|
# SEED=$(getArg $*)
|
|
# shift
|
|
# ;;
|
|
# --undo-pct)
|
|
# UNDO_PCT=$(getArg $*)
|
|
# shift
|
|
# ;;
|
|
# --http-pct)
|
|
# HTTP_PCT=$(getArg $*)
|
|
# [ $HTTP_PCT -ge 0 -a $HTTP_PCT -le 100 ] || usage "$1: n must be 0 <= n <= 100"
|
|
# shift
|
|
# ;;
|
|
# --send-chat)
|
|
# SEND_CHAT=$(getArg $*)
|
|
# shift
|
|
# ;;
|
|
# --resign-pct)
|
|
# RESIGN_PCT=$(getArg $*)
|
|
# [ $RESIGN_PCT -ge 0 -a $RESIGN_PCT -le 100 ] || usage "$1: n must be 0 <= n <= 100"
|
|
# shift
|
|
# ;;
|
|
# --no-timeout)
|
|
# TIMEOUT=0x7FFFFFFF
|
|
# ;;
|
|
# --help)
|
|
# usage
|
|
# ;;
|
|
# *) usage "unrecognized option $1"
|
|
# ;;
|
|
# esac
|
|
# shift
|
|
# done
|
|
|
|
def assignDefaults(args):
|
|
if not args.NROOMS: args.NROOMS = args.NGAMES
|
|
if len(args.DICTS) == 0: args.DICTS.append('CollegeEng_2to8.xwd')
|
|
args.LOGDIR = os.path.splitext(os.path.basename(sys.argv[0]))[0] + '_logs'
|
|
# Move an existing logdir aside
|
|
if os.path.exists(args.LOGDIR):
|
|
shutil.move(args.LOGDIR, '/tmp/' + args.LOGDIR + '_' + str(random.randint(0, 100000)))
|
|
for d in ['', 'done', 'dead',]:
|
|
os.mkdir(args.LOGDIR + '/' + d)
|
|
# [ -z "$SAVE_GOOD" ] && SAVE_GOOD=YES
|
|
# # [ -z "$RESIGN_PCT" -a "$NGAMES" -gt 1 ] && RESIGN_RATIO=1000 || RESIGN_RATIO=0
|
|
# [ -z "$DROP_N" ] && DROP_N=0
|
|
# [ -z "$USE_GTK" ] && USE_GTK=FALSE
|
|
# [ -z "$UPGRADE_ODDS" ] && UPGRADE_ODDS=10
|
|
# #$((NGAMES/50))
|
|
# [ 0 -eq $UPGRADE_ODDS ] && UPGRADE_ODDS=1
|
|
# [ -n "$SEED" ] && RANDOM=$SEED
|
|
# [ -z "$ONEPER" -a $NROOMS -lt $NGAMES ] && usage "use --one-per if --num-rooms < --num-games"
|
|
|
|
# [ -n "$DO_CLEAN" ] && cleanup
|
|
|
|
# RESUME=""
|
|
# for FILE in $(ls $LOGDIR/*.{xwg,txt} 2>/dev/null); do
|
|
# if [ -e $FILE ]; then
|
|
# echo "Unfinished games found in $LOGDIR; continue with them (or discard)?"
|
|
# read -p "<yes/no> " ANSWER
|
|
# case "$ANSWER" in
|
|
# y|yes|Y|YES)
|
|
# RESUME=1
|
|
# ;;
|
|
# *)
|
|
# ;;
|
|
# esac
|
|
# fi
|
|
# break
|
|
# done
|
|
|
|
# if [ -z "$RESUME" -a -d $LOGDIR ]; then
|
|
# NEWNAME="$(basename $LOGDIR)_$$"
|
|
# (cd $(dirname $LOGDIR) && mv $(basename $LOGDIR) /tmp/${NEWNAME})
|
|
# fi
|
|
# mkdir -p $LOGDIR
|
|
|
|
# if [ "$SAVE_GOOD" = YES ]; then
|
|
# DONEDIR=$LOGDIR/done
|
|
# mkdir -p $DONEDIR
|
|
# fi
|
|
# DEADDIR=$LOGDIR/dead
|
|
# mkdir -p $DEADDIR
|
|
|
|
# for VAR in NGAMES NROOMS USE_GTK TIMEOUT HOST PORT SAVE_GOOD \
|
|
# MINDEVS MAXDEVS ONEPER RESIGN_PCT DROP_N ALL_VIA_RQ SEED \
|
|
# APP_NEW; do
|
|
# echo "$VAR:" $(eval "echo \$${VAR}") 1>&2
|
|
# done
|
|
# echo "DICTS: ${DICTS[*]}"
|
|
# echo -n "APPS_OLD: "; [ xx = "${APPS_OLD[*]+xx}" ] && echo "${APPS_OLD[*]}" || echo ""
|
|
|
|
# echo "*********$0 starting: $(date)**************"
|
|
# STARTTIME=$(date +%s)
|
|
# [ -z "$RESUME" ] && build_cmds || read_resume_cmds
|
|
# if [ TRUE = "$ALL_VIA_RQ" ]; then
|
|
# run_via_rq
|
|
# else
|
|
# run_cmds
|
|
# fi
|
|
|
|
# wait
|
|
|
|
# SECONDS=$(($(date +%s)-$STARTTIME))
|
|
# HOURS=$((SECONDS/3600))
|
|
# SECONDS=$((SECONDS%3600))
|
|
# MINUTES=$((SECONDS/60))
|
|
# SECONDS=$((SECONDS%60))
|
|
# echo "*********$0 finished: $(date) (took $HOURS:$MINUTES:$SECONDS)**************"
|
|
|
|
def termHandler(signum, frame):
|
|
global gDone
|
|
print('termHandler() called')
|
|
gDone = True
|
|
|
|
def main():
|
|
startTime = datetime.datetime.now()
|
|
signal.signal(signal.SIGINT, termHandler)
|
|
|
|
args = parseArgs()
|
|
# Hack: old files confuse things. Remove is simple fix good for now
|
|
if args.WITH_SMS:
|
|
try: rmtree('/tmp/xw_sms')
|
|
except: None
|
|
devs = build_cmds(args)
|
|
nDevs = len(devs)
|
|
run_cmds(args, devs)
|
|
print('{} finished; took {} for {} devices'.format(sys.argv[0], datetime.datetime.now() - startTime, nDevs))
|
|
log_scores( devs )
|
|
|
|
##############################################################################
|
|
if __name__ == '__main__':
|
|
main()
|