Overview
The Process class is the base class for Server and Network. It handles process lifecycle, console monitoring, plugin management, and Discord integration.
Class Definition
from mcdis_rcon.classes import Process
class Process ():
def __init__ ( self , name : str , client : McDisClient, config : dict ):
self .name = name
self .client = client
# ... initialization
Attributes
Process name (from md_config.yml)
Directory for process files (equals name)
Reference to main McDisClient instance
Command prefix (inherited from client)
Backup directory: .mdbackups/<name>
Plugin directory: <name>/.mdplugins
Commands directory: <name>/.mdcommands
Command to start the process (from config)
Command to stop the process (from config)
Log patterns to exclude from Discord relay
Dictionary of loaded mdplugin instances
Actual Java process (for resource monitoring)
Process Lifecycle
start()
def start ( self ):
if self .is_running():
return
self ._console_log = queue.Queue()
self ._console_relay = queue.Queue()
self .process = subprocess.Popen(
self .start_cmd.split( ' ' ),
cwd = self .path_files,
stdout = subprocess. PIPE ,
stderr = subprocess. PIPE ,
stdin = subprocess. PIPE ,
start_new_session = True )
self .load_plugins()
asyncio.create_task( self ._listener_console())
Starts the process with:
Creates console log queues
Spawns subprocess with start_cmd
Loads mdplugins
Begins console monitoring
stop(omit_task=False)
def stop ( self , * , omit_task = False ):
if not self .is_running():
return
self .execute( self .stop_cmd)
if not omit_task:
asyncio.create_task( self .stop_task())
Gracefully stops the process using stop_cmd.
kill(omit_task=False)
def kill ( self , * , omit_task = False ):
if isinstance ( self .process, subprocess.Popen):
try :
self .process.kill()
except :
pass
self ._find_real_process()
try :
self .real_process.kill()
except :
pass
Force-kills the process immediately.
restart()
async def restart ( self ):
if not self .is_running():
return
self .stop()
while self .is_running():
await asyncio.sleep( 0.1 )
self .start()
Stops then starts the process.
is_running(poll_based=False)
def is_running ( self , poll_based : bool = False ) -> bool :
if self .process != None :
if self .process.poll() is None or not poll_based:
return True
return False
return False
True if process is running, False otherwise
Plugin Management
load_plugins(reload=False)
def load_plugins ( self , * , reload = False ):
if not self .is_running():
return
if reload :
self .unload_plugins()
valid_extensions = [ '.py' , '.mcdis' ]
files_in_plugins_dir = os.listdir( self .path_plugins)
plugins = [ file for file in files_in_plugins_dir
if os.path.splitext( file )[ 1 ] in valid_extensions]
for plugin in plugins:
module_path = os.path.join( self .path_plugins, plugin)
spec = importlib.util.spec_from_file_location(
plugin.removesuffix( '.py' ), module_path)
mod = importlib.util.module_from_spec(spec)
spec.loader.exec_module(mod)
plugin_instance = mod.mdplugin( self )
self .plugins[os.path.splitext(plugin)[ 0 ]] = plugin_instance
Loads plugins from .mdplugins/ directory.
call_plugins(function, args)
async def call_plugins ( self , function : str , args : tuple = tuple ()):
for name, plugin in self .plugins.items():
try :
func = getattr (plugin, function, None )
if func:
await func( * args)
except :
await self .error_report(
title = f ' { function } () of { plugin } ' ,
error = traceback.format_exc()
)
Calls a method on all loaded plugins.
Console Management
execute(command)
def execute ( self , command : str ):
try :
self .process.stdin.write((command + ' \n ' ).encode())
self .process.stdin.flush()
except :
pass
Sends a command to the process’s stdin.
send_to_console(message)
async def send_to_console ( self , message : str ) -> discord.Message:
mrkd = f '```md \n { truncate(message, 1990 ) } \n ```'
remote_console = await thread( f 'Console { self .name } ' , self .client.panel)
discord_message = await remote_console.send(mrkd)
return discord_message
Sends a message to the Discord console thread.
add_log(log)
def add_log ( self , log : str ):
self ._console_relay.put( self .log_format(log))
Adds a formatted log entry to the relay queue.
def log_format ( self , log : str , type : str = 'INFO' ):
return f '[McDis] [ { datetime.now().strftime( "%H:%M:%S" ) } ] [MainThread/ { type } ]: { log } '
Formats log messages in Minecraft-style format.
Resource Monitoring
ram_usage()
def ram_usage ( self ) -> str :
if not self .is_running():
pass
elif not isinstance ( self .real_process, psutil.Process):
self ._find_real_process()
elif not self .real_process.is_running():
self ._find_real_process()
return ram_usage( self .real_process)
RAM usage string (e.g., “2.5 GB”)
disk_usage(string=True)
def disk_usage ( self , string = True ) -> float :
return get_path_size( self .path_files, string = string)
Disk usage as string or bytes
Backup Management
make_bkp(counter)
def make_bkp ( self , * , counter : list = None ):
if self .is_running():
return
# Rotate existing backups
bkp_path = os.path.join( self .path_bkps, f ' { self .name } 1.zip' )
pattern = os.path.join( self .path_bkps, f ' { self .name } [1- { self .client.config[ "Backups" ] } ].zip' )
bkps = glob.glob(pattern)
sorted_bkps = sorted (bkps, key = os.path.getmtime, reverse = True )
# Create new backup
make_zip( self .path_files, bkp_path, counter)
Creates a backup of the process directory.
unpack_bkp(backup, counter)
def unpack_bkp ( self , backup , * , counter : list = None ):
shutil.rmtree( self .path_files)
os.makedirs( self .path_files, exist_ok = True )
source = os.path.join( self .path_bkps, backup)
unpack_zip(source, self .path_files, counter)
Restores from a backup.
Discord Integration
discord_listener(message)
async def discord_listener ( self , message : discord.Message):
if not isinstance (message.channel, discord.Thread):
return
elif not message.channel.parent_id == self .client.panel.id:
return
elif not message.channel.name == f 'Console { self .name } ' :
return
if message.content.lower() == 'start' :
self .start()
elif message.content.lower() == 'stop' :
self .stop()
elif message.content.lower() == 'restart' :
await self .restart()
else :
self .execute(message.content)
Handles messages in the process’s console thread.
Error Reporting
error_report(title, error)
async def error_report ( self , * , title : str , error : str ):
formatted_title = f ' { self .name } : { title } '
error_link = await self .client.error_report(
title = formatted_title,
error = error
)
formatted_error = self .log_format( f 'Error report created. { formatted_title } ' )
remote_console = await thread( f 'Console { self .name } ' , self .client.panel)
await remote_console.send( f ' { error_link } \n ```md \n { formatted_error } \n ```' )
Reports errors to Discord with clickable links.
Usage Example
# In a plugin
class mdplugin :
def __init__ ( self , process ):
self .process = process # Process instance
def listener_events ( self , log : str ):
if "Player joined" in log:
# Execute server command
self .process.execute( "say Welcome!" )
# Send to Discord
asyncio.create_task(
self .process.send_to_console( "New player joined!" )
)
Next Steps
Server Class Server-specific implementation
Network Class Network-specific implementation
Creating Plugins Build custom plugins