Pyinstaller Onefile Frage zum _MEIPASS

Wenn du dir nicht sicher bist, in welchem der anderen Foren du die Frage stellen sollst, dann bist du hier im Forum für allgemeine Fragen sicher richtig.
Antworten
rlinke81
User
Beiträge: 3
Registriert: Mittwoch 5. Juli 2023, 20:59

Hallo Python-Community,

ich habe vor Kurzem mein Python-Projekt mit PyInstaller in ein Onefile komprimiert. Das Projekt besteht aus zwei Hauptskripten: main.py und config_editor.py. Vor der Komprimierung konnte ich beide Skripte problemlos ausführen.

Mein Ziel ist es, eine GUI-Anwendung (gui.pyw) zu erstellen, die einen Login-Prozess enthält und nach erfolgreichem Login das Hauptskript main.py startet. Das GUI-Skript habe ich ebenfalls zu einem Onefile komprimiert.

Code: Alles auswählen

import tkinter as tk
from tkinter import messagebox
import subprocess
import os
import time
import requests
import ctypes
import sys


def resource_path(relative_path):
    """ Get absolute path to resource, works for dev and for PyInstaller """
    base_path = getattr(sys, '_MEIPASS', os.path.dirname(os.path.abspath(__file__)))
    return os.path.join(base_path, relative_path)


class LoginWindow(tk.Tk):
    def __init__(self):
        super().__init__()
        self.title("Login")
        self.geometry("200x150")

        self.username_label = tk.Label(self, text="Username:")
        self.username_label.pack()
        self.username_entry = tk.Entry(self)
        self.username_entry.pack()

        self.password_label = tk.Label(self, text="Password:")
        self.password_label.pack()
        self.password_entry = tk.Entry(self, show="*")
        self.password_entry.pack()

        self.login_button = tk.Button(self, text="Login", command=self.login)
        self.login_button.pack()

    def login(self):
        username = self.username_entry.get()
        password = self.password_entry.get()
        r = requests.get(f'/login/{username}/{password}/')
        if 'Success' in r.text:
            login_status_file = resource_path("login_status.txt")
            with open(login_status_file, "w") as f:
                f.write("True")
            self.destroy()
            MainWindow().start_main()
        elif 'Wrong' in r.text:
            messagebox.showerror("Login Failed", f"Wrong password for username {username}!")
        elif 'existing' in r.text:
            messagebox.showerror("Login Failed", "This account does not exist! Please register!")


class MainWindow(tk.Tk):
    def __init__(self):
        super().__init__()
        self.title("Bra1nBot")

        # Vollständiger Pfad zur Bilddatei
        image_path = resource_path("hintergrundbild.png")

        # Hintergrundbild
        self.background_image = tk.PhotoImage(file=image_path)
        self.background_label = tk.Label(self, image=self.background_image)
        self.background_label.place(x=0, y=0, relwidth=1, relheight=1)

        # Fenstergröße an Bildgröße anpassen
        self.geometry("{}x{}".format(self.background_image.width(), self.background_image.height()))

        # Status-Label
        self.status_label = tk.Label(self, text="Status: Inaktiv", fg="red")
        self.status_label.pack()

        # Start-Button
        self.start_button = tk.Button(self, text="Start", command=self.start_main)
        self.start_button.pack()

        # Config-Button
        self.config_button = tk.Button(self, text="Config", command=self.open_config_editor)
        self.config_button.pack()

        # Exit-Button
        self.exit_button = tk.Button(self, text="Exit", command=self.exit_program)
        self.exit_button.pack()

        # Beenden-Button für main.py
        self.close_button = tk.Button(self, text="Beenden", command=self.close_main)
        self.close_button.pack()

    def start_main(self):
        # Erstelle die login_status.txt-Datei
        login_status_file = resource_path("login_status.txt")
        with open(login_status_file, "w") as file:
            file.write("True")

        # Starte main.py im Hintergrund
        main_path = resource_path("main.py")
        subprocess.Popen(["python", main_path], creationflags=subprocess.DETACHED_PROCESS, close_fds=True)

        # Rufe die update_status_label-Methode auf, um den Status zu aktualisieren
        self.update_status_label()

    def open_config_editor(self):
        try:
            base_path = getattr(sys, '_MEIPASS', os.path.dirname(os.path.abspath(__file__)))
            config_editor_path = os.path.join(base_path, "config_editor.py")
            subprocess.Popen(["pythonw", config_editor_path], creationflags=subprocess.DETACHED_PROCESS, close_fds=True)
        except FileNotFoundError:
            messagebox.showerror("Fehler", "config_editor.py not found!")
        except Exception as e:
            messagebox.showerror("Fehler", str(e))


    def exit_program(self):
        login_status_file = resource_path("login_status.txt")
        if os.path.exists(login_status_file):
            os.remove(login_status_file)
        # Beende das Programm
        self.destroy()

    def close_main(self):
        login_status_file = resource_path("login_status.txt")
        if os.path.exists(login_status_file):
            os.remove(login_status_file)
        # Schließe die main.py
        pid_file = resource_path("main_pid.txt")
        try:
            with open(pid_file, "r") as f:
                pid = int(f.read().strip())
                subprocess.Popen(["taskkill", "/F", "/PID", str(pid)], shell=True)
            os.remove(pid_file)
            self.update_status_label()
        except FileNotFoundError:
            messagebox.showerror("Fehler", "Bra1nBot not active!")

    def update_status_label(self):
        # Überprüfe, ob main.py aktiv ist und aktualisiere den Status
        pid_file = resource_path("main_pid.txt")
        if os.path.isfile(pid_file):
            self.status_label.config(text="Status: Aktiv", fg="green")
        else:
            self.status_label.config(text="Status: Inaktiv", fg="red")
        self.after(1000, self.update_status_label)  # Überprüfe den Status alle 1 Sekunde


if __name__ == "__main__":
    try:
        r = requests.get('login url')
        if 'Im online!' in r.text:
            login_window = LoginWindow()
            login_window.mainloop()
        else:
            ctypes.windll.user32.MessageBoxW(0, "Server is offline! Try again later!", "Server is offline!", 0)
    except:
        ctypes.windll.user32.MessageBoxW(0, "Couldn't connect to server!", "You are offline!", 0)
Das Problem ist, dass ich nach der Komprimierung und Erstellung der gui.exe-Datei nicht mehr in der Lage bin, das Hauptskript main.py über die GUI zu starten. Die GUI-Exe funktioniert einwandfrei und der Login-Prozess läuft problemlos ab, aber nach dem Login möchte ich gerne das Hauptskript ausführen.

Beim Erstellen des Onefile mit PyInstaller wird der _MEIPASS-Ordner erstellt, der alle Dateien des Projekts enthält. Die Endgröße der gui.exe-Datei beträgt etwa 2 GB, was darauf hindeutet, dass alle Skripte und Ressourcen darin enthalten sind.

Meine Frage lautet nun: Wie kann ich das Hauptskript main.py über die gui.exe starten, nachdem der Login abgeschlossen ist?

Noch mal kurz :)
Alle Skripte, einschließlich gui.pyw, main.py und config_editor.py, sind im selben Verzeichnis. In der Python-Version funktionieren alle Skripte einwandfrei.

Um das Projekt auf anderen Computern auszuführen, habe ich versucht, sie in ausführbare .exe-Dateien umzuwandeln. Dabei wurden alle Abhängigkeiten, einschließlich der Konfigurationsdatei im JSON-Format, als separate Dateien mit aufgenommen. Die Erstellung der .exe-Dateien war erfolgreich, aber nach der Umwandlung kann ich weder das Hauptskript main.py noch den Konfigurationseditor über die GUI starten.

Ich habe versucht, die Pfade zu den Skripten in der GUI entsprechend anzupassen, indem ich den _MEIPASS-Ordner verwendet habe, um auf die Dateien zuzugreifen. Trotzdem bekomme ich beim Versuch, die Skripte über die GUI.exe zu starten, Fehlermeldungen oder es passiert einfach nichts.

Ich frage mich, ob es bestimmte Anpassungen gibt, die ich vornehmen muss, um die Skripte erfolgreich über die GUI.exe auszuführen. Gibt es möglicherweise Besonderheiten beim Umgang mit dem _MEIPASS-Ordner, die ich berücksichtigen muss?

Ich freue mich über Ihre Hilfe und bedanke mich im Voraus für Ihre Unterstützung.

Beste Grüße
Benutzeravatar
__blackjack__
User
Beiträge: 14056
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@rlinke81: Ich würde sagen aus einem Python-Programm aus (sogar mehrere) weitere Python-Programme mit `Popen` starten zu wollen ist ziemlicher Murks.

Ebenfalls falsch ist, dass es in diesem Programm zwei `Tk`-Objekte gibt. Das darf es nur einmal geben — das ist *das* Hauptfenster. Zusätzliche Fenster macht man mit `Toplevel`.

`ctypes.windll` wird verwendet um Fenster mit Nachrichten zu öffnen — dafür hat `tkinter` doch auch etwas was nicht von Windows abhängig ist. `Popen()` mit ``shell=True`` um einen Prozess zu beenden: das geht auch mit Python. Das ``shell=True`` ist besonders sinnfrei, weil das Kommando ja als Liste übergeben wird.

`os.path` in neuen Programmen? Es gibt `pathlib`. `resource_path()` wird dann auch eher überflüssig.

`strip()` ist vor `int()` unnötig, das macht `int()` selbst schon.

Sofern nicht irgendwo in der Dokumentation garantiert ist, dass man im `_MEIPASS` Verzeichnis schreiben darf, würde ich die Kommunikation der Programme über Textdateien dort als Fehler ansehen. Wirklich schön ist das sowieso nicht. Die "login_status.txt" scheint sinnfrei zu sein. Und die PID-Datei könnte man sich sparen wenn man sich den Prozess als Objekt merken würde und nicht einfach „fire and forget“ praktizieren würde.

Es gibt im Hauptfenster einen „Exit“-Button und einen „Beenden“-Button. Hä‽

`r` ist kein guter Name. Es wird nirgends geprüft ob bei dem Anfragen an den Server eine normale Antwort oder eine HTTP-Fehler-Seite kam.

Fenstergrössen fest vorgeben ist nicht richtig. Das kann funktionieren. Kann aber auch komisch aussehen oder sogar unbenutzbar sein.
“Vir, intelligence has nothing to do with politics!” — Londo Mollari
rlinke81
User
Beiträge: 3
Registriert: Mittwoch 5. Juli 2023, 20:59

Ja das hilft mir jetzt schon mal weiter, aber ich hänge immer noch an dem Problem das ich nachdem kompilieren in ein Onefile über meine Gui.exe keine weiteren Programme starten kann.
Vorher bin ich so vorgegangen, das sich Gui.pyw und main.py und config_editor.py im gleichen Verzeichnis befunden haben und ich konnte diese dann einfach starten. Jetzt weiß ich aber nicht wie ich die kompilierte main.py die ja zusammen in der Gui.exe ist nachträglich starten kann.

Ich ging davon aus das das Verzeichnis ins Temporäre _MEIPASS entpackt wird und von dort aus gestartet wird, aber selbst wenn ich versuche über pathlib, resource oder sonstigen pfad im Anschluß meine main.py zu starten aus dem MEIPASS Ordner passiert einfach nichts.
Kompiliere ich nur die main.py wird mein Programm einwandfrei ausgeführt, sobald ich aber die Gui ins Spiel bringe kann ich die einzelnen Instanzen nicht mehr aufrufen.

Beenden ist dafür das ich die main.py wieder beenden konnte ohne die Gui zu verlassen und Exit war einfach nur das ich die Gui verlassen konnte. Ist noch übrig geblieben vom Testen, weil ich es so einfacher hatte.
Jetzt möchte ich einfach nur mein Main Programm und den Config_Editor.py starten können nachdem alles in eine .exe kompiliert wurde.
Benutzeravatar
__blackjack__
User
Beiträge: 14056
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@rlinke81: Wie gesagt, hör einfach auf das zu wollen. Auch wenn das keine einzige EXE ist, macht man das nicht, aus dem eigenen Python-Programm andere eigene Python-Programme starten. Das ist ja letztlich *ein* Programm, also würde man die anderen Module importieren und die Funktionen zum starten einfach aufrufen. Wenn das auch GUI-Programme sind, dann in die gleiche `mainloop()` integrieren, und wenn das etwas ist was im Hintergrund läuft, in einem Thread starten.
“Vir, intelligence has nothing to do with politics!” — Londo Mollari
Antworten