3
0
mirror of https://github.com/jlu5/PyLink.git synced 2024-11-24 03:29:28 +01:00

Merge branch 'devel' into wip/handle-ts-bursts

Conflicts:
	plugins/relay.py
This commit is contained in:
James Lu 2015-08-15 19:28:49 -07:00
commit 0d41e35d9c
10 changed files with 263 additions and 139 deletions

1
.gitignore vendored
View File

@ -7,3 +7,4 @@ __pycache__/
*.save*
*.db
*.pid
*.pem

View File

@ -4,7 +4,7 @@ PyLink is an extensible, plugin-based IRC PseudoService written in Python. It ai
## Usage
**PyLink is a work in progress and thus may be very unstable**! No warranty is provided if this completely wrecks your network and causes widespread rioting throughout your user base!
**PyLink is a work in progress and thus may be very unstable**! No warranty is provided if this completely wrecks your network and causes widespread rioting amongst your users!
That said, please report any bugs you find to the [issue tracker](https://github.com/GLolol/PyLink/issues). Pull requests are open if you'd like to contribute.
@ -17,8 +17,9 @@ Dependencies currently include:
#### Supported IRCds
* InspIRCd 2.0.x - module: `inspircd`
* charybdis (3.5.x / git master) - module: `ts6`
* InspIRCd 2.0.x - module `inspircd`
* charybdis (3.5.x / git master) - module `ts6`
* Elemental-IRCd (6.6.x / git master) - module `ts6`
### Installation

View File

@ -21,6 +21,7 @@ class IrcUser():
self.identified = False
self.channels = set()
self.away = ''
def __repr__(self):
return repr(self.__dict__)

View File

@ -53,6 +53,17 @@ servers:
# PyLink might introduce a nick that is too long and cause netsplits!
maxnicklen: 30
# Toggles SSL for this network. Defaults to false if not specified, and requires the
# ssl_certfile and ssl_keyfile options to work.
# ssl: true
# ssl_certfile: pylink-cert.pem
# ssl_keyfile: pylink-key.pem
# Optionally, you can set this option to verify the SSL certificate
# fingerprint (SHA1) of your uplink.
# ssl_fingerprint: "e0fee1adf795c84eec4735f039503eb18d9c35cc"
ts6net:
ip: 127.0.0.1
port: 7000

78
main.py
View File

@ -7,6 +7,8 @@ import time
import sys
from collections import defaultdict
import threading
import ssl
import hashlib
from log import log
import conf
@ -18,6 +20,8 @@ class Irc():
def initVars(self):
# Server, channel, and user indexes to be populated by our protocol module
self.connected = threading.Event()
self.lastping = time.time()
self.servers = {self.sid: classes.IrcServer(None, self.serverdata['hostname'], internal=True)}
self.users = {}
self.channels = defaultdict(classes.IrcChannel)
@ -52,7 +56,6 @@ class Irc():
def __init__(self, netname, proto, conf):
# Initialize some variables
self.connected = threading.Event()
self.name = netname.lower()
self.conf = conf
self.serverdata = conf['servers'][netname]
@ -67,26 +70,78 @@ class Irc():
self.connection_thread = threading.Thread(target = self.connect)
self.connection_thread.start()
self.pingTimer = None
self.lastping = time.time()
def connect(self):
ip = self.serverdata["ip"]
port = self.serverdata["port"]
while True:
log.info("Connecting to network %r on %s:%s", self.name, ip, port)
self.initVars()
checks_ok = True
try:
self.socket = socket.socket()
self.socket.setblocking(0)
# Initial connection timeout is a lot smaller than the timeout after
# we've connected; this is intentional.
self.socket = socket.create_connection((ip, port), timeout=self.pingfreq)
self.socket.setblocking(0)
self.socket.settimeout(self.pingfreq)
self.ssl = self.serverdata.get('ssl')
if self.ssl:
log.info('(%s) Attempting SSL for this connection...', self.name)
certfile = self.serverdata.get('ssl_certfile')
keyfile = self.serverdata.get('ssl_keyfile')
if certfile and keyfile:
try:
self.socket = ssl.wrap_socket(self.socket,
certfile=certfile,
keyfile=keyfile)
except OSError:
log.exception('(%s) Caught OSError trying to '
'initialize the SSL connection; '
'are "ssl_certfile" and '
'"ssl_keyfile" set correctly?',
self.name)
checks_ok = False
else:
log.error('(%s) SSL certfile/keyfile was not set '
'correctly, aborting... ', self.name)
checks_ok = False
log.info("Connecting to network %r on %s:%s", self.name, ip, port)
self.socket.connect((ip, port))
self.socket.settimeout(self.pingtimeout)
if self.ssl and checks_ok:
peercert = self.socket.getpeercert(binary_form=True)
sha1fp = hashlib.sha1(peercert).hexdigest()
expected_fp = self.serverdata.get('ssl_fingerprint')
if expected_fp:
if sha1fp != expected_fp:
log.error('(%s) Uplink\'s SSL certificate '
'fingerprint (SHA1) does not match the '
'one configured: expected %r, got %r; '
'disconnecting...', self.name,
expected_fp, sha1fp)
checks_ok = False
else:
log.info('(%s) Uplink SSL certificate fingerprint '
'(SHA1) verified: %r', self.name, sha1fp)
else:
log.info('(%s) Uplink\'s SSL certificate fingerprint '
'is %r. You can enhance the security of your '
'link by specifying this in a "ssl_fingerprint"'
' option in your server block.', self.name,
sha1fp)
if checks_ok:
self.proto.connect(self)
self.spawnMain()
log.info('(%s) Starting ping schedulers....', self.name)
self.schedulePing()
log.info('(%s) Server ready; listening for data.', self.name)
self.run()
else:
log.error('(%s) A configuration error was encountered '
'trying to set up this connection. Please check'
' your configuration file and try again.',
self.name)
except (socket.error, classes.ProtocolError, ConnectionError) as e:
log.warning('(%s) Disconnected from IRC: %s: %s',
self.name, type(e).__name__, str(e))
@ -112,14 +167,15 @@ class Irc():
def run(self):
buf = b""
data = b""
while (time.time() - self.lastping) < self.pingtimeout:
log.debug('(%s) time_since_last_ping: %s', self.name, (time.time() - self.lastping))
log.debug('(%s) self.pingtimeout: %s', self.name, self.pingtimeout)
while True:
data = self.socket.recv(2048)
buf += data
if self.connected and not data:
log.warn('(%s) No data received and self.connected is not set; disconnecting!', self.name)
break
if self.connected.is_set() and not data:
log.warning('(%s) No data received and self.connected is set; disconnecting!', self.name)
return
elif (time.time() - self.lastping) > self.pingtimeout:
log.warning('(%s) Connection timed out.', self.name)
return
while b'\n' in buf:
line, buf = buf.split(b'\n', 1)
line = line.strip(b'\r')

View File

@ -186,7 +186,7 @@ def showchan(irc, source, args):
def mode(irc, source, args):
"""<source> <target> <modes>
Admin-only. Sets modes <modes> on <target> from <source>, where <source> is the nick of a PyLink client."""
Admin-only. Sets modes <modes> on <target> from <source>, where <source> is either the nick of a PyLink client, or the SID of a PyLink server."""
checkauthenticated(irc, source)
try:
modesource, target, modes = args[0], args[1], args[2:]
@ -208,3 +208,30 @@ def mode(irc, source, args):
else:
sourceuid = utils.nickToUid(irc, modesource)
irc.proto.modeClient(irc, sourceuid, target, parsedmodes)
@utils.add_cmd
def msg(irc, source, args):
"""<source> <target> <text>
Admin-only. Sends message <text> from <source>, where <source> is the nick of a PyLink client."""
checkauthenticated(irc, source)
try:
msgsource, target, text = args[0], args[1], ' '.join(args[2:])
except IndexError:
utils.msg(irc, source, 'Error: not enough arguments. Needs 3: source nick, target, text.')
return
sourceuid = utils.nickToUid(irc, msgsource)
if not sourceuid:
utils.msg(irc, source, 'Error: unknown user %r' % msgsource)
return
if not utils.isChannel(target):
real_target = utils.nickToUid(irc, target)
if real_target is None:
utils.msg(irc, source, 'Error: unknown user %r' % target)
return
else:
real_target = target
if not text:
utils.msg(irc, source, 'Error: no text given.')
return
irc.proto.messageClient(irc, sourceuid, real_target, text)

View File

@ -16,7 +16,9 @@ dbname = "pylinkrelay"
if confname != 'pylink':
dbname += '-%s' % confname
dbname += '.db'
relayusers = defaultdict(dict)
spawnlocks = defaultdict(threading.Lock)
def relayWhoisHandlers(irc, target):
user = irc.users[target]
@ -30,13 +32,6 @@ def relayWhoisHandlers(irc, target):
utils.whois_handlers.append(relayWhoisHandlers)
def normalizeNick(irc, netname, nick, separator=None, oldnick=''):
# Block until we know the IRC network's nick length (after capabilities
# are sent)
if not hasattr(irc, 'relay_waitFinished'):
log.debug('(%s) normalizeNick: waiting for irc.connected', irc.name)
irc.connected.wait(1)
irc.relay_waitFinished = True
separator = separator or irc.serverdata.get('separator') or "/"
log.debug('(%s) normalizeNick: using %r as separator.', irc.name, separator)
@ -49,7 +44,7 @@ def normalizeNick(irc, netname, nick, separator=None, oldnick=''):
separator = separator.replace('/', '|')
nick = nick.replace('/', '|')
if nick.startswith(tuple(string.digits)):
# On TS6 IRCd-s, nicks that start with 0-9 are only allowed if
# On TS6 IRCds, nicks that start with 0-9 are only allowed if
# they match the UID of the originating server. Otherwise, you'll
# get nasty protocol violations!
nick = '_' + nick
@ -116,7 +111,8 @@ def getPrefixModes(irc, remoteirc, channel, user):
for pmode in ('owner', 'admin', 'op', 'halfop', 'voice'):
if pmode in remoteirc.cmodes: # Mode supported by IRCd
mlist = irc.channels[channel].prefixmodes[pmode+'s']
log.debug('(%s) getPrefixModes: checking if %r is in %r', irc.name, user, mlist)
log.debug('(%s) getPrefixModes: checking if %r is in %s list: %r',
irc.name, user, pmode, mlist)
if user in mlist:
modes += remoteirc.cmodes[pmode]
return modes
@ -130,11 +126,12 @@ def getRemoteUser(irc, remoteirc, user, spawnIfMissing=True):
return remoteirc.pseudoclient.uid
except AttributeError: # Network hasn't been initialized yet?
pass
with spawnlocks[irc.name]:
try:
u = relayusers[(irc.name, user)][remoteirc.name]
except KeyError:
userobj = irc.users.get(user)
if userobj is None or (not spawnIfMissing) or (not remoteirc.connected):
if userobj is None or (not spawnIfMissing) or (not remoteirc.connected.is_set()):
# The query wasn't actually a valid user, or the network hasn't
# been connected yet... Oh well!
return
@ -148,7 +145,10 @@ def getRemoteUser(irc, remoteirc, user, spawnIfMissing=True):
u = remoteirc.proto.spawnClient(remoteirc, nick, ident=ident,
host=host, realname=realname,
modes=modes, ts=userobj.ts).uid
remoteirc.users[u].remote = irc.name
remoteirc.users[u].remote = (irc.name, user)
away = userobj.away
if away:
remoteirc.proto.awayClient(remoteirc, u, away)
relayusers[(irc.name, user)][remoteirc.name] = u
return u
@ -163,23 +163,10 @@ def getLocalUser(irc, user, targetirc=None):
representing the original user on the target network, similar to what
getRemoteUser() does."""
# First, iterate over everyone!
try:
remoteuser = irc.users[user].remote
except (AttributeError, KeyError):
remoteuser = None
for k, v in relayusers.items():
log.debug('(%s) getLocalUser: processing %s, %s in relayusers', irc.name, k, v)
if k[0] == irc.name:
# We don't need to do anything if the target users is on
# the same network as us.
log.debug('(%s) getLocalUser: skipping %s since the target network matches the source network.', irc.name, k)
continue
if v.get(irc.name) == user:
# If the stored pseudoclient UID for the kicked user on
# this network matches the target we have, set that user
# as the one we're kicking! It's a handful, but remember
# we're mapping (home network, UID) pairs to their
# respective relay pseudoclients on other networks.
remoteuser = k
log.debug('(%s) getLocalUser: found %s to correspond to %s.', irc.name, v, k)
break
log.debug('(%s) getLocalUser: remoteuser set to %r (looking up %s/%s).', irc.name, remoteuser, user, irc.name)
if remoteuser:
# If targetirc is given, we'll return simply the UID of the user on the
@ -241,7 +228,7 @@ def initializeChannel(irc, channel):
if remoteirc is None:
continue
rc = remoteirc.channels[remotechan]
if not (remoteirc.connected and findRemoteChan(remoteirc, irc, remotechan)):
if not (remoteirc.connected.is_set() and findRemoteChan(remoteirc, irc, remotechan)):
continue # They aren't connected, don't bother!
# Join their (remote) users and set their modes.
relayJoins(remoteirc, remotechan, rc.users,
@ -313,20 +300,8 @@ def handle_privmsg(irc, numeric, command, args):
text = args['text']
if target == irc.pseudoclient.uid:
return
sent = 0
relay = findRelay((irc.name, target))
# Don't send any "you must be in common channels" if we're not part
# of a relay, or we are but there are no links!
remoteusers = relayusers[(irc.name, numeric)].items()
'''
if utils.isChannel(target) and ((relay and not db[relay]['links']) or \
relay is None):
return
'''
if not remoteusers:
return
for netname, user in relayusers[(irc.name, numeric)].items():
remoteirc = utils.networkobjects[netname]
remoteusers = relayusers[(irc.name, numeric)]
# HACK: Don't break on sending to @#channel or similar.
try:
prefix, target = target.split('#', 1)
@ -334,36 +309,46 @@ def handle_privmsg(irc, numeric, command, args):
prefix = ''
else:
target = '#' + target
if utils.isChannel(target):
log.debug('(%s) relay privmsg: prefix is %r, target is %r', irc.name, prefix, target)
if utils.isChannel(target) and relay and numeric not in irc.channels[target].users:
# The sender must be in the target channel to send messages over the relay;
# it's the only way we can make sure they have a spawned client on ALL
# of the linked networks. This affects -n channels too; see
# https://github.com/GLolol/PyLink/issues/91 for an explanation of why.
utils.msg(irc, numeric, 'Error: You must be in %r in order to send '
'messages over the relay.' % target, notice=True)
return
if utils.isChannel(target):
for netname, user in relayusers[(irc.name, numeric)].items():
remoteirc = utils.networkobjects[netname]
real_target = findRemoteChan(irc, remoteirc, target)
if not real_target:
continue
real_target = prefix + real_target
else:
remoteuser = getLocalUser(irc, target)
if remoteuser is None:
continue
real_target = remoteuser[1]
if notice:
remoteirc.proto.noticeClient(remoteirc, user, real_target, text)
else:
remoteirc.proto.messageClient(remoteirc, user, real_target, text)
sent += 1
'''
if not sent:
# We must be on a common channel with the target. Otherwise, the sender
# doesn't have a client representing them on the remote network,
# and we won't have anywhere to send our messages from.
# In this case, we've iterated over all networks where the sender
# has pseudoclients, and found no suitable targets to send to.
if target in irc.users:
target_s = 'a common channel with %r' % irc.users[target].nick
else:
target_s = repr(target)
utils.msg(irc, numeric, 'Error: You must be in %s in order to send messages.' % \
target_s, notice=True)
'''
remoteuser = getLocalUser(irc, target)
if remoteuser is None:
return
homenet, real_target = remoteuser
# For PMs, we must be on a common channel with the target.
# Otherwise, the sender doesn't have a client representing them
# on the remote network, and we won't have anything to send our
# messages from.
if homenet not in remoteusers.keys():
utils.msg(irc, numeric, 'Error: you must be in a common channel '
'with %r in order to send messages.' % \
irc.users[target].nick, notice=True)
return
remoteirc = utils.networkobjects[homenet]
user = getRemoteUser(irc, remoteirc, numeric, spawnIfMissing=False)
if notice:
remoteirc.proto.noticeClient(remoteirc, user, real_target, text)
else:
remoteirc.proto.messageClient(remoteirc, user, real_target, text)
utils.add_hook(handle_privmsg, 'PRIVMSG')
utils.add_hook(handle_privmsg, 'NOTICE')
@ -391,7 +376,7 @@ def handle_kick(irc, source, command, args):
# they originate from the same network. We won't have
# to filter this; the uplink IRCd will handle it appropriately,
# and we'll just follow.
real_target = getRemoteUser(irc, remoteirc, target)
real_target = getRemoteUser(irc, remoteirc, target, spawnIfMissing=False)
log.debug('(%s) Relay kick: real target for %s is %s', irc.name, target, real_target)
else:
log.debug('(%s) Relay kick: target %s is an internal client, going to look up the real user', irc.name, target)
@ -408,8 +393,8 @@ def handle_kick(irc, source, command, args):
# Join the kicked client back with its respective modes.
irc.proto.sjoinServer(irc, irc.sid, remotechan, [(modes, target)])
if kicker in irc.users:
utils.msg(irc, kicker, "This channel is claimed; your kick has "
"to %s been blocked because you are not "
utils.msg(irc, kicker, "This channel is claimed; your kick to "
"%s has been blocked because you are not "
"(half)opped." % channel, notice=True)
return
@ -434,9 +419,9 @@ def handle_kick(irc, source, command, args):
remotechan, real_target, text)
if target != irc.pseudoclient.uid and not irc.users[target].channels:
irc.proto.quitClient(irc, target, 'Left all shared channels.')
remoteuser = getLocalUser(irc, target)
del relayusers[remoteuser][irc.name]
irc.proto.quitClient(irc, target, 'Left all shared channels.')
utils.add_hook(handle_kick, 'KICK')
@ -639,7 +624,7 @@ def handle_kill(irc, numeric, command, args):
client = getRemoteUser(remoteirc, irc, realuser[1])
irc.proto.sjoinServer(irc, irc.sid, remotechan, [(modes, client)])
if userdata and numeric in irc.users:
utils.msg(irc, numeric, "Your kill has to %s been blocked "
utils.msg(irc, numeric, "Your kill to %s has been blocked "
"because PyLink does not allow killing"
" users over the relay at this time." % \
userdata.nick, notice=True)
@ -685,11 +670,9 @@ def relayJoins(irc, channel, users, ts, modes):
u = getRemoteUser(irc, remoteirc, user)
# Only join users if they aren't already joined. This prevents op floods
# on charybdis from all the SJOINing.
if u not in remoteirc.channels[channel].users:
if ts < rts:
if u not in remoteirc.channels[remotechan].users:
ts = irc.channels[channel].ts
prefixes = getPrefixModes(irc, remoteirc, channel, user)
else:
prefixes = ''
userpair = (prefixes, u)
queued_users.append(userpair)
log.debug('(%s) relayJoins: joining %s to %s%s', irc.name, userpair, remoteirc.name, remotechan)
@ -735,9 +718,9 @@ def removeChannel(irc, channel):
irc.proto.partClient(irc, user, channel, 'Channel delinked.')
# Don't ever quit it either...
if user != irc.pseudoclient.uid and not irc.users[user].channels:
irc.proto.quitClient(irc, user, 'Left all shared channels.')
remoteuser = getLocalUser(irc, user)
del relayusers[remoteuser][irc.name]
irc.proto.quitClient(irc, user, 'Left all shared channels.')
@utils.add_cmd
def create(irc, source, args):
@ -864,7 +847,10 @@ def delink(irc, source, args):
if entry:
if entry[0] == irc.name: # We own this channel.
if not remotenet:
utils.msg(irc, source, "Error: you must select a network to delink, or use the 'destroy' command no remove this relay entirely.")
utils.msg(irc, source, "Error: You must select a network to "
"delink, or use the 'destroy' command to remove "
"this relay entirely (it was created on the current "
"network).")
return
else:
for link in db[entry]['links'].copy():
@ -950,3 +936,9 @@ def linked(irc, source, args):
else:
s += '(no relays yet)'
utils.msg(irc, source, s)
def handle_away(irc, numeric, command, args):
for netname, user in relayusers[(irc.name, numeric)].items():
remoteirc = utils.networkobjects[netname]
remoteirc.proto.awayClient(remoteirc, user, args['text'])
utils.add_hook(handle_away, 'AWAY')

View File

@ -56,10 +56,11 @@ def joinClient(irc, client, channel):
if not server:
log.error('(%s) Error trying to join client %r to %r (no such pseudoclient exists)', irc.name, client, channel)
raise LookupError('No such PyLink PseudoClient exists.')
# One channel per line here!
# Strip out list-modes, they shouldn't be ever sent in FJOIN.
modes = [m for m in irc.channels[channel].modes if m[0] not in irc.cmodes['*A']]
_send(irc, server, "FJOIN {channel} {ts} {modes} :,{uid}".format(
ts=irc.channels[channel].ts, uid=client, channel=channel,
modes=utils.joinModes(irc.channels[channel].modes)))
modes=utils.joinModes(modes)))
irc.channels[channel].users.add(client)
irc.users[client].channels.add(channel)
@ -74,13 +75,8 @@ def sjoinServer(irc, server, channel, users, ts=None):
ts = irc.channels[channel].ts
log.debug("sending SJOIN to %s%s with ts %s (that's %r)", channel, irc.name, ts,
time.strftime("%c", time.localtime(ts)))
''' TODO: handle this properly!
if modes is None:
modes = irc.channels[channel].modes
else:
utils.applyModes(irc, channel, modes)
'''
modes = irc.channels[channel].modes
# Strip out list-modes, they shouldn't be ever sent in FJOIN.
modes = [m for m in irc.channels[channel].modes if m[0] not in irc.cmodes['*A']]
uids = []
changedmodes = []
namelist = []
@ -297,6 +293,16 @@ def numericServer(irc, source, numeric, text):
"locally by InspIRCd servers, so there is no "
"need for PyLink to send numerics directly yet.")
def awayClient(irc, source, text):
"""<irc object> <numeric> <text>
Sends an AWAY message with text <text> from PyLink client <numeric>.
<text> can be an empty string to unset AWAY status."""
if text:
_send(irc, source, 'AWAY %s :%s' % (int(time.time()), text))
else:
_send(irc, source, 'AWAY')
def connect(irc):
ts = irc.start_ts
@ -667,3 +673,13 @@ def handle_fname(irc, numeric, command, args):
def handle_endburst(irc, numeric, command, args):
return {}
def handle_away(irc, numeric, command, args):
# <- :1MLAAAAIG AWAY 1439371390 :Auto-away
try:
ts = args[0]
irc.users[numeric].away = text = args[1]
return {'text': text, 'ts': ts}
except IndexError: # User is unsetting away status
irc.users[numeric].away = ''
return {'text': ''}

View File

@ -230,6 +230,16 @@ def pingServer(irc, source=None, target=None):
def numericServer(irc, source, numeric, target, text):
_send(irc, source, '%s %s %s' % (numeric, target, text))
def awayClient(irc, source, text):
"""<irc object> <numeric> <text>
Sends an AWAY message with text <text> from PyLink client <numeric>.
<text> can be an empty string to unset AWAY status."""
if text:
_send(irc, source, 'AWAY :%s' % text)
else:
_send(irc, source, 'AWAY')
def connect(irc):
ts = irc.start_ts
@ -651,3 +661,13 @@ def handle_472(irc, numeric, command, args):
' desyncs, try adding the line "loadmodule "extensions/%s.so";" to '
'your IRCd configuration.', irc.name, setter, badmode,
charlist[badmode])
def handle_away(irc, numeric, command, args):
# <- :6ELAAAAAB AWAY :Auto-away
try:
irc.users[numeric].away = text = args[0]
except IndexError: # User is unsetting away status
irc.users[numeric].away = text = ''
return {'text': text}

View File

@ -1,18 +1,17 @@
#!/usr/bin/env bash
# Shell script to start PyLink under CPUlimit, killing it if it starts abusing the CPU.
# Shell script to start PyLink under CPUlimit, throttling it if it starts abusing the CPU.
# Set this to whatever you want. cpulimit --help
LIMIT=20
LIMIT=35
# Change to the PyLink root directory.
WRAPPER_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
cd "$WRAPPER_DIR"
if [[ ! -z "$(which cpulimit)" ]]; then
# -k kills the PyLink daemon if it goes over $LIMIT
# -z makes cpulimit exit when PyLink dies.
cpulimit -l $LIMIT -z -k ./main.py
echo "PyLink has been started (daemonized) under cpulimit, and will automatically be killed if it goes over the CPU limit of ${LIMIT}%."
cpulimit -l $LIMIT -z ./main.py
echo "PyLink has been started (daemonized) under cpulimit, and will automatically be throttled if it goes over the CPU limit of ${LIMIT}%."
echo "To kill the process manually, run ./kill.sh"
else
echo 'cpulimit not found in $PATH! Aborting.'