Advertisement
Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- Minecraft Initscript
- Copyright (C) 2011 D3Phoenix
- For /r/minecraft:
- This is the script that I use to simplify my personal minecraft server
- administration. I am releasing it as open source to the /r/minecraft community
- in the hopes that someone will find it useful, and perhaps be able to develop
- it further. I am releasing this under the GNU GPLv3. (See code below for license)
- This started as a project to teach myself Python 3. It probably shows in the
- code that I am not a professional programmer, but hey, it works :)
- I had originally planned on developing this much further before release, but
- unfortunately I have found that I simply no longer have the time to work on it.
- As such, rather than let the project go to waste, I have decided to release
- what I have so far, in full, to the community just in case it might be of use
- to one of you.
- Thank you,
- -- D3Phoenix
- Current feature list:
- * Runs minecraft servers automatically on boot w/o login
- * Runs minecraft servers w/o root permissions, yet still as daemons.
- * LSB initscript / update-rc.d compatible
- * Still allows you to directly access the minecraft server console(s)
- even though they are running in the background.
- * Enables automation via simple cron jobs.
- * Makes it easier to run servers on an Xless box, freeing up resources
- for MOAR MINECRAFT!
- * Stop / start / restart instance(s)
- * Backup worldfiles for instance(s)
- * Update cartographer-like maps with c10t
- * Update google-like maps with minecraft overviewer
- * Broadcast a multi-line MOTD from a text file
- * Execute arbitrary commands in the server consoles
- * Connect your current terminal to a running server's console for direct control
- Stuff I should probably implement but have no time to do so right now:
- * Automatic updates for server.jar's and utility apps
- * Some way to automagically clean up all of the MOTD / backup spam in the server logs
- * Make the google map / cartographer parts optional so that they present an error if not configured
- or if the utility apps are missing, rather than preventing the script from running
- Prerequisites:
- * Good working knowledge of Linux
- * Debian-based linux OS - preferably as slim as possible
- (Developed on a debian 6 box w/ only the core packages)
- * Required additional packages:
- - GNU screen
- - pngcrush
- - python 3.x
- * Latest c10t build (optional)
- * Latest minecraft overviewer build (optional)
- Installation:
- =============
- - install prereqs above
- - create a folder structure similar to below
- - install utilities / minecraft server instances in the appropriate places
- - create an /etc/minecraft.conf accordingly
- - check your permissions! make sure everything is at owned by and at least
- rw- for the user that you configured your servers to run as!
- - copy the script into /etc/init.d/minecraft
- - copy the config file to /etc/minecraft.conf
- - test! make sure everything is working (start/stop, backups, etc)
- - Once you're satisfied, add any automated tasks you like to /etc/crontab
- - Finally, insert it into your LSB startup scripts with update-rc.d if you
- want it to automagically start/stop minecraft servers with the machine.
- Example File Structure:
- =======================
- (Through editing of the script and/or the config file, this is pretty flexible)
- /etc
- crontab (Use cron jobs to set up automatic scheduled tasks)
- minecraft.conf (The master configuration file)
- /init.d
- minecraft (The python script itself)
- /home
- /minecraft
- /mcserver
- /util
- /c10t
- c10t
- /mco
- gmap.py
- (other minecraft overviewer files)
- /www
- index.html (with links to mco and c10t output)
- /creative
- /map
- (output from c10t -- day.png, night.png, & caves.png)
- /googlemap
- (output from minecraft overviewer)
- /survival
- /map
- (output from c10t -- day.png, night.png, & caves.png)
- /googlemap
- (output from minecraft overviewer)
- /survival
- minecraft_server.jar
- server.log
- server.properties
- /mapcache
- (this is where minecraft overviewer stores its cache)
- /<worldname>
- (your minecraft world files)
- /creative
- CRAFTBUKKIT_SNAPSHOT.jar
- minecraft_server.jar
- server.log
- server.properties
- /mapcache
- (this is where minecraft overviewer stores its cache)
- /<worldname>
- (your minecraft world files)
- Note: The "minecraft" user's home folder is being used because that is the user
- that the script is configured to "demote" each server to when it starts them.
- You can move these places around (especially the www publishing directory, I can
- definitely see wanting to move that somewhere else), however it is CRITICAL that
- these locations have at least rw- permissions for the user that the servers are
- running under! Don't be fooled by the fact that you start the script as root.
- Example crontab:
- ================
- SHELL=/bin/sh
- PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin
- # Minute Hour DayOfMonth Month DayOfWeek User Command
- # Take backup snapshot of minecraft world (Every hour on the hour)
- 0 * * * * root service minecraft backup
- # Update minecraft server cartography maps (Every hour on 15m)
- 15 * * * * root service minecraft update-cartograph
- # Update minecraft server google maps (Every hour on 30m)
- 30 * * * * root service minecraft update-googlemap
- # Broadcast minecraft server MOTD messages (Every hour on 45m)
- 45 * * * * root service minecraft motd
- # Daily server restart -- warning and action (Every Thursday at 4AM)
- 0 5 * * * root service minecraft exec-all 'say Warning: Daily server restart in 5 minutes. \nsay The server takes about 5 minutes to restart. \nsay Please be patient when trying to log back in. \nsay Thank you! '
- 4 5 * * * root service minecraft exec-all 'say Warning: Daily server restart in 1 minute. \nsay The server takes about 5 minutes to restart. \nsay Please be patient when trying to log back in. \nsay Thank you! '
- 5 5 * * * root service minecraft restart
- Example minecraft.conf:
- =======================
- # This section provides all global configuration parameters
- [global]
- backup_max_age: 30
- gmap_app_path: /home/minecraft/mcserver/util/mco/
- carto_app_path: /home/minecraft/mcserver/util/c10t/
- # each of the following sections is a named server instance and all of its configuration parameters
- # For now, all options are required
- [creative]
- auto_start: 1
- server_path: /home/minecraft/mcserver/creative/
- server_jar: craftbukkit-0.0.1-SNAPSHOT.jar
- user_id: minecraft
- max_ram: 1024
- world_name: hydrogen.creative
- backup_target: /data/minecraft/backups/creative/
- googlemap_path: /home/minecraft/mcserver/www/creative/googlemap/
- cartograph_path: /home/minecraft/mcserver/www/creative/map/
- motd_path: /home/minecraft/mcserver/motd.txt
- [survival]
- auto_start: 1
- server_path: /home/minecraft/mcserver/survival/
- server_jar: minecraft_server.jar
- user_id: minecraft
- max_ram: 1536
- world_name: hydrogen.survival
- backup_target: /data/minecraft/backups/survival/
- googlemap_path: /home/minecraft/mcserver/www/survival/googlemap/
- cartograph_path: /home/minecraft/mcserver/www/survival/map/
- motd_path: /home/minecraft/mcserver/motd.txt
- The script:
- ===========
- #!/usr/bin/python3.1
- ### BEGIN INIT INFO
- # Provides: minecraft
- # Required-Start: $all
- # Required-Stop: $all
- # Default-Start: 2 3 4 5
- # Default-Stop: 0 1 6
- # Short-Description: Starts and controls a minecraft-server instance.
- ### END INIT INFO
- # Minecraft Initscript
- # Copyright (C) 2011 d3phoenix
- #
- # This program is free software: you can redistribute it and/or modify
- # it under the terms of the GNU General Public License as published by
- # the Free Software Foundation, either version 3 of the License, or
- # (at your option) any later version.
- #
- # This program is distributed in the hope that it will be useful,
- # but WITHOUT ANY WARRANTY; without even the implied warranty of
- # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- # GNU General Public License for more details.
- #
- # You should have received a copy of the GNU General Public License
- # along with this program. If not, see <http://www.gnu.org/licenses/>.
- import os
- import datetime
- import configparser
- from time import sleep
- from sys import argv, exit
- now = datetime.datetime.now()
- cp = configparser.RawConfigParser()
- scriptname = os.path.basename(argv[0])
- DEBUG = 0
- CREEPER = 1
- def main():
- # Only root is allowed to run this script, or else the way it manipulates screen won't work.
- if not os.getuid()==0:
- usage('root privileges are required to run this script.')
- else:
- # Parse the configuration file
- result = cp.read('/etc/minecraft.conf')
- if len(result) == 0:
- fatal('missing or invalid /etc/minecraft.conf')
- # Read in the global settings
- try:
- backup_max_age = cp.get('global','backup_max_age')
- gmap_app_path = cp.get('global','gmap_app_path')
- carto_app_path = cp.get('global','carto_app_path')
- except(NoSectionError, NoOptionError):
- fatal('missing or invalid /etc/minecraft.conf')
- debug('read settings from config file: backup_max_age=' + backup_max_age + ' gmap_app_path=' + gmap_app_path + ' carto_app_path=' + carto_app_path)
- # Get the list of instances (remove the 'global' section we already parsed)
- try:
- instance_list = cp.sections()
- instance_list.remove('global')
- except ValueError:
- fatal('missing or invalid /etc/minecraft.conf')
- debug('retrieved instance list: ' + ' '.join(instance_list))
- debug('retreived command line args: ' + ' '.join(argv))
- # Begin processing the arguments; start with the first.
- if len(argv) >= 2:
- action = argv[1].lower()
- if action in ['help', '--help', 'usage']:
- usage()
- # There are two variants of the special 'exec' command:
- # minecraft exec <instance-name> <execute-string>
- # minecraft exec-all <execute-string>
- # The first executes against a named instance,
- # The second executes sequentially against them all.
- # Having them as distinct functions removes ambiguity in the parameters as far as whether you are specifying
- # an instance name or part of the command you want to execute.
- elif action in ['exec-all']:
- debug('selected action: exec-all')
- if len(argv) >= 3:
- execute_string = ' '.join(argv[2:])
- for instance_name in instance_list:
- report(instance_name + ': executing "' + execute_string + '"...')
- send_to_screen(instance_name, execute_string)
- else:
- usage('missing required parameter')
- elif action in ['exec']:
- debug('selected action: exec')
- if len(argv) >= 3:
- if argv[2].lower() in instance_list:
- # The second argument was a valid instance, continue...
- if len(argv) >= 4:
- instance_name = argv[2].lower()
- execute_string = ' '.join(argv[3:])
- report(instance_name + ': executing "' + execute_string + '"...')
- send_to_screen(instance_name, execute_string)
- else:
- usage('missing required parameter')
- else:
- usage('missing required parameter')
- # The 'console' action is special in that it requires you to specify the instance name or it will not work.
- elif action in ['console']:
- debug('selected action: console')
- if len(argv) >= 3:
- instance_name = argv[2].lower()
- if not instance_name in instance_list:
- fatal('instance "' + instance_name + '" not configured. Please check /etc/minecraft.conf (World names are case-sensitive)')
- else:
- if is_instance_running(instance_name):
- send_to_screen(instance_name, '***** Connected to server console. Press CTRL+A, then D to leave. *****')
- os.system('screen -r ' + instance_name)
- else:
- usage('missing required parameter')
- # The 'status' action is special in that it does not accept any arguments. It lists all known server instances regardless of the command line.
- elif action in ['status']:
- print()
- print("Server Instance: Status")
- print("=============================================")
- for instance in instance_list:
- if is_instance_running(instance):
- instance_status = "Running"
- else:
- instance_status = "Stopped"
- print(" " + instance + ": " + instance_status)
- print("=============================================")
- print()
- # All remaining valid actions accept either one or zero arguments based on whether the user wants it to run against a selected instance or all instances.
- else:
- debug('selected action: other (text:' + action + ')')
- # Determine whether we are running against a single instance or all of them, and return a list for the loop accordingly
- if len(argv) >= 3:
- if argv[2].lower() in instance_list:
- selected_instances = [argv[2].lower()]
- else:
- fatal('instance "' + argv[2].lower() + '" not configured in /etc/minecraft.conf')
- else:
- selected_instances = instance_list
- debug('selected instances: ' + ' '.join(selected_instances))
- for instance_name in selected_instances:
- debug('processing instance: ' + instance_name)
- # Read in the instance-specific variables for each iteration of the loop
- auto_start = cp.get(instance_name, 'auto_start')
- server_path = cp.get(instance_name, 'server_path')
- server_jar = cp.get(instance_name, 'server_jar')
- user_id = cp.get(instance_name, 'user_id')
- max_ram = cp.get(instance_name, 'max_ram')
- world_name = cp.get(instance_name, 'world_name')
- backup_target = cp.get(instance_name, 'backup_target')
- googlemap_path = cp.get(instance_name, 'googlemap_path')
- cartograph_path = cp.get(instance_name, 'cartograph_path')
- motd_path = cp.get(instance_name, 'motd_path')
- if action in ['start']:
- # Only start a server if 'auto_start' is true, or the server name was given explicitly
- if (auto_start.lower() in ['1', 'true', 'yes']) or (len(selected_instances) == 1):
- start_instance(instance_name, user_id, server_path, server_jar, max_ram)
- elif action in ['stop']:
- stop_instance(instance_name)
- elif action in ['stop-now', 'force-stop']:
- stop_instance(instance_name, True)
- elif action in ['restart', 'try-restart', 'reload']:
- restart_instance(instance_name, server_path, server_jar, max_ram)
- elif action in ['restart-now', 'force-restart', 'force-reload']:
- restart_instance(instance_name, server_path, server_jar, max_ram, True)
- elif action in ['update-backup', 'backup']:
- update_backup(instance_name, server_path, world_name, backup_target, backup_max_age)
- elif action in ['update-cartograph']:
- update_cartograph(user_id, instance_name, server_path, world_name, carto_app_path, cartograph_path)
- elif action in ['update-googlemap']:
- update_googlemap(user_id, instance_name, server_path, world_name, gmap_app_path, googlemap_path)
- elif action in ['motd']:
- broadcast_motd(instance_name, motd_path)
- else:
- # Unrecognized command? Time to troll a bit :)
- if CREEPER:
- creeper()
- usage('("' + action + '" is not a supported action)')
- else:
- usage('missing required parameter')
- def start_instance(instance_name, user_id, server_path, server_jar, max_ram):
- debug('start_instance called')
- if is_instance_running(instance_name):
- report(instance_name + ': already started.')
- else:
- report(instance_name + ': spawning & configuring screen daemon...')
- # Spawn a new screen daemon containing a bash shell. We will manipulate this
- # to drop permissions and start the server. The screen itself runs as root.
- os.system('screen -dmS ' + instance_name + ' /bin/bash')
- # Wait for shell to initialize
- sleep(3)
- # Because this is an initscript, we have scary root permissions inside our spawned shell.
- # DO NOT WANT! The server shouldn't these perms, therefore, su - to a more appropriate account:
- send_to_screen(instance_name, 'su - ' + user_id)
- # This next line is an ugly hack to move the terminal to a new /dev/pts/* attachment. When we su -,
- # the terminal's /dev/pts/* owner is not updated, so screen will not have appropriate rights to interact with it properly.
- # Firing off the script command works around this, by forcing us into a new /dev/pts/* with correct ownership.
- send_to_screen(instance_name, 'script /dev/null')
- # Move to the correct working directory
- send_to_screen(instance_name, 'cd ' + server_path)
- # Finally, we can start the server!
- report(instance_name + ': starting server, please wait (30 sec)...')
- send_to_screen(instance_name, 'java -Xms' + max_ram + 'm -Xmx' + max_ram + 'M -jar ' + server_jar + ' nogui')
- # Wait a bit longer -- it takes the server a while to start
- sleep(30)
- # Report our status
- report(instance_name + ': startup complete.')
- def stop_instance(instance_name, stop_now=False):
- debug('stop_instance called')
- if is_instance_running(instance_name):
- send_to_screen(instance_name, 'say *** SERVER SHUTDOWN REQUESTED FROM CONSOLE ***')
- if not stop_now:
- report(instance_name + ': informing users of shutdown. Please wait (30 sec)...')
- send_to_screen(instance_name, 'say Server will go offline in 30 seconds. Please log out.')
- sleep(12)
- report(instance_name + ': informing users of shutdown. Please wait (15 sec)...')
- send_to_screen(instance_name, 'say Server will go offline in 15 seconds. Please log out.')
- sleep(7)
- send_to_screen(instance_name, 'say Server will go offline in 5 seconds. Goodbye!')
- sleep(2)
- report(instance_name + ': stopping server...')
- send_to_screen(instance_name, 'stop')
- sleep(10)
- report(instance_name + ': cleaning up...')
- # Once the server has stopped, we will need to exit from our su - session...
- send_to_screen(instance_name, 'exit')
- # Close out of the script session...
- send_to_screen(instance_name, 'exit')
- # And finally, close out of the original shell
- send_to_screen(instance_name, 'exit')
- # At this point the screen should detach and die automatically.
- report(instance_name + ': stop complete.')
- def restart_instance(instance_name, server_path, server_jar, max_ram, restart_now=False):
- debug('restart_instance called')
- if is_instance_running(instance_name):
- send_to_screen(instance_name, 'say *** SERVER RESTART REQUESTED FROM CONSOLE ***')
- if not restart_now:
- report(instance_name + ': informing users of restart. Please wait (30 sec)...')
- send_to_screen(instance_name, 'say Server will restart in 30 seconds. Please log out.')
- sleep(12)
- report(instance_name + ': informing users of restart. Please wait (15 sec)...')
- send_to_screen(instance_name, 'say Server will restart in 15 seconds. Please log out.')
- sleep(7)
- send_to_screen(instance_name, 'say Server will restart in 5 seconds. Goodbye!')
- sleep(2)
- send_to_screen(instance_name, 'say Server restarting...')
- report(instance_name + ': stopping server...')
- send_to_screen(instance_name, 'stop')
- sleep(10)
- report(instance_name + ': restarting server...')
- send_to_screen(instance_name, 'java -Xms' + max_ram + 'm -Xmx' + max_ram + 'M -jar ' + server_jar + ' nogui')
- sleep(10)
- report(instance_name + ': restart cycle complete.')
- def update_backup(instance_name, server_path, world_name, backup_target, backup_max_age):
- debug('update_backup called')
- source = '"' + server_path + world_name + '/"'
- destination_path = '"' + backup_target + world_name + '.backup.'
- destination = destination_path + now.strftime('%Y.%m.%d-%H.%M') + '.tar.bz2"'
- if is_instance_running(instance_name):
- report(instance_name + ': informing users of update. Please wait (15 sec).')
- send_to_screen(instance_name, 'say World backup starting in 15 seconds...')
- sleep(12)
- send_to_screen(instance_name, 'say Starting backup...')
- send_to_screen(instance_name, 'save-off')
- send_to_screen(instance_name, 'save-all')
- sleep(5)
- report(instance_name + ': backing up world files to ' + destination + '; please wait...')
- os.system('mkdir -p "' + backup_target + '"')
- os.system('tar -cjf ' + destination + ' ' + source)
- os.system('find ' + destination_path + '"* -mtime +' + backup_max_age + ' -exec rm {} \;')
- if is_instance_running(instance_name):
- send_to_screen(instance_name, 'save-on')
- send_to_screen(instance_name, 'say Backup complete.')
- report(instance_name + ': backup complete.')
- def update_googlemap(user_id, instance_name, server_path, world_name, gmap_app_path, googlemap_path):
- debug('update_googlemap called')
- report(instance_name + ': updating google map; please wait...')
- if is_instance_running(instance_name):
- send_to_screen(instance_name, 'say Updating google map...')
- user_exec(user_id, 'mkdir -p "' + googlemap_path + '"')
- user_exec(user_id, 'nice ' + gmap_app_path + 'gmap.py -v -p 1 --cachedir=' + server_path + 'mapcache/ ' + server_path + world_name + ' ' + googlemap_path)
- if is_instance_running(instance_name):
- send_to_screen(instance_name, 'say Google map update finished.')
- report(instance_name + ': google map update complete.')
- def update_cartograph(user_id, instance_name, server_path, world_name, carto_app_path, cartograph_path):
- debug('update_cartograph called')
- print(instance_name + ': updating cartography maps; please wait...')
- if is_instance_running(instance_name):
- send_to_screen(instance_name, 'say Updating cartography maps...')
- user_exec(user_id, 'nice ' + carto_app_path + 'c10t -m 1 -w ' + server_path + world_name + ' -o ' + cartograph_path + 'day.pngcache')
- user_exec(user_id, 'nice ' + carto_app_path + 'c10t -n -m 1 -w ' + server_path + world_name + ' -o ' + cartograph_path + 'night.pngcache')
- user_exec(user_id, 'nice ' + carto_app_path + 'c10t -c -m 1 -w ' + server_path + world_name + ' -o ' + cartograph_path + 'caves.pngcache')
- user_exec(user_id, 'nice pngcrush ' + cartograph_path + 'day.pngcache ' + cartograph_path + 'day.png')
- user_exec(user_id, 'nice pngcrush ' + cartograph_path + 'night.pngcache ' + cartograph_path + 'night.png')
- user_exec(user_id, 'nice pngcrush ' + cartograph_path + 'caves.pngcache ' + cartograph_path + 'caves.png')
- user_exec(user_id, 'rm ' + cartograph_path + '*.pngcache')
- if is_instance_running(instance_name):
- send_to_screen(instance_name, 'say Cartography update finished.')
- report(instance_name + ': cartography update complete.')
- def broadcast_motd(instance_name, motd_path):
- debug('broadcast_motd called')
- if is_instance_running(instance_name):
- report(instance_name + ': broadcasting motd from file: ' + motd_path)
- debug('reading motd file')
- motdfile = open(motd_path, 'r')
- motdlines = motdfile.readlines()
- for m in motdlines:
- debug('parsing motd line: ' + m)
- if not m[0] in ['#', ';']:
- debug('line does not start with comment')
- send_to_screen(instance_name, 'say ' + m.strip())
- else:
- debug('line started with comment, skipping')
- report(instance_name + ': motd broadcast complete.')
- def send_to_screen(instance_name, command):
- debug('send_to_screen called; instance=' + instance_name + ', command=' + command)
- if is_instance_running(instance_name):
- # The nested quotes to get this working are ugly as the nether.
- # Made a function out of it so that I didn't go insane.
- execute_string = 'screen -S ' + instance_name + ' -p 0 -X stuff "`printf "' + command + '\\r"`"'
- os.system(execute_string)
- # Always wait a few seconds between commands, or things tend to glitch out on multicore systems (race condition? limitation of screen?)
- # Three seems to be the fastest setting that works reliably.
- sleep(3)
- def is_instance_running(instance_name):
- debug('is_instance_running called against: ' + instance_name)
- # Assume no screen session exists
- screen_exists = False
- # Search for a matching screen session
- for screen in os.listdir('/var/run/screen/S-root'):
- if instance_name in screen:
- # If we found a match, then the screen is running.
- screen_exists = True
- break
- # Return what we found
- return screen_exists
- def user_exec(user, execute_string):
- su_execute_string = "su " + user + " -l -c '" + execute_string + "'"
- debug('Executing: ' + su_execute_string)
- os.system(su_execute_string)
- def report(msg):
- print('[' + scriptname + ']: ' + msg)
- def debug(msg):
- if DEBUG:
- print('[' + scriptname + ']: ' + msg)
- def fatal(msg):
- print('[' + scriptname + ']: Fatal - ' + msg)
- print()
- exit(1)
- def usage(msg = ''):
- if len(msg):
- print('[' + scriptname + ']: Error - ' + msg)
- print()
- print('Usage: ' + scriptname + ' <action> [instance_name]')
- print()
- print('<action>')
- print(' start start instance(s)')
- print(' stop stop instance(s) with user warning period')
- print(' stop-now stop instance(s) immediately')
- print(' restart restart instance(s) with user warning period')
- print(' restart-now restart instance(s) immediately')
- print(' backup back up world files for the selected instance(s)')
- print(' update-cartograph update the cartography (run c10t)')
- print(' update-googlemap update the google map (run minecraft-overviewer)')
- print(' motd send the preconfigured motd text file to chat')
- print(" * exec <command> execute <command> in the selected instance")
- print(" * console <instance> connect to selected instance's console")
- print()
- print('[instance_name]')
- print(" Optional except for commandes noted with '*' above. ")
- print()
- print(' When specified, if it matches an instance configured in')
- print(' /etc/minecraft.conf, then the selected action will apply')
- print(' only to that instance. If there is no match, the script')
- print(' will exit with an error code.')
- print()
- print(' If [instance_name] is not specified, then the script will')
- print(' execute the command against each server in series.')
- print()
- exit(1)
- def creeper():
- print()
- print('SSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSS')
- print('SSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSS')
- print('SSS SSSSSSSSSS SSS')
- print('SSS SSSSSSSSSS SSS')
- print('SSS SSSSSSSSSS SSS')
- print('SSS SSSSSSSSSS SSS')
- print('SSS SSSSSSSSSS SSS')
- print('SSSSSSSSSSSSS SSSSSSSSSSSSS')
- print('SSSSSSSSSSSSS SSSSSSSSSSSSS')
- print('SSSSSSSSSSSSS SSSSSSSSSSSSS')
- print('SSSSSSSS SSSSSSSS')
- print('SSSSSSSS SSSSSSSS')
- print('SSSSSSSS SSSSSSSS')
- print('SSSSSSSS SSSSSSSS')
- print('SSSSSSSS SSSSSSSS')
- print('SSSSSSSS SSSSSSSSSS SSSSSSSS')
- print('SSSSSSSS SSSSSSSSSS SSSSSSSS')
- print('SSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSS')
- print('SSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSS')
- print()
- ####################################
- if __name__ == "__main__":
- main()
- ####################################
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement