1

Edit: I am running this on windows as the servers are administered on my main gaming rig

I am trying to create a minecraft server controller so that I can streamline the process of administering servers. I have been able to use subprocess to start and stop the server with no issues however when I try to get it to issue commands It will not. Unless I use subprocess.communicate() the problem with this solution however is that it causes the script to wait forever and eventually crash, leaving the server running. Here is the code for what I have.

import subprocess


class Server:
    def __init__(self, server_start_bat, dir):
        self.server_start_bat = server_start_bat
        self.dir = dir

    def start_server(self):
        self.server = subprocess.Popen(self.server_start_bat, cwd=self.dir, shell=True, stdin=subprocess.PIPE,
                                       universal_newlines=True, text=True)

    def stop_server(self):
        self.run_command('stop')
        self.server.communicate()

    def op_me(self):
        self.run_command(f'op Username')

    def clear_weather(self):
        self.run_command(f'weather clear 1000000')

    def mob_griefing_on(self):
        self.run_command(f'gamerule mobGriefing True')

    def mob_griefing_off(self):
        self.run_command(f'gamerule mobGriefing True')

    def set_time_day(self):
        self.run_command(f'time set day')

    def set_time_night(self):
        self.run_command(f'time set night')

    def run_command(self, command_text):
        self.server.stdin.write(f'{command_text}\n')

1 Answers1

1

Edit: Check out Non-blocking read on a subprocess.PIPE in python

I was battling a very similar issue and it took me 3 days to get it all running. The problem with subprocess.communicate is that you can only call it once, and the output seems to be given only once the subprocess terminates (in the experience i had, could very well be wrong).

The question i had, which your question imho boils down to was: how can I write to a processes stdin and read from its stdout while it is still running?

I ended up using Pipes. Note that they have to me flushed if you write something that is smaller that BUFSIZE, which is 0x1000 in my case. See man pipe for more info.

Anyway, below is my code.

import time
import pty
import os
import subprocess
PIPE_BUFSIZE = 4096
_control = False


class Pipe:

    def __init__(self, flags=None, terminal=True):
        """Creates a Pipe you can easily write to and read from. Default is to open up a pseudo-terminal.
            If you supply flags, pipe2 is used."""

        if flags or not terminal:
            self._readfd, self._writefd = os.pipe2(flags)
        else:   # default
            self._readfd, self._writefd = pty.openpty()

        self.readobj = open(self._readfd, "rb", 0)  
        self.writeobj = open(self._writefd, "wb", 0)

    def write(self, text):
        if isinstance(text, str):
            text = text.encode()

        result = self.writeobj.write(text)
        self.writeobj.flush()
        return result

    def read(self, n):
        if _control:    # im not using this anymore since the flush seems to be reliable
            controlstr = b"this_was_flushed"
            controllen = len(controlstr)

            self.writeobj.write(controlstr)
            time.sleep(.001)
            self.writeobj.flush()

            result = self.readobj.read(n+controllen)
            assert result[-controllen:] == controlstr
            return result[:-controllen]
        else:
            self.writeobj.flush()
            return self.readobj.read(n)


class ProcessIOWrapper:
    """Provides an easy way to redirect stdout and stderr using pipes. Write to the processes STDIN and read from STDOUT at any time! """

    def __init__(self, args, inittext=None, redirect=True):

        #create three pseudo terminals
        self.in_pipe = Pipe()
        self.out_pipe = Pipe()
        self.err_pipe = Pipe()

        # if we want to redirect, tell the subprocess to write to our pipe, else it will print to normal stdout
        if redirect:
            stdout_arg= self.out_pipe.writeobj
            stderr_arg= self.err_pipe.writeobj
        else:
            stdout_arg=None
            stderr_arg= None

        self.process = subprocess.Popen(args, stdin=self.in_pipe.readobj, stdout=stdout_arg, stderr=stderr_arg)


    def write(self, text):
        return self.in_pipe.write(text)

    def read(self, n, start=None):
        return self.out_pipe.read(n)




Small C Program i used for testing (used others, too)

#include <stdio.h>


int main(){
    puts("start\n");
    char buf[100]="bufbufbuf";
    while(1){

        gets(buf);
        for (int i=0; i<strlen(buf); i++)
            if (i%2==0) buf[i]+=1;

        puts(buf);
    }
}


python00b
  • 113
  • 10
  • 1
    Also, if you think this answers the question, consider changing the title of your question to something more general (no minecraft) so people can find the question more easily:) – python00b Mar 27 '20 at 14:22
  • 2
    In this case it might be helpful to read up on *blocking* and *non-blocking* subprocess calls. – code-lukas Mar 27 '20 at 14:36
  • I think that solution would work. Other than that I am on windows and don't have access to pty and I'm unaware of any alternatives for windows – SammyRigdon496 Mar 27 '20 at 15:21
  • 1
    @code-lukas Good point, I also struggled with that. subprocess.checkoutput or subprocess.run is blocking, for example. Sammy, if you like my solution please consider accepting my question:) I think os.pipe should work fine, did you try that? – python00b Mar 27 '20 at 16:49
  • @pythonn00b I feel like I may be misunderstanding your answer. I am relatively new to programming in general and am unable to get the solution you provided to work. I am getting the servers to all start when I want them to but using am unable to issue commands or even stop the server using the write function at all. I've tried making the server process an instance of the class and modifying the wrapper class to include the commands I need still haven't been able to get it to function. Am I missing something simple? – SammyRigdon496 Mar 27 '20 at 20:12
  • 1
    I recommend trying my code on a small simple program to see if my class even works on your windows machine. Use python3 -i script.py to open up the interactive shell and play around, maybe manually launch a minecraft server and see if you can read from it. :) This stuff took me 3 days, so dont let it get to you if its not working right away. Look up how Pipes work in Windows and try to find some code examples online. – python00b Mar 28 '20 at 06:35