PROJET AUTOBLOG


Sam & Max: Python, Django, Git et du cul

Site original : Sam & Max: Python, Django, Git et du cul

⇐ retour index

Mise à jour

Mise à jour de la base de données, veuillez patienter...

Files de tâches et tâches récurrentes avec Celery

samedi 27 juillet 2013 à 09:57

Quand on a à traiter des choses bloquantes, avec des dépendances, des flux complexes ou des actions répétitives, créer des files d’attente peut se révéler très judicieux.

Par exemple lancer la génération d’un gros zip sur le clic d’un utilisateur, télécharger plein fichiers en parallèle pour son site de cul, lancer des calculs sur plusieurs machines et récupérer le résultat, encoder des videos en arrière plan, etc.

Le problème, c’est que fabriquer des files d’attente à la main, ça mène généralement à une grosse galère. La première boîte dans laquelle j’ai travaillé avait tout un système de queues à base de PHP + SQL fait à la main qui tapait dans du MySQL, c’était pas marrant du tout

Je fais une pause, et je note que le potentiel de jeux de mots sur cet article est fortement élevé. Mais je resterai fort.

Locking, priorité, dépendance, asynchronicité, concurrence, sérialisation, encoding, stockage, accessibilité, load balancing… Toutes ces problématiques sont bien vicieuses et chronophages. Il vaut mieux utiliser une lib solide et éprouvée.

Je resterai fort.

Kombu est une telle lib, mais elle est lourde et complexe à utiliser. J’avais fais le choix de la prendre pour un gros projet avec Max, je le regrette sur le long terme : c’est dur à maintenir et à faire évoluer. Le code est vraiment pas sympa.

Heureusement il existe une bibliothèque qui se met au dessus de Kombu pour et nous expose juste les fonctionnalités que l’on souhaite : celery.

Fort.

Celery est simple pour démarrer, mais très puissant si on rentre dans le détail, et croyez moi, le détail, on peut y rentrer très très profondément.

F…

Installation

Qui dit file d’attente, dit stockage. Il faut bien mettre les tâches quelque part et communiquer avec ce quelque part. En base de données ? Dans un gestionnaire de messages ? En mémoire ? Dans un cache ?

Celery résout le problème en proposant la même interface, quelque soit le support. Actuellement, on peut utiliser :

Dans notre exemple, nous allons le faire avec Redis car :

Pour ceux qui ont pas redis, c’est généralement dans les dépôts. Par exemple sur Ubuntu :

sudo apt-get install redis-server

Il n’y a rien à faire de plus, ça tourne, c’est configuré avec des valeurs par défaut qui sont saines. Je vous l’ai dis, redis, c’est fantastiquement bien foutu.

Ensuite on install celery et la lib d’accès à redis en Python qui porte un nom très original :

pip install celery redis

Ca devrait compiler un peu, et comme hab avec les extensions en C, assurez vous d’avoir un compilateur et les headers en place comme indiqué dans l’article sur pip.

Ensuite on peut créer ses tâches. Créez un module, par exemple tasks.py :

import urllib2
 
from collections import Counter
 
from celery import Celery
 
# Configuration de celery. Ceci peut aussi se faire dans un fichier de config.
# Ici on dit à celery que pour le module 'tasks', on va utiliser redis
# comme broker (passeur de massage) et comme result backend (stockage du
# resultat des tâches).
celery = Celery('tasks', broker='redis://localhost', backend='redis://localhost')
 
 
# Et voici notre première tâche. C'est une fonction Python normale, décorée
# avec un decorateur de celery. Elle prend une URL, et calcule le nombre
# de lettre "e" qu'il y a dans la page.
@celery.task
def ecount(url):
    return Counter(urllib2.urlopen(url).read())['e']

On lance ensuite le processus celery dans un terminal (en production, mettez ça dans supervisord ou systemd pour que ça démarre automatiquement) :

[test] sam ~/Bureau/celery_test $ celery -A tasks worker -B --loglevel=info

 -------------- celery@sam v3.0.21 (Chiastic Slide)
---- **** -----
--- * ***  * -- Linux-3.2.0-48-generic-x86_64-with-Ubuntu-12.04-precise
-- * - **** ---
- ** ---------- [config]
- ** ---------- .> broker:      redis://localhost:6379//
- ** ---------- .> app:         tasks:0x2a2fa50
- ** ---------- .> concurrency: 4 (processes)
- *** --- * --- .> events:      OFF (enable -E to monitor this worker)
-- ******* ----
--- ***** ----- [queues]
 -------------- .> celery:      exchange:celery(direct) binding:celery

[Tasks]
  . tasks.ecount

[2013-07-26 13:22:21,631: INFO/Beat] Celerybeat: Starting..
[2013-07-26 13:04:51,274: WARNING/MainProcess] celery@sam ready.
[2013-07-26 13:04:51,280: INFO/MainProcess] consumer: Connected to redis://localhost:6379//.

-A précise le module à importer, -B démarre le beat (on verra ça plus tard), worker dit à celery que démarrer des processus de consommation de files d’attente (par défaut 4 qui travaillent en parallèle), et --loglevel=info va nous permettre d’avoir un affichage verbeux pour comprendre ce qui se passe.

Votre file d’attente est prête, et frétille d’impatience.

Lancer une tâche

A partir de là, vous pouvez envoyer des tâches dans la file d’attente, depuis n’importe où :

Plusieurs programmes peuvent envoyer plein de tâches, en même temps, et elles vont se loger dans la file d’attente, sans bloquer le programme qui les a envoyé.

Par exemple, depuis le shell :

>>> from tasks import ecount
>>> res = ecount.delay('http://danstonchat.com')

Ceci ne bloque pas mon shell, la ligne s’exécute immédiatement. La fonction ecount n’est pas appelée depuis le shell, elle est dans la file d’attente et sera appelée par un des processus (les fameux ‘worker’) qui consomment la queue. Du côté de la file, on peut voir dans le log :

[2013-07-26 14:18:08,609: INFO/MainProcess] Got task from broker: tasks.ecount[599a52ea-ef6b-4499-981d-cd17fab592df]
[2013-07-26 14:18:09,070: INFO/MainProcess] Task tasks.ecount[599a52ea-ef6b-4499-981d-cd17fab592df] succeeded in 0.446974039078s: 1242

On a donc notre tâche qui a bien été traitée.

On peut récupérer le résultat dans le shell :

>>> res.state
'PENDING'

Ah… La tâche n’est pas encore terminée. Et un peu plus tard :

>>> res.state
'SUCCESS'
>>> res.result
1242

Lancer une tâche est bien entendu peu intéressant, les listes d’attente sont vraiment sympa quand on a plein de tâches à lancer, par plein de processus différents :

results = [ecount.delay(url) for url in ('http://google.com', 'http://sametmax.com', 'http://sebsauvage.com', 'http://multiboards.com', 'http://0bin.net', 'http://danstonchat.com')]
[2013-07-26 14:25:46,646: INFO/MainProcess] Got task from broker: tasks.ecount[5d072a7b-29f8-4ea6-8d92-6a4c1740d724]
[2013-07-26 14:25:46,649: INFO/MainProcess] Got task from broker: tasks.ecount[402f6a4f-6b35-4f62-a786-9a5ba27707d2]
[2013-07-26 14:25:46,650: INFO/MainProcess] Got task from broker: tasks.ecount[bbe46b1b-4719-4c42-bd2f-21e4d72e613e]
[2013-07-26 14:25:46,652: INFO/MainProcess] Got task from broker: tasks.ecount[8fb35186-66e2-4eae-a40c-fc42e500ab9d]
[2013-07-26 14:25:46,653: INFO/MainProcess] Got task from broker: tasks.ecount[fc63f5db-8ade-4383-b719-c3d6390ca246]
[2013-07-26 14:25:46,654: INFO/MainProcess] Got task from broker: tasks.ecount[8434e21d-79ea-4559-a90e-92e2bc2b9dc7]
[2013-07-26 14:25:47,144: INFO/MainProcess] Task tasks.ecount[bbe46b1b-4719-4c42-bd2f-21e4d72e613e] succeeded in 0.479865789413s: 27
[2013-07-26 14:25:47,242: INFO/MainProcess] Task tasks.ecount[5d072a7b-29f8-4ea6-8d92-6a4c1740d724] succeeded in 0.578661203384s: 609
[2013-07-26 14:25:47,501: INFO/MainProcess] Task tasks.ecount[fc63f5db-8ade-4383-b719-c3d6390ca246] succeeded in 0.35736989975s: 263
[2013-07-26 14:25:47,645: INFO/MainProcess] Task tasks.ecount[8434e21d-79ea-4559-a90e-92e2bc2b9dc7] succeeded in 0.403187036514s: 1270
[2013-07-26 14:25:47,815: INFO/MainProcess] Task tasks.ecount[8fb35186-66e2-4eae-a40c-fc42e500ab9d] succeeded in 1.14100408554s: 23
[2013-07-26 14:25:49,010: INFO/MainProcess] Task tasks.ecount[402f6a4f-6b35-4f62-a786-9a5ba27707d2] succeeded in 2.34633708s: 3158

Car du coup on sait que ces multiples tâches ne vont pas bloquer le processus en cour, mais qu’en plus la charge sera répartie sur le nombre de workers qu’on a décidé au départ, ni plus (surcharge du serveur), ni moins (traitement trop lent).

Comment je sais quand une tâche est terminée ?

On peut attendre que la tâche soit terminée :

>>> print res.wait()
9999

Mais ce n’est pas vraiment le but. On cherche avant tout à ce que les tâches soient non bloquantes, et exécutées dans un processus à part voir potentiellement distribuées sur plusieurs serveurs.

Par ailleurs, Celery n’est pas un remplacement d’un système de traitement asynchrone comme Tornado ou NodeJS, il n’est pas fait pour envoyer des réponses asynchrones à l’utilisateur. Il est fait pour faire des tâches en background, répartir la charge et ordonner le traitement. Bien entendu, on peut faire communiquer un système asynchrone avec celery comme ici ou ici, mais c’est une autre histoire.

Concentrons nous sur les tâches.

La question de “Comment je sais quand une tâche est terminée ?” est souvent traduisible par “comment je réagis à une tâche pour lancer du code quand elle s’est terminée sans erreur ?”.

Et là, il y une solution toute simple :

res = tache1.s(arg1, arg2) | tache2.s() | tache3.s(arg1)

Ceci va créer une chaîne de tâches. Quand la première se termine, la deuxième se lance en recevant le résultat de la première en argument.

s() fabrique une sous-tâche, c’est à dire une tâche à envoyer dans la file plus tard avec des arguments pré-enregistrés. Dans notre exemple, celery va lancer tache1 avec deux arguments, puis si ça marche, va appeler tache2 en lui passant le résultat de tache1 comme argument, puis si ça marche, va appeler tache3 avec le résultat de tache2 en premier argument et arg1 en second argument.

En fait, celery vient avec tout un tas d’outils pour exécuter des tâches dépendantes les unes des autres : par groupes, par chaînes, par morceaux, etc. Mais de toute façon, vous pouvez appeler une tâche… à l’intérieur d’une autre tâche. Donc parti de là vous pouvez faire pas mal de choses.

Comment je fais pour faire une tâche récurrente ?

C’est là qu’intervient le “beat” dont j’ai parlé tout à l’heure. Avec cette option, celery va vérifier toutes les secondes si il n’y a pas une tâche répétitive à lancer, et la mettre dans une file d’attente, à la manière d’un cron.

Il suffit de définir une tâche comme periodic_task pour qu’elle soit lancée régulièrement.

import smtplib
 
from celery.schedules import crontab
from celery.decorators import periodic_task
 
# va executer la tâche à 5h30, 13h30 et 23h30 tous les lundi
# run_every accepte aussi un timedelta, pour par exemple dire "toutes les 10m"
@periodic_task(run_every=crontab(hour='5,13,23', minute=30, day_of_week='monday')
def is_alive():
    """
        Vérifie que le blog est toujours en ligne, et si ce n'est pas le cas,
        envoie un mail en panique.
    """
    if urllib2.urlopen('http://sametmax.com').code != 200:
        mail = 'lesametlemax__AT__gmail.com'.replace('__AT__', '@')
        server = smtplib.SMTP('smtp.gmail.com:587')
        server.starttls()
        server.login('root', 'admin123')
        server.sendmail(mail, mail, msg)
        server.quit()

Il y a de bons exemples sur la syntaxe sur crontab() dans la doc.

D’une manière générale, la doc de Celery est très très riche, donc plongez vous dedans si cet article ne répond pas à vos besoins, car si ça peut être mis dans une file, ça peut être fait par Celery.

Note de fin

Celery n’autoreload pas le code, donc redémarrez les workers à chaque fois que vous modifiez vos tasks.

Attention aussi aux tâches récurrentes, la suivante peut se lancer avant que la précédente soit terminée. C’est à vous de faire des tâches idempotentes, ou alors de mettre en place un système de locking.

flattr this!

Ouverture du coffre : la méthode simple, bourine, mais efficace

vendredi 26 juillet 2013 à 12:16

Ceci est un post invité de MrAaaah posté sous licence creative common 3.0 unported.

Salut à toutes et à tous c’est MrAaaah, je suis celui qui réussi à résoudre le plus rapidement les énigmes du coffre secret de Sam et Max, on ma demandé de vous faire un "petit" article expliquant un peu ma démarche et mes méthodes.

Avant-propos

Avant de me lancer dans la résolution des énigmes je tiens à signaler que je n’ai qu’un an de python dans les pattes, de ce fait le code peut paraitre cradoc et peut-être un poil bourrin vu que je ne connais pas encore très bien python, ses librairies et ses subtilités (ça a également été codé très vite). Je n’ai pas retouché le fonctionnement de mes scripts, j’ai juste renommé quelques variables et mis des commentaires, donc c’est du brut, y’a pas de vérif, ça plante à la fin, etc.

Y U NO OPEN?

Un petit meme : "Code Y U NO WORK?"

Première énigme http://game.sametmax.com/ : un message, un champ d’entrée et une image. Comme beaucoup je suppose, je commence par rentrer diverses conneries dans le formulaire, toujours le même résultat : le message “I don’t GET it.”.

Je parcours le code source de la page à la recherche d’un éventuel script où autre indice dans le code, nada.

Si on regarde l’url on peut voir que le code est envoyé via la méthode GET : http://game.sametmax.com/?code=monnezsurtescouilles. En relisant le message de ci-dessus on peut deviner qu’il faut balancer une requête de type POST. Pour faire ça rien de plus simple, on sort Firebug (ou équivalent), on édite le code source en changeant l’attribut method du formulaire et on renvoi avec un code bidon.

Modification de la methode d'envoi d'un formulaire avec Firebug.

Là on obtient un nouveau message : “Error log : areyouhuman”. Bon je retente des codes bidon (genre “yes”, etc.). Je cherche sur le net “areyouhuman”, mais rien de concluant.

Au final rien de bien complexe, mais y’a moyen de tourner un peu en rond, il suffit «juste» d’allez sur l’URL http://game.sametmax.com/areyouhuman

areyouhuman

À partir de là des connaissances en Python (basiques) vont être nécessaires. (j’utilise ici Python 2.7)

La page nous renvoie ce qui ressemble à une définition de classe NextUrlContainer avec un attribut next suivi d’une bouillie de caractères en commentaire.

NextUrlContainer = type("NextUrlContainer", (), {'__init__': (lambda s, n: setattr(s, 'next', n))}) # Y2NvcHlfcmVnCl9yZWNvbnN0cnVjdG9yCnAwCihjX19tYWluX18KTmV4dFVybENvbnRhaW5lcgpw MQpjX19idWlsdGluX18Kb2JqZWN0CnAyCk50cDMKUnA0CihkcDUKUyduZXh0JwpwNgpJMjA5NzM1 MQpzYi4=

Avec un peu de recherche on découvre que la soi-disant bouillie n’est autre que du texte encodé en base64. Une fois décodé (il y a des utilitaires en ligne qui font ça très bien), on obtient quelque chose qui nous parle un peu plus (quoique).

ccopy_reg
_reconstructor
p0
(c__main__
NextUrlContainer
p1
c__builtin__
object
p2
Ntp3
Rp4
(dp5
S'next'
p6
I2097351
sb.

Encore un peu de recherche et je découvre que c’est un fameux pickle, on sort notre Python préféré.

# -*-coding:Utf-8 -*
import base64, pickle
 
NextUrlContainer = type("NextUrlContainer", (), {'__init__': (lambda s, n: setattr(s, 'next', n))})
 
b64 = "Y2NvcHlfcmVnCl9yZWNvbnN0cnVjdG9yCnAwCihjX19tYWluX18KTmV4dFVybENvbnRhaW5lcgpw MQpjX19idWlsdGluX18Kb2JqZWN0CnAyCk50cDMKUnA0CihkcDUKUyduZXh0JwpwNgpJMjA5NzM1 MQpzYi4="
 
# on décode et on dépickle-ise
obj = pickle.loads(base64.urlsafe_b64decode(b64))
 
print obj.next

Et on obtient un objet de type NextUrlContainer contenant l’id de la prochaine URL : 2097351.
En ce rendant sur http://game.sametmax.com/areyouhuman/2097351 on se retrouve avec de nouveau un pickle encodé en base64. Après en avoir fait deux URL à la main, on ressort Python pour automatiser tout ça.

Notre script doit :

Voilà le script utilisé. Il affiche chaque nouvel id et plante quand on arrive au bout.

# -*-coding:Utf-8 -*
import httplib
import base64, pickle
 
# la classe donnée sur la page http://game.sametmax.com/areyouhuman
NextUrlContainer = type("NextUrlContainer", (), {'__init__': (lambda s, n: setattr(s, 'next', n))})
 
# initialisation de la connection
connection = httplib.HTTPConnection("game.sametmax.com:80")
 
# Fait une requête sur l'URL http://game.sametmax.com/areyouhuman/:id
# Et retourne le contenu de la page
def make_request(id):
    url = "/areyouhuman/%i" % id
    connection.request("GET", url)
    response = connection.getresponse()
    return response.read()
 
 
# id de départ
id = 2097351
 
# tant que ça plante pas ça suit les liens
while 1:
    # récupération du pickle en base64 contenant l'id de la prochain URL
    response_crypt = make_request(id)
 
    # on décode et on dépickle-ise
    obj = pickle.loads(base64.urlsafe_b64decode(response_crypt))
 
    # on remplace l'id courant par le prochain afin de pouvoir recommencer
    id = obj.next
 
    print id

On le lance et ça tourne un petit bout de temps avant d’en arriver au bout. (le serveur doit adorer)

    3245669
    2993679
    1050294
    .......
    9683898
    8147905
    9664723
    wololo.zip
    Traceback (most recent call last):
      File "1_requests.py", line 27, in 
        response_crypt = make_request(id)
      File "1_requests.py", line 14, in make_request
        URL = "/areyouhuman/%i" % id
    TypeError: %d format: a number is required, not str

On a donc notre prochaine destination : http://game.sametmax.com/wololo.zip

Wololo

Petit strip humoristique en réference à AOE et ses prètres wololoteur

Pour pas me prendre la tête j’ai télécharger le .zip dans le même répertoire que mes scripts.

On commence par dézipper wololo.zip, on obtient one_more_time_1.zip que l’on dézippe, on obtient one_more_time_2.zip que l’on dézippe, on obtient one_more_time_3.zip que l’on dézippe, etc.

Bon par besoin de chercher très loin pour savoir quoi faire, l’algo est simple :

Ce qui se traduit en python par :

# -*-coding:Utf-8 -*
import zipfile
 
# archive de départ
archive_name = "wololo.zip"
 
# tant qu’il y a quelque chose à dézipper, ça tourne
while 1:
    # ouverture de l'archive
    archive = zipfile.ZipFile(archive_name, 'r')
 
    # récupération du nom du fichier contenu dans l'archive
    file_to_extract = archive.namelist()[0]
 
    # extraction
    archive.extract(file_to_extract)
 
    print file_to_extract
 
    # le fichier tout fraichement extrait sera le prochain à être dézippé
    # (ça plantera quand y'aura plus de .zip)
    archive_name = file_to_extract

On exécute

one_more_time_1.zip
one_more_time_2.zip
one_more_time_3.zip
...................
one_more_time_998.zip
one_more_time_999.zip
one_more_time_1000.zip
youdiditjonhy.txt
Traceback (most recent call last):
  File "2_zips.py", line 10, in 
    archive = zipfile.ZipFile(archive_name, 'r')
  File "/System/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/zipfile.py", line 712, in __init__
    self._GetContents()
  File "/System/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/zipfile.py", line 746, in _GetContents
    self._RealGetContents()
  File "/System/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/zipfile.py", line 761, in _RealGetContents
    raise BadZipfile, "File is not a zip file"
zipfile.BadZipfile: File is not a zip file

On se retrouve donc avec un nouveau petit fichier youdiditjonhy.txt contenant le texte

api.json

ainsi que 1000 fichiers zip … un petit rm one_more_time* dans son terminal (sur Windows débrouillez-vous) pour cleaner tout ça et on repart pour l’énigme suivante.

api.json

On se rend donc sur http://game.sametmax.com/api.json

Et on se prend ça dans la tronche :

{"\f": ["."], " ": ["."], "$": [".", "."], "(": [".", ".", "."], ",": [".", "."], "0": [".", "."], "4": ["."], "8": [".", ".", "."], "<": ["."], "@": [".", ".", "."], "D": [".", ".", "."], "H": [".", "."], "L": [".", "."], "P": ["."], "T": [".", "."], "X": [".", ".", "."], "\\": [".", "."], "`": ["."], "d": ["."], "h": [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", "."], "l": [".", ".", "."], "p": [".", ".", "."], "t": [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", "."], "x": [".", ".", "."], "|": [".", ".", "."], "\u000b": [".", ".", "."], "#": [".", ".", "."], "'": [".", ".", "."], "+": [".", "."], "/": [".", ".", "."], "3": [".", "."], "7": [".", ".", "."], ";": [".", "."], "?": ["."], "C": [".", ".", "."], "G": ["."], "K": [".", "."], "O": ["."], "S": [".", ".", "."], "W": ["."], "[": ["."], "_": [".", "."], "c": [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", "."], "g": [".", ".", ".", ".", ".", ".", ".", ".", ".", "."], "k": ["."], "o": [".", ".", "."], "s": [".", "."], "w": ["."], "{": [".", "."], "\n": [".", "."], "\"": ["."], "&": [".", "."], "*": ["."], ".": [".", ".", "."], "2": ["."], "6": [".", "."], ":": ["."], ">": [".", "."], "B": ["."], "F": [".", "."], "J": ["."], "N": ["."], "R": [".", "."], "V": [".", "."], "Z": [".", ".", "."], "^": [".", "."], "b": [".", ".", "."], "f": [".", ".", "."], "j": [".", "."], "n": [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", "."], "r": [".", "."], "v": [".", "."], "z": [".", ".", "."], "~": [".", "."], "\t": ["."], "\r": [".", ".", "."], "!": ["."], "%": [".", "."], ")": ["."], "-": [".", "."], "1": [".", ".", "."], "5": [".", "."], "9": [".", ".", "."], "=": [".", "."], "A": ["."], "E": [".", ".", "."], "I": [".", "."], "M": ["."], "Q": [".", ".", "."], "U": ["."], "Y": [".", ".", "."], "]": ["."], "a": [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", "."], "e": [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", "."], "i": [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", "."], "m": [".", ".", "."], "q": [".", "."], "u": [".", "."], "y": [".", ".", "."], "}": [".", ".", "."]}
Sir Patrick Stewart jurant à la vu de ce fichier json

Même Sir Patrick Stewart semble contrarié par ce fichier !

Bon déjà on sait que c’est du JSON, on se fait un nouveau script pour voir ce que donne ce json en Python :

# -*-coding:Utf-8 -*
import httplib
import json
 
# on récupère le json
connection = httplib.HTTPConnection("game.sametmax.com:80")
url = "/api.json"
connection.request("GET", url)
response = connection.getresponse()
 
# on transorme en python
decoded = json.loads(response.read())
 
print decode

Bon c’est pas beaucoup mieux, on à affaire à un dictionnaire avec pour clé un caractère Unicode avec une liste de ‘.’ plus où moins longue associé. En mettant un peu en forme ça donne un truc du genre :

Y :  . . .
Z :  . . .
[ :  .
\ :  . .
] :  .
^ :  . .
_ :  . .
` :  .
a :  . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
b :  . . .

Soit pas grand-chose d’exploitable, je modifie le script pour obtenir le nombre de points associé à chaque caractère :

# -*-coding:Utf-8 -*
import httplib
import json
 
# on récupère le json
connection = httplib.HTTPConnection("game.sametmax.com:80")
url = "/api.json"
connection.request("GET", url)
response = connection.getresponse()
 
# on met ça en python
decoded = json.loads(response.read())
 
# on passe le dico en liste pour pouvoir le trier
items = decoded.items()
items.sort()
 
for i in items:
    print "%s : %s" % (i[0], len(i[1]))
Z : 3
[ : 1
\ : 2
] : 1
^ : 2
_ : 2
` : 1
a : 50
b : 3

À partir de là je tente de convertir des mots en remplaçant les lettres par le nombre de ‘.’ associé, par exemple “Max” => 1503. J’arrête vite mes conneries et me rend compte qu’une grosse partie des caractères n’est associé qu’a 1,2 ou 3 points et quelques autres sont à 50, 30, 60, étrange…

Je garde juste les plus grosses valeurs, les tris dans l’ordre croissant, soit : 10 20 … 80. En reprenant chaque clé associée, on obtient le mot gnitaehc, soit cheating à l’envers. Peu de chance que ce soit là par hasard.

On se rend sur http://game.sametmax.com/cheating, on arrive à la fin.

↑↑↓↓←→←→BA

Retour sur la page d’accueil avec cette fois-ci le Konami code en message (↑↑↓↓←→←→BA). Pas de temps à perdre, on va sur http://game.sametmax.com/konami.

Achievement unlocked

Et c’est là que ce termine la série d’énigmes ! Au final la difficulté était plutôt bien dosée, pas trop hard pour un débutant tout en offrant du challenge et du cassage de tête.

J’espère avoir été clair dans mes explications, si y’a des questions sur certains point allez-y, même si ce n’est pas moi je pense qu’il y’aura toujours quelqu’un pour vous éclairer.

Si vous avez utilisé d’autres techniques ou s’il y a des remarques sur mon code, allez-y. Dans les commentaires de l’article original, il y’a une solution “collaborative” similaire à la mienne (bon leurs codes est quand même un peu plus propre), y’a également Recher qui nous propose d’utiliser un éditeur hexadécimal pour l’énigme du zip.

Je suis assez amateur de ce genre de puzzle/challenge, si ça interesse des gens il y a quelques sites bien sympa pour se prendre la tête :

Sur ce, bravo à ceux qui ont tenté le jeu, bon voyage à Max et encore merci à nos deux taulier !

flattr this!

Code source du jeu

jeudi 25 juillet 2013 à 14:09

Mraaaah vient d’avoir un compte sur le blog, et peut être aura-t-il le temps de publier les sources de ses solutions. En attendant, voici le code source du jeu.

En général on documente un peu le truc, on met un README, des commentaires, etc. Mais là je vais invoquer le fait que je veux vous montrer ce que ça donne un code à la vite pour justifier ma grosse flemme. Ce code source est intéressant car il montre plusieurs choses :

Vous trouverez dans le répertoire scripts les soluces des énigmes que j’ai utilisé pour vérifier que ça marchait. Vous verrez que là aussi, c’est du vite fait, avec parfois des trucs qui plantent tout simplement par fainéantise.

Pour démarrer le jeu : python site.py avec du 2.7.


Mraaaah a publié ses solutions.

flattr this!

Colis envoyé !

mercredi 24 juillet 2013 à 21:14

Le colis pour le gagnant du jeu est parti.

Il a écrit un article pour montrer ses codes qu’il faudra publier, et moi je posterai les sources du jeu dès que j’ai plus de temps.

A pluche.

flattr this!

Le coffre a été ouvert !

mardi 23 juillet 2013 à 10:08

J’avais dosé une difficulté sans trop savoir jusqu’où aller car le niveau de prog sur le blog est plutôt disparate, et afin de laisser sa chance à tout le monde. Mais ce mail me fait assez plaisir quant au résultat.

Le gagnant a ouvert le coffre et nous a contacté à 2 heures du math :

Salut les gars !

Bon visiblement j’ai réussi à pénétrer le petit coffre rouge en premier [censored].

Alors tout d’abord un gros merci pour le taff que vous faites sur votre blog. Je vous ai découvert y’a un peu plus d’un an en cherchant des infos sur Django et Python que je ne connaissait pas du tout à l’époque. Et depuis je ne vous lâche plus et je me régale de vos articles (bien que je fasse parti de la masse discrète en ne commentant jamais …). Un an plus tard me voilà à appliquer des techniques apprises (en partie) sur votre blog (json, pickle, etc.).

Première fois que je résout un challenge de ce genre, c’était bien sympathique (un peu prise de tête par moment mais quel bonheur à chaque pas réaliser), bravo pour la réalisation en tout cas !

On voit les acharnés !

Comme pour le cadeau de la dernière fois, on va lui demander d’envoyer une petite photo de ce qui l’a reçu et on la postera pour que vous découvriez tous ensemble ce qu’il y avait a gagner. Ça restera donc une surprise jusqu’au déballage (mais moi je suis jaloux parce que j’en voulais un).

Si notre gagnant à le courage, ce serait marrant qu’il fasse un article pour expliquer comment il a passé les énigmes, et poster son code. Tu nous le dis en comment si tu es chaud ?

Dans tous les cas je laisse le jeu en ligne quelques jours pour le fun, et on le retirera, puis on mettera le code source sur github.

flattr this!

Error happened! 0 - count(): Argument #1 ($value) must be of type Countable|array, null given In: /var/www/ecirtam.net/autoblogs/autoblogs/autoblog.php:428 http://ecirtam.net/autoblogs/autoblogs/sametmaxcom_a844ada43a979e3b1395ab9acb6afafb84340999/?142 #0 /var/www/ecirtam.net/autoblogs/autoblogs/autoblog.php(999): VroumVroum_Blog->update() #1 /var/www/ecirtam.net/autoblogs/autoblogs/sametmaxcom_a844ada43a979e3b1395ab9acb6afafb84340999/index.php(1): require_once('...') #2 {main}