mirror of
https://github.com/greenseeker/t2server.git
synced 2026-07-10 22:14:32 +00:00
First Commit
This commit is contained in:
commit
8ffa4dacbd
16 changed files with 1383 additions and 0 deletions
31
usr/local/bin/t2bouncer
Executable file
31
usr/local/bin/t2bouncer
Executable file
|
|
@ -0,0 +1,31 @@
|
|||
#!/usr/bin/env -S python3 -B
|
||||
import yaml
|
||||
from datetime import datetime
|
||||
from os import system
|
||||
from t2support import *
|
||||
|
||||
# Set default configuration
|
||||
config_defaults = {
|
||||
'RestartTime' : False,
|
||||
'RestartDay' : 'Mon',
|
||||
}
|
||||
|
||||
# Read configuration from config.yaml
|
||||
with open(f'{etc_dir}/config.yaml', 'r') as f:
|
||||
loaded_config = yaml.full_load(f)
|
||||
|
||||
# Merge config_defaults and loaded_config, with loaded_config taking precedence where there are conflicts.
|
||||
# This ensures there are no undefined values in the case of a user removing one from config.yaml.
|
||||
config = {**config_defaults, **loaded_config}
|
||||
|
||||
now=datetime.now()
|
||||
|
||||
if not config['RestartTime']:
|
||||
print("RestartTime is disabled.")
|
||||
|
||||
elif config['RestartTime'] > 0 and config['RestartTime'] < 25:
|
||||
if config['RestartTime'] == int(now.strftime('%H')) and config['RestartDay'][:3].upper() == now.strftime('%a').upper():
|
||||
print("RestartTime and RestartDay match current time and day. Restarting t2server.service.")
|
||||
system("/usr/bin/systemctl try-restart t2server.service")
|
||||
else:
|
||||
print("RestartTime and RestartDay do not match current time and day.")
|
||||
103
usr/local/bin/t2fixer
Executable file
103
usr/local/bin/t2fixer
Executable file
|
|
@ -0,0 +1,103 @@
|
|||
#!/usr/bin/env -S python3 -B
|
||||
from os import geteuid, chmod, walk
|
||||
from os.path import isdir, islink, join
|
||||
from shutil import chown
|
||||
from sys import exit as bail
|
||||
from t2support import *
|
||||
|
||||
if geteuid() != 0:
|
||||
bail("This script must be run with sudo or as root.\n")
|
||||
|
||||
def setperm(file):
|
||||
if islink(file):
|
||||
pass
|
||||
elif isdir(file):
|
||||
log.write(f"Setting mode 775 on directory {file}")
|
||||
try:
|
||||
chmod(file,0o775)
|
||||
log.write(f"\n")
|
||||
except:
|
||||
pwarn(f"Failed to set permissions on {file}")
|
||||
log.write(f"... FAILED!\n")
|
||||
pass
|
||||
elif file.endswith("t2support.py"):
|
||||
log.write(f"Setting mode 664 on file {file}")
|
||||
try:
|
||||
chmod(file,0o664)
|
||||
log.write(f"\n")
|
||||
except:
|
||||
pwarn(f"Failed to set permissions on {file}")
|
||||
log.write(f"... FAILED!\n")
|
||||
pass
|
||||
elif file.startswith(bin_dir):
|
||||
log.write(f"Setting mode 775 on script {file}")
|
||||
try:
|
||||
chmod(file,0o775)
|
||||
log.write(f"\n")
|
||||
except:
|
||||
pwarn(f"Failed to set permissions on {file}")
|
||||
log.write(f"... FAILED!\n")
|
||||
pass
|
||||
elif file.endswith(".exe"):
|
||||
log.write(f"Setting mode 775 on exe {file}")
|
||||
try:
|
||||
chmod(file,0o775)
|
||||
log.write(f"\n")
|
||||
except:
|
||||
pwarn(f"Failed to set permissions on {file}")
|
||||
log.write(f"... FAILED!\n")
|
||||
pass
|
||||
else:
|
||||
log.write(f"Setting mode 664 on file {file}")
|
||||
try:
|
||||
chmod(file,0o664)
|
||||
log.write(f"\n")
|
||||
except:
|
||||
pwarn(f"Failed to set permissions on {file}")
|
||||
log.write(f"... FAILED!\n")
|
||||
pass
|
||||
|
||||
if islink(file):
|
||||
pass
|
||||
else:
|
||||
try:
|
||||
log.write(f"Setting owner/group on {file}")
|
||||
chown(file, "t2server", "t2server")
|
||||
log.write(f"\n")
|
||||
except:
|
||||
pwarn(f"Failed to set owner/group on {file}")
|
||||
log.write(f"... FAILED!\n")
|
||||
pass
|
||||
|
||||
|
||||
with open(f'{log_dir}/t2fixer.log', 'w') as log:
|
||||
setperm(install_dir)
|
||||
setperm(etc_dir)
|
||||
|
||||
for r, d, f in walk(f"{install_dir}/GameData"):
|
||||
for file in d:
|
||||
setperm(join(r, file))
|
||||
for file in f:
|
||||
setperm(join(r, file))
|
||||
|
||||
for r, d, f in walk(etc_dir):
|
||||
for file in d:
|
||||
setperm(join(r, file))
|
||||
for file in f:
|
||||
setperm(join(r, file))
|
||||
|
||||
for r, d, f in walk(log_dir):
|
||||
for file in d:
|
||||
setperm(join(r, file))
|
||||
for file in f:
|
||||
setperm(join(r, file))
|
||||
|
||||
setperm(f"{unit_dir}/t2server.service")
|
||||
setperm(f"{unit_dir}/t2bouncer.service")
|
||||
setperm(f"{unit_dir}/t2bouncer.timer")
|
||||
setperm(f"{bin_dir}/t2server")
|
||||
setperm(f"{bin_dir}/t2bouncer")
|
||||
setperm(f"{bin_dir}/t2remove")
|
||||
setperm(f"{bin_dir}/t2fixer")
|
||||
setperm(f"{bin_dir}/t2help")
|
||||
setperm(f"{bin_dir}/t2support.py")
|
||||
22
usr/local/bin/t2help
Executable file
22
usr/local/bin/t2help
Executable file
|
|
@ -0,0 +1,22 @@
|
|||
#!/usr/bin/env -S python3 -B
|
||||
from t2support import color
|
||||
|
||||
print(f"\n{color.DC}The follow commands can be used to manage your Tribes 2 server:")
|
||||
|
||||
print(f"""\n{color.BW}systemctl <action> t2server{color.DC}: The t2server service can be managed with the
|
||||
standard Linux systemctl command.""")
|
||||
|
||||
print(f"""\n{color.BW}t2fixer{color.DC}: This command resets the owner and permissions on all files associated
|
||||
with t2server. It's recommended to run this command any time you make changes
|
||||
or add files (such as mods or map packs) to your server to ensure they can be
|
||||
properly read and accessed. In the future, this command may also resolve other
|
||||
common issues.""")
|
||||
|
||||
print(f"""\n{color.BW}t2remove{color.DC}: This command uninstalls t2server and removes most associated files.
|
||||
Configuration and serverprefs files will be left alone.""")
|
||||
|
||||
print(f"\n{color.DC}Tribes 2 is installed in {color.BW}/opt/t2server{color.DC}.")
|
||||
print(f"{color.DC}The {color.BW}/etc/t2server/config.yaml{color.DC} file holds basic service configuration values.")
|
||||
print(f"{color.DC}The {color.BW}/etc/t2server/serverprefs{color.DC} directory holds your T2 serverprefs .cs files.")
|
||||
|
||||
print(color.X)
|
||||
64
usr/local/bin/t2remove
Executable file
64
usr/local/bin/t2remove
Executable file
|
|
@ -0,0 +1,64 @@
|
|||
#!/usr/bin/env -S python3 -B
|
||||
from os import system, unlink, geteuid
|
||||
from sys import argv
|
||||
from t2support import *
|
||||
|
||||
if geteuid() != 0:
|
||||
bail(f"This script must be run with sudo or as root.\n")
|
||||
|
||||
if menu(["[Y]es, remove Tribes 2", "~~[N]o, exit t2remove"],header=f"This script will remove Tribes 2, service files, and utilities. {etc_dir} will be left as it is.") == "N": bail()
|
||||
|
||||
system("/usr/bin/systemctl stop t2server.service")
|
||||
system("/usr/sbin/userdel -fr t2server > /dev/null 2>&1")
|
||||
system("/usr/sbin/groupdel t2server > /dev/null 2>&1")
|
||||
|
||||
systemd_delete_failed=0
|
||||
|
||||
try:
|
||||
unlink(f"{unit_dir}/t2bouncer.service")
|
||||
except:
|
||||
pwarn(f"Failed to delete {unit_dir}/t2bouncer.service")
|
||||
systemd_delete_failed+=1
|
||||
|
||||
try:
|
||||
unlink(f"{unit_dir}/t2bouncer.timer")
|
||||
except:
|
||||
pwarn(f"Failed to delete {unit_dir}/t2bouncer.timer")
|
||||
systemd_delete_failed+=1
|
||||
|
||||
try:
|
||||
unlink(f"{unit_dir}/t2server.service")
|
||||
except:
|
||||
pwarn(f"Failed to delete {unit_dir}/t2server.service")
|
||||
systemd_delete_failed+=1
|
||||
|
||||
|
||||
if systemd_delete_failed > 0:
|
||||
pinfo("After manually removing the files above, run 'systemctl daemon-reload' to refresh systemd.")
|
||||
else:
|
||||
system("/usr/bin/systemctl daemon-reload")
|
||||
|
||||
try:
|
||||
unlink(f"{bin_dir}/t2bouncer")
|
||||
except:
|
||||
pwarn(f"Failed to delete {bin_dir}/t2bouncer")
|
||||
|
||||
try:
|
||||
unlink(f"{bin_dir}/t2fixer")
|
||||
except:
|
||||
pwarn(f"Failed to delete {bin_dir}/t2fixer")
|
||||
|
||||
try:
|
||||
unlink(f"{bin_dir}/t2server")
|
||||
except:
|
||||
pwarn(f"Failed to delete {bin_dir}/t2server")
|
||||
|
||||
try:
|
||||
unlink(f"{bin_dir}/t2support.py")
|
||||
except:
|
||||
pwarn(f"Failed to delete {bin_dir}/t2support.py")
|
||||
|
||||
try:
|
||||
if argv[0].startswith(bin_dir): unlink(argv[0])
|
||||
except:
|
||||
pwarn(f"Failed to delete {argv[0]}")
|
||||
192
usr/local/bin/t2server
Executable file
192
usr/local/bin/t2server
Executable file
|
|
@ -0,0 +1,192 @@
|
|||
#!/usr/bin/env -S python3 -B
|
||||
import yaml
|
||||
from os import unlink, symlink, chdir, getppid, makedirs
|
||||
from os.path import isfile, islink, isdir, getmtime
|
||||
from re import match
|
||||
from glob import iglob
|
||||
from subprocess import run, PIPE
|
||||
from time import sleep
|
||||
from requests import get
|
||||
from threading import Thread
|
||||
from t2support import *
|
||||
|
||||
winecmd=["/usr/bin/wineconsole", "--backend=curses"]
|
||||
argbase=["Tribes2.exe","-dedicated"]
|
||||
basecmd=winecmd+argbase
|
||||
parent=getppid()
|
||||
|
||||
def dso_cleanup():
|
||||
"""
|
||||
This function finds all .dso files (compiled .cs scripts) and deletes them
|
||||
if they are older than their associated .cs script so that Tribes 2 will
|
||||
recompile them at startup.
|
||||
"""
|
||||
for dso_file in iglob(f"{install_dir}/**/*.dso", recursive=True):
|
||||
cs_file=dso_file[:-4]
|
||||
if isfile(cs_file):
|
||||
cs_mtime=getmtime(cs_file)
|
||||
dso_mtime=getmtime(dso_file)
|
||||
if cs_mtime > dso_mtime:
|
||||
print(f"Deleting {dso_file} so it can be rebuilt.")
|
||||
unlink(dso_file)
|
||||
|
||||
def build_args(basecmd,config):
|
||||
"""
|
||||
This function assembles the command line to launch the server based on the
|
||||
config.yaml file.
|
||||
"""
|
||||
server_command=basecmd
|
||||
if config['Public']:
|
||||
server_command.append("-online")
|
||||
else:
|
||||
server_command.append("-nologin")
|
||||
|
||||
if config['Mod'] != "base":
|
||||
server_command.extend(["-mod", config['Mod']])
|
||||
|
||||
if config['ServerPrefs']:
|
||||
server_command.extend(["-serverprefs","prefs\\ServerPrefs.cs"])
|
||||
|
||||
return server_command
|
||||
|
||||
def server_files(config):
|
||||
"""
|
||||
This function creates a symlink to the specified /etc/t2server/serverprefs
|
||||
file in the GameData/<mod>/prefs directory where Tribes 2 expects to find it.
|
||||
If MapList and MissionType are also configured, this funtion writes
|
||||
GameData/base/prefs/missions.txt with the appropriate values for the
|
||||
GameData/base/scripts/autoexec/missioncycle.cs script. If either MapList or
|
||||
MissionType is False, an empty missions.txt is written.
|
||||
"""
|
||||
if config['ServerPrefs']:
|
||||
if isfile(f"{install_dir}/GameData/{config['Mod']}/prefs/ServerPrefs.cs") or islink(f"{install_dir}/GameData/{config['Mod']}/prefs/ServerPrefs.cs"):
|
||||
print(f"Deleting {install_dir}/GameData/{config['Mod']}/prefs/ServerPrefs.cs")
|
||||
unlink(f"{install_dir}/GameData/{config['Mod']}/prefs/ServerPrefs.cs")
|
||||
print(f"Linking {install_dir}/GameData/{config['Mod']}/prefs/ServerPrefs.cs -> {etc_dir}/serverprefs/{config['ServerPrefs']}")
|
||||
makedirs(f"{install_dir}/GameData/{config['Mod']}/prefs", mode=0o755, exist_ok=True)
|
||||
symlink(f"{etc_dir}/serverprefs/{config['ServerPrefs']}", f"{install_dir}/GameData/{config['Mod']}/prefs/ServerPrefs.cs")
|
||||
|
||||
if config["MapList"] and config["MissionType"]:
|
||||
print(f"Writing {install_dir}/GameData/base/prefs/missions.txt")
|
||||
makedirs(f"{install_dir}/GameData/base/prefs", mode=0o755, exist_ok=True)
|
||||
with open(f"{install_dir}/GameData/base/prefs/missions.txt", 'w') as mlist:
|
||||
for mission in config["MapList"]:
|
||||
mlist.write(f"{config['MissionType']} {mission}\n")
|
||||
else:
|
||||
if isfile(f"{install_dir}/GameData/base/prefs/missions.txt"):
|
||||
print(f"Purging {install_dir}/GameData/base/prefs/missions.txt")
|
||||
# missions.txt needs to exist or the missioncycle.cs script will hang, so overwrite with an empty file
|
||||
open(f"{install_dir}/GameData/base/prefs/missions.txt", 'w').close()
|
||||
|
||||
|
||||
def runaway_control():
|
||||
"""
|
||||
When run in the background, wine will spawn a 'wineconsole --use-event=52'
|
||||
process that will consume all available CPU. This function finds that
|
||||
process and uses cpulimit to keep it under control.
|
||||
"""
|
||||
for x in range(20):
|
||||
sleep(15)
|
||||
print("Checking for runaway wineconsole process...")
|
||||
runaway_pid=run(["/usr/bin/pgrep","-f","wineconsole --use-event=52"],stdout=PIPE).stdout
|
||||
if runaway_pid:
|
||||
runaway_pid=str(int(runaway_pid))
|
||||
print(f"Limiting runaway wineconsole process: {runaway_pid}")
|
||||
run(["/usr/bin/cpulimit","-bp",runaway_pid,"-l2"])
|
||||
break
|
||||
|
||||
def master_heartbeat():
|
||||
"""
|
||||
A public Tribes 2 server should send a regular heartbeat to the TribexNext
|
||||
master server so that it appears in the list, however this seems
|
||||
inconsistent, so this function takes over that responsibility.
|
||||
"""
|
||||
print("Starting TribesNext heartbeat thread...")
|
||||
while True:
|
||||
get('http://master.tribesnext.com/add/28000')
|
||||
sleep(240)
|
||||
|
||||
def is_valid_ip(ip):
|
||||
"""Check if an ip looks like a valid IP address."""
|
||||
return bool(match(r"^(\d{1,3}\.){3}\d{1,3}$", ip))
|
||||
|
||||
def override_mitm():
|
||||
"""
|
||||
Tribes 2 servers try to detect descrepencies between their own IP and the IP
|
||||
that the client believes it's connecting to as possible man-in-the-middle
|
||||
attacks, however this often interfers with connections when the server is
|
||||
NATed or multi-homed. This function gets the public-facing IP of the host
|
||||
and writes an autoexec script to effectively disable this detection.
|
||||
"""
|
||||
for ip_service in ["http://api.ipify.org","http://ifconfig.me","http://ipinfo.io/ip"]:
|
||||
r=get(ip_service)
|
||||
if r.status_code == 200 and is_valid_ip(r.text):
|
||||
print(f"Got public IP address {r.text}")
|
||||
break
|
||||
if r.status_code != 200: bail("Could not get this server's public IP address.")
|
||||
print(f"Overriding Man-in-the-Middle attack detection.")
|
||||
with open(f'{install_dir}/GameData/base/scripts/autoexec/noMITM.cs', 'w') as nomitm_script:
|
||||
nomitm_script.write(f'$IPv4::InetAddress = "{r.text}";\n')
|
||||
|
||||
if __name__ == "__main__":
|
||||
# If run interactively, warn user that they probably don't want to do this unless troubleshooting.
|
||||
if parent == 1:
|
||||
print(f"Started by init")
|
||||
else:
|
||||
interactive_run=menu(['[Y]es, run t2server interactively.',"~~[N]o, abort."],header="Running t2server directly can be helpful for troubleshooting but generally it's best to manage your server with 'systemctl'. Do you still want to run t2server?",footer="Choose [N] and run 't2help' if you're unsure of what to do.")
|
||||
if interactive_run == 'N': bail()
|
||||
chdir(f"{install_dir}/GameData")
|
||||
|
||||
# Set default configuration
|
||||
config_defaults = {
|
||||
'ServerPrefs' : 'Classic_CTF.cs',
|
||||
'Mod' : 'Classic',
|
||||
'Public' : False,
|
||||
'OverrideMITM': True,
|
||||
'MissionType' : 'CTF',
|
||||
'MapList' : False
|
||||
}
|
||||
|
||||
# Read configuration from config.yaml
|
||||
with open(f'{etc_dir}/config.yaml', 'r') as f:
|
||||
loaded_config = yaml.full_load(f)
|
||||
|
||||
# Merge config_defaults and loaded_config, with loaded_config taking precedence where there are conflicts.
|
||||
# This ensures there are no undefined values in the case of a user removing one from config.yaml.
|
||||
config = {**config_defaults, **loaded_config}
|
||||
print(config)
|
||||
|
||||
# Validate the mod directory and serverprefs file
|
||||
if not isdir(f"{install_dir}/GameData/{config['Mod']}"):
|
||||
bail(f"Invalid Mod directory: {config['Mod']}")
|
||||
if not isfile(f"{etc_dir}/serverprefs/{config['ServerPrefs']}"):
|
||||
bail(f"Invalid ServerPrefs file: {config['ServerPrefs']}")
|
||||
|
||||
# Delete any pre-existing noMITM script/dso. It will be recreated below, if needed.
|
||||
if isfile(f"{install_dir}/GameData/base/scripts/autoexec/noMITM.cs"): unlink(f"{install_dir}/GameData/base/scripts/autoexec/noMITM.cs")
|
||||
if isfile(f"{install_dir}/GameData/base/scripts/autoexec/noMITM.cs.dso"): unlink(f"{install_dir}/GameData/base/scripts/autoexec/noMITM.cs.dso")
|
||||
|
||||
# Create serverprefs symlink and missions.txt (if appropriate), clean out stale dso files, then assemble the command line arguments to launch the server
|
||||
server_files(config)
|
||||
dso_cleanup()
|
||||
server_command=build_args(basecmd,config)
|
||||
|
||||
# If this is a public server, start a hearbeat thread. Also write the MITM override file if configured.
|
||||
if config['Public']:
|
||||
print("Starting heartbeat...")
|
||||
if config['OverrideMITM']: override_mitm()
|
||||
heartbeat=Thread(target=master_heartbeat)
|
||||
heartbeat.daemon=True
|
||||
heartbeat.start()
|
||||
|
||||
# Cap the CPU of the runaway wineconsole process
|
||||
wcpid_limit=Thread(target=runaway_control)
|
||||
wcpid_limit.start()
|
||||
|
||||
# Open the console log file if running as service and start the Tribes 2 server
|
||||
print(f"Starting Tribes 2 server: " + " ".join(server_command))
|
||||
if parent == 1:
|
||||
with open(f"{log_dir}/console.log", 'w') as consolelog:
|
||||
run(server_command,stdout=consolelog)
|
||||
else:
|
||||
run(server_command)
|
||||
132
usr/local/bin/t2support.py
Normal file
132
usr/local/bin/t2support.py
Normal file
|
|
@ -0,0 +1,132 @@
|
|||
from tqdm import tqdm
|
||||
from re import search
|
||||
from requests import get
|
||||
from hashlib import md5
|
||||
from os import walk
|
||||
from os.path import join, islink
|
||||
from shutil import chown
|
||||
from sys import exit
|
||||
from textwrap import wrap
|
||||
from itertools import chain
|
||||
|
||||
user = "t2server"
|
||||
install_parent = "/opt"
|
||||
install_dir = f"{install_parent}/t2server"
|
||||
etc_dir = "/etc/t2server"
|
||||
log_dir = "/var/log/t2server"
|
||||
unit_dir = "/etc/systemd/system"
|
||||
bin_dir = "/usr/local/bin"
|
||||
|
||||
class color:
|
||||
X = '\033[m' # Reset
|
||||
DR = '\033[31m' # Dark Red
|
||||
DG = '\033[32m' # Dark Green
|
||||
DY = '\033[33m' # Dark Yellow (Brown)
|
||||
DU = '\033[34m' # Dark Blue
|
||||
DP = '\033[35m' # Dark Purple
|
||||
DC = '\033[36m' # Dark Cyan
|
||||
DW = '\033[37m' # Dark White (light grey)
|
||||
BK = '\033[90m' # Bright Black (dark grey)
|
||||
BR = '\033[91m' # Bright Red
|
||||
BG = '\033[92m' # Bright Green
|
||||
BY = '\033[93m' # Bright Yellow
|
||||
BU = '\033[94m' # Bright Blue
|
||||
BP = '\033[95m' # Bright Purple
|
||||
BC = '\033[96m' # Bright Cyan
|
||||
BW = '\033[97m' # Bright White
|
||||
|
||||
class box:
|
||||
TR = u'\u2510'
|
||||
TL = u'\u250c'
|
||||
BR = u'\u2518'
|
||||
BL = u'\u2514'
|
||||
H = u'\u2500'
|
||||
V = u'\u2502'
|
||||
|
||||
def menu(option_list,header="",footer=""):
|
||||
"""
|
||||
This function takes a list of options and make a BBS-style menu out of them.
|
||||
Each item should have a unique alphanum in square brackets (eg. "[Q]uit" or "[1] Quit")
|
||||
that represents the letter the user will type to select that option. Additionally, one
|
||||
option may start with ~~ to indicate that it is the default option. This option will be
|
||||
selected if the user presses Enter without typing a letter. ~~ will not be displayed on
|
||||
screen. The function returns the selected letter in uppercase.
|
||||
"""
|
||||
default=None
|
||||
keys=[]
|
||||
line_list=list(option_list)
|
||||
|
||||
if header:
|
||||
header_list = wrap(header,width=76)
|
||||
line_list.extend(header_list)
|
||||
else:
|
||||
header_list = None
|
||||
|
||||
if footer:
|
||||
footer_list = wrap(footer,width=76)
|
||||
line_list.extend(footer_list)
|
||||
else:
|
||||
footer_list = None
|
||||
|
||||
longest = max(list(chain((len(ele) for ele in line_list),[40])))
|
||||
|
||||
print(f"{color.BG}{box.TL}{box.H}{''.ljust(longest,box.H)}{box.H}{box.TR}{color.X}")
|
||||
print(f"{color.BG}{box.V} {''.ljust(longest)} {color.DG}{box.V}{color.X}")
|
||||
if header_list:
|
||||
for line in header_list:
|
||||
print(f"{color.BG}{box.V} {color.DG}{line.ljust(longest)} {box.V}{color.X}")
|
||||
print(f"{color.BG}{box.V} {''.ljust(longest)} {color.DG}{box.V}{color.X}")
|
||||
for option in option_list:
|
||||
try:
|
||||
key=search(r'\[([0-9a-zA-Z])\]', option).group(1)
|
||||
except AttributeError:
|
||||
pass
|
||||
if option.startswith("~~"):
|
||||
default = str(key)
|
||||
keys.append(key.upper())
|
||||
option=option[2:]
|
||||
else:
|
||||
keys.append(str(key).lower())
|
||||
option=option.ljust(longest)
|
||||
option=option.replace("[", color.BG + "[" + color.BW)
|
||||
option=option.replace("]", color.BG + "]" + color.DG)
|
||||
option=color.DG + option + color.X
|
||||
print(f"{color.BG}{box.V} {option.ljust(longest)} {color.DG}{box.V}{color.X}")
|
||||
if footer_list:
|
||||
print(f"{color.BG}{box.V} {''.ljust(longest)} {color.DG}{box.V}{color.X}")
|
||||
for line in footer_list:
|
||||
print(f"{color.BG}{box.V} {color.DG}{line.ljust(longest)} {box.V}{color.X}")
|
||||
print(f"{color.BG}{box.V} {''.ljust(longest)} {color.DG}{box.V}{color.X}")
|
||||
print(f"{color.BG}{box.BL}{color.DG}{box.H}{''.ljust(longest,box.H)}{box.H}{box.BR}{color.X}")
|
||||
menu_choice=" "
|
||||
if default:
|
||||
prompt=f"{color.DG}Choice {color.BK}({color.BG}{default}{color.BK}){color.DG}: {color.X}"
|
||||
else:
|
||||
prompt=f"{color.DG}Choice: {color.X}"
|
||||
while menu_choice.upper() not in [x.upper() for x in keys]:
|
||||
menu_choice = str(input(prompt))
|
||||
if default and menu_choice == "": menu_choice = default
|
||||
return menu_choice.upper()
|
||||
|
||||
def chowner(path, owner):
|
||||
""" Recursively chown path with owner as both user and group """
|
||||
for dirpath, ignore, filenames in walk(path):
|
||||
chown(dirpath, owner, owner)
|
||||
for filename in filenames:
|
||||
if not islink(join(dirpath, filename)):
|
||||
chown(join(dirpath, filename), owner, owner)
|
||||
|
||||
def pinfo(message):
|
||||
print(color.BU + message + color.X)
|
||||
|
||||
def pwarn(message):
|
||||
print(color.BY + message + color.X)
|
||||
|
||||
def perror(message):
|
||||
print(color.BR + message + color.X)
|
||||
|
||||
def bail(message=""):
|
||||
exit(color.BR + message + color.X)
|
||||
|
||||
if __name__ == "__main__":
|
||||
bail("This file contains only shared functions and variables for the other scripts and is not meant to be run interactively.")
|
||||
Loading…
Add table
Add a link
Reference in a new issue