Mini Serveur Web Python et problème de log

Salut, j’ai demandé à ChatGPT, mais il semble un peu perdu dans ce genre de problème. Le problème n’est pas spécifique au Raspberry Pi, en fait c’est même pour Windows, mais vu que le Python est fortement utilisé et que c’est le seul forum que je suis actif, je me suis dit que je pourrais trouver un début de solution.

Mon projet en gros est de pouvoir gérer des étiquette pour imprimante Zebra et les imprimer, toute cette facette marche, je me suis basé sur mon code pour les imprimante Brother QL que j’avais fait.

Le problème est avec le module Web, qui offre l’interface pour gérer l’étiquette et l’envoyer en impression. Ce script marche parfaitement si je le lance directement avec Python ou si je le compile avec une console visible. Mais l’objectif c’est de ne pas avoir de console pour le lancer.

Je peux cacher la console avec un script en .VBS (Visual Basic Script), mais je préfère éviter.

Alors il faut savoir que j’ai bien sur des logs pour moi, soit avec print() ou avec showMessage(), mais eux ne semble pas poser problème. Dans mes tests, je sais que dans tout les cas le script marche et traite les demande correctement.

Le problème est le suivant; en version compilé sans console, toutes demandes Web par un client est accepté mais retourne aucune réponse, pas de donnée, pas d’entête, c’est une fin de connexion sans réponse.

Je pense que le problème vient du module socket, ce code est pris d’un code public et donc je ne comprend pas nécessairement le fonctionnement (comme la partie pour créer la « class »), mais j’ai fais le restant du code en me basant sur l’exemple et l’ajustant avec mes observations. Ce n’est peut-être pas optimal, mais ça marche.

Mais quand le socket reçois une demande, il retourne dans la console une information, soit l’entête HTTP du client (du même style qu’ont retrouve dans les logs de Apache), ce qui est normal. Je sais qu’il apparait APRÈS mes print() qui commence par « ** ». Alors il doit être généré lors du « return », ou dans les environ. J’ai tenté une recherche pour voir si je peux arrêter ce genre de log (même avant le problème de la version compilé) mais je n’ai rien trouvé.

ChatGPT ma donné quelques lignes pour orienter les logs ailleurs, dans un fichier, dans un Null, … mais mis a part mes propre print(), les logs du module Socket ne change pas de place. J’ai même tenté de démarrer le serveur en sous processus, j’ai toujours des réponse vide si je n’ai pas de console.

Au final j’aimerais que la version compilé puisse répondre, et je crois que si j’arrive à éliminer les sortie du Socket je réglerais mon problème de la version compilé. Si vous connaissez directement la solution pour la compilation, tant mieu, mais vu que RPi est linux, si vous avez au moins la solution pour « fermer la gueule » du Socket, ce sera déjà ça de gagné et testé.

#!/usr/bin/python3
import urllib.parse as urlparse
import http.server
import socketserver
import os
import subprocess
import configparser
import json
import datetime
import time
from threading import Timer
from urllib.parse import parse_qs
from os import path
from datetime import datetime as dt
from os import listdir
from os.path import isfile, join, isdir
from subprocess import Popen

def is_compiled():
    if getattr(sys, 'frozen', False):
        return True
    else:
        return hasattr(sys, '_MEIPASS')

print("Starting Web Server script")
if is_compiled():
    scriptpath=os.path.dirname(sys.executable)+"/"
else:
    scriptpath=os.path.dirname(os.path.realpath(__file__))+"/"
print(" * Loading configuration file: "+scriptpath+"config.ini")
config = configparser.ConfigParser()
config.read("config.ini", encoding='utf-8')
print("Setting web directory")
WEBPATH=scriptpath + config["web"]["path"]

def is_compiled():
    if getattr(sys, 'frozen', False):
        return True
    else:
        return hasattr(sys, '_MEIPASS')

if is_compiled():
    import wx
    
    class appMessage:
        msg=""
        def __init__(self, msg):
            self.msg=""
            
    class appDialog(wx.Dialog):
        def __init__(self, *args, **kwds):
            kwds["style"] = kwds.get("style", 0) | wx.CAPTION | wx.STAY_ON_TOP
            wx.Dialog.__init__(self, *args, **kwds)
            self.SetTitle("Zebra Label Printer Server - Message")
            sizer_1 = wx.BoxSizer(wx.VERTICAL)
            self.dialog_text = wx.StaticText(self, wx.ID_ANY, diamsg.msg)
            sizer_1.Add(self.dialog_text, 0, wx.ALL | wx.EXPAND, 10)
            sizer_2 = wx.StdDialogButtonSizer()
            sizer_1.Add(sizer_2, 0, wx.ALIGN_RIGHT | wx.ALL, 4)
            self.button_CLOSE = wx.Button(self, wx.ID_CLOSE, "")
            sizer_2.AddButton(self.button_CLOSE)
            sizer_2.Realize()
            self.SetSizer(sizer_1)
            sizer_1.Fit(self)
            self.SetEscapeId(self.button_CLOSE.GetId())
            self.Layout()
            self.Centre()
            
    def showMessage(txt):
        diamsg.msg=txt
        dlg = appDialog(None)
        dlg.ShowModal()
    
    app = wx.App(False)
    diamsg=appMessage(0)


class MyHttpRequestHandler(http.server.SimpleHTTPRequestHandler):
    update = False
    def do_GET(self):
        if self.path == '/':
            self.path = "/index.html"
        pathSplit = self.path.split("?")
        try:
            self.runPage(pathSplit[0],pathSplit[1])
        except:
            self.runPage(pathSplit[0])
            pass
    
    def do_POST(self):
        content_length = int(self.headers['Content-Length'])
        post_data = self.rfile.read(content_length)
        postDatas = post_data.decode("utf-8")
        if self.path == '/':
            self.path = "/index.html"
        pathSplit = self.path.split("?")
        try:
            self.runPage(pathSplit[0],pathSplit[1],postDatas)
        except:
            self.runPage(pathSplit[0],"",postDatas)
            pass

    def runPage(self,page,getDatas="",postDatas=""):
        pathSection = page.split("/")
        print("** URL: "+page)
        print("** _GET: "+getDatas)
        print("** _POST: "+postDatas)
        try:
            getDatas = urlparse.parse_qs(getDatas.replace('"',"''"))
        except:
            getDatas=""
            pass
        try:
            postDatas = urlparse.parse_qs(postDatas.replace('"',"''"))
        except:
            postDatas=""
            pass
       
        if path.exists(WEBPATH+page) is True:
            self.path = page
            try:
                f = open(WEBPATH+self.path, 'rb')
                ctype = self.guess_type(self.path)
                fs = os.fstat(f.fileno())
                self.send_response(200)
                self.send_header("Access-Control-Allow-Origin", "*")
                self.send_header("Content-type", ctype)
                self.send_header("Content-Length", str(fs[6]))
                self.send_header("Last-Modified",
                    self.date_time_string(fs.st_mtime))
                self.end_headers()            
                try:
                    self.copyfile(f, self.wfile)
                finally:
                    f.close()
                #return
                #return http.server.SimpleHTTPRequestHandler.do_GET(self)
            except OSError:
                self.send_response(404)
                self.send_header("Content-type", "text/html")
                self.end_headers()
                self.wfile.write(bytes('Document requested is not found.', "utf-8"))
                return None
            
        elif pathSection[1] == "api":
            outputJson={"result":"error","reason":"Invalid action"}
            values=""
            if pathSection[2] == "images":
                imgs=[]
                for x in os.listdir("images"):
                    if x.endswith(".jpg") or x.endswith(".png") or x.endswith(".ico"):
                        imgs.append(x)
                outputJson={"result":"success","reason":"Get images","images":imgs}
            elif pathSection[2] == "image":
                try:
                    f = open("images/"+pathSection[3], 'rb')
                    ctype = self.guess_type("images/"+pathSection[3])
                    fs = os.fstat(f.fileno())
                    self.send_response(200)
                    self.send_header("Access-Control-Allow-Origin", "*")
                    self.send_header("Content-type", ctype)
                    self.send_header("Content-Length", str(fs[6]))
                    self.send_header("Last-Modified",
                        self.date_time_string(fs.st_mtime))
                    self.end_headers()            
                    try:
                        self.copyfile(f, self.wfile)
                    finally:
                        f.close()
                    return
                except OSError:
                    self.send_response(404)
                    self.send_header("Content-type", "text/html")
                    self.end_headers()
                    self.wfile.write(bytes('Document requested is not found.', "utf-8"))
                    return None

            elif pathSection[2] == "templates":
                templates=[]
                for x in os.listdir("templates"):
                    if x.endswith(".zpt"):
                        templates.append(x)
                outputJson={"result":"success","reason":"Get templates","templates":templates}
            elif pathSection[2] == "template":
                result="{"
                template = configparser.ConfigParser()
                template.read("templates/" + pathSection[3] + ".zpt", encoding='utf-8')
                for s in template.sections():
                    result=result+'"'+str(s)+'":{'
                    for o,v in template.items(s):
                        result=result+'"'+str(o)+'":"'+str(v)+'",'
                    result=result+'},'
                result=result+"}"
                result=result.replace(",}","}")
                outputJson={"result":"success","reason":"Reading template.","datas":json.loads(result)}
            self.send_response(200)
            self.send_header("Access-Control-Allow-Origin", "*")
            self.send_header("Content-type", "application/json")
            self.end_headers()
            return self.wfile.write(bytes(json.dumps(outputJson), "utf-8"))
        elif pathSection[1] == "print":
            self.send_response(200)
            self.send_header("Access-Control-Allow-Origin", "*")
            self.send_header("Content-type", "application/json")
            self.end_headers()
            outputJson={"result":"error","reason":"Invalid action"}
            values=""
            try:
                if(int(postDatas['copie'])<=0):
                    copie=1
                else:
                    copie=int(postDatas['copie'])
            except:
                copie=1
            try:
                if pathSection[2] == "exemple":
                    if(postDatas.get('text') is not None):
                        values=values+' -a text -k "'+str(copie)+'" -t "'+str(postDatas['text'])+'"'
                        outputJson={"result":"success","reason":"Printing text label."}
                        self.goPrint(str(values))
                    else:
                        outputJson={"result":"error","reason":"Text is missing."}
                        
                elif pathSection[2] == "cli":
                    print("CLI")
                    if(postDatas.get('data') is not None):
                        params=str(postDatas['data']).replace('["',"").replace('"]',"").replace("''",'"')
                        outputJson={"result":"success","reason":"Send to printer script the parameters."}
                        self.goPrint(" "+params)
                    else:
                        print("NO PARAMS")
                        outputJson={"result":"error","reason":"No parameter to send to printer script."}
            except:
                outputJson={"result":"error","reason":"Error when processing the request."}
                pass
                    
            return self.wfile.write(bytes(json.dumps(outputJson), "utf-8"))
        
        elif pathSection[1] == "manage":
            print(" * Loading configuration file: "+scriptpath+"config.ini")
            config = configparser.ConfigParser()
            config.read(scriptpath+"config.ini", encoding='utf-8')
            outputJson={"result":"error","reason":"Invalid action"}
            if(postDatas.get('password') is not None):
                if(str(postDatas.get('password')).replace("['","").replace("']","")==str(config['web']['adminpass'])):
                    try:
                        if pathSection[2] == "config":
                            if pathSection[3] == "load":
                                result="{"
                                readconfig = configparser.ConfigParser()
                                readconfig.read('config.ini', encoding='utf-8')
                                for s in readconfig.sections():
                                    result=result+'"'+str(s)+'":{'
                                    for o,v in readconfig.items(s):
                                        if(str(o)!="adminpass"):
                                            result=result+'"'+str(o)+'":"'+str(v)+'",'
                                    result=result+'},'
                                result=result+"}"
                                result=result.replace(",}","}")
                                outputJson={"result":"success","reason":"Reading configuration.","datas":json.loads(result)}
                            elif pathSection[3] == "save":
                                saveconfig = configparser.ConfigParser()
                                saveconfig.read('config.ini', encoding='utf-8')
                                if(postDatas.get('printername') is not None):
                                    saveconfig['printer']['printername']=str(postDatas['printername']).replace("['","").replace("']","")
                                with open('config.ini', 'w', encoding='utf-8') as configfile:    # save
                                    saveconfig.write(configfile)
                                result="{"
                                saveconfig = configparser.ConfigParser()
                                saveconfig.read('config.ini', encoding='utf-8')
                                for s in saveconfig.sections():
                                    result=result+'"'+str(s)+'":{'
                                    for o,v in saveconfig.items(s):
                                        if(str(o)!="adminpass"):
                                            result=result+'"'+str(o)+'":"'+str(v)+'",'
                                    result=result+'},'
                                result=result+"}"
                                result=result.replace(",}","}")
                                outputJson={"result":"success","reason":"Saving configuration.","datas":json.loads(result)}                                    
                            else:
                                print("Error in config")
                                outputJson={"result":"error","reason":"This action is not valid."}
                        else:
                           outputJson={"result":"error","reason":"Request is not valid."} 
                    except Exception as e:
                        print(f"{type(e).__name__} at line {e.__traceback__.tb_lineno} of {__file__}: {e}")
                        outputJson={"result":"error","reason":"Error when processing the request."}
                        pass
                else:    
                    outputJson={"result":"error","reason":"Password is incorrect."}
            else:
                outputJson={"result":"error","reason":"No password or not in POST method."}
            self.send_response(200)
            self.send_header("Content-type", "application/json")
            self.end_headers()                
            return self.wfile.write(bytes(json.dumps(outputJson), "utf-8"))
        else:
            self.send_response(404)
            self.send_header("Content-type", "text/html")
            self.end_headers()
            self.wfile.write(bytes('Document requested is not found.', "utf-8"))
        return
    def goPrint(self,values):
        strclean=str(values).replace("['","").replace("']","")
        print("Send to printer : "+strclean)
        try:
            os.system('./zp.exe '+strclean)
            #os.system('python ./zp.py '+strclean)
        except:
            if is_compiled():
                showMessage("Can't run the print command.")
            else: 
                print("Can't run the print command.")
 
def start():
    zpWeb = MyHttpRequestHandler
    zpWebserver = socketserver.TCPServer(("0.0.0.0", int(config['web']['port'])), zpWeb)
    if is_compiled():
        showMessage("Starting Web Server at port "+str(config['web']['port'])+".\n\nWeb path : "+WEBPATH)
    else:    
        print("*** RUNNING WEB SERVER AT PORT "+str(config['web']['port'])+" ***")
    zpWebserver.serve_forever()

start()

Bonjour,

Je n’ai pas les compétences pour dépanner ton scritp, il y a quand même un détail qui me surprend dans les imports :

import urllib.parse as urlparse
from urllib.parse import parse_qs

Je ne pense pas que cela soit bon.

Sinon, sais-tu où vont les « print » quand aucune console n’est lancée ?
Si j’étais confronté à ce genre de problème, j’utiliserais un fichier de log
avec la bibliothèque logging Logging HOWTO — Python 3.12.3 documentation j’ai eu une fois l’occasion de la configurer, c’est un peu lourd à mettre en place (par rapport à print), mais ça fait bien le job.

Une fois le logging mis en place, mettre en place un max de messages de DEBUG. Lancer l’appli en mode DEBUG et voir si c’est bien du coté des websocket que se pose le problème.

Bon debugging.

A+

J’ai mis des « print » pour mon debug en mode script, ceux si ne sont pas important en mode production. L’idée est d’avoir le service en Background pour un usage en entreprise.

Alors tout ce qui est « logging » est inutile et je désire les retirer, les « print » je peux les retirer pour compiler. mais les log que le module Socket génère, je n’ai pas de contrôle, et compiler sans la console semble le « déranger » et ne marche pas. Seul moyen actuel est de cacher la console via un hack avec un script VBS que j’ai, c’est actuellement comme ça que je le test.

J’ai demandé l’aide de ChatGPT et j’ai tenter de contrôler les logs, tout les print et autres logs sont gérable via l’implantation du code de log, mais n’affecte pas celui que le module Socket génère, continue de passer en console.

Je pointais tout sur un fichier externe, tous les print ont été mis en fichier, mais pas les sorties du module Socket.


Comme j’ai dis dans mon poste, le code n’est pas optimisé, et peut inclure des import qui ne sont plus essentiel au projet. Mais le poste ici s’attarde plus au problème des output du module Socket, le reste je m’y attarderais plus tard. Car oui, je vais réviser le code pour retirer l’inutile, pour aider le tout à bien se charger sans.

Bonjour,

J’ai regardé vite fait la page socketserver sur le net. (socketserver — A framework for network servers — Python 3.12.3 documentation) c’est vrai que c’est assez léger comme documentation. Je ne vois effectivement pas de gestion de log.
Du coup il me semble que toutes les logs peuvent être générée par le développeur dans la classe MyHttpRequestHandler.
D’ailleurs dans le code on voit que la commande print est appelée comme ici

Ces prints sont-il visibles dans une log lors des appels ?

Sinon autre approches:

  • existe-il une autre bibliothèque qui ferait le job ?
  • chatgpt ne peut-il pas aider à « écrire » un module serveur à parti du module socket" ?

A+

ChatGPT n’arrive pas a produire de quoi qui marche dans ce cas la. Avant de poster sur le forum, je lui demande souvent de m’aider.

** URL: /index.html
** _GET:
** _POST:
127.0.0.1 - - [23/May/2024 08:38:11] "GET / HTTP/1.1" 200 -

La ligne qui me dérange est celle commençant par le 127.0.0.1, comme tu peux voir dans mon code, j’ai rien qui génère ça. Quand je demande à ChatGPT, il focus sur mes commandes print() ou l’ajout d’un module de gestion des logs.

ChatGPT est atteint de TDAH par moment. Il focus sur les mauvaise choses, et surcommente les réponses qu’il donne.

J’imagine que d’autre lib existe, car ChatGPT me sortait des codes que je n’avais pas en lien avec un HTTP.server (de quoi du genre). Mais je n’ai pas le goût de réécrire la gestion des réponses, et les réponses était partiel.

Bonjour,

Loin d’être un spécialiste, je m’excuse d’avance si je te met sur la mauvaise piste. :confounded:
Mais il me semble que cette ligne

est plutôt générée par httpServer. Je suppose qu’elle est formatée ici

                self.send_response(200)

Dans la doc de htt.server (http.server — HTTP servers — Python 3.12.3 documentation) il y a ceci

send_response(code, message=None )

Adds a response header to the headers buffer and logs the accepted request.

A+

Merci, au moins ceci explique cela. Mais maintenant, a t-il un moyen de le retirer ce log ?
A mon retour du travail j’irais voir la doc à ce sujet.