Mini rsync

Baptiste Allorant, Alexandre Bonlarron, Etienne Lozes, Laurent Tichit

But du projet

Le but du projet est de réaliser un utilitaire que l’on nommera mrsync pour “mini rsync” et qui reprend les principales fonctionnalités de l’utilitaire Rsync de synchronisation unidirectionnelle de fichiers. Rsync permet de créer et mettre à jour une copie entière d’une arborescence de fichiers. Bien que l’original et la copie puissent être deux arborescences d’un même système de fichiers, l’usage le plus courant de Rsync est de transférer l’arborescence d’un système de fichiers SRC vers un autre système de fichiers DST via une communication réseau. Lorsque SRC est le système de fichier local, on parle d’un PUSH, et inversement lorsque DST est le système de fichier local, on parle de PULL. L’objectif de Rsync est de réduire le traffic réseau autant que possible en ne transférant que les portions de fichiers qui ne sont pas déjà présentes sur DEST.

La spécification complète de ce que devra réaliser mrsync est donnée par le fichier texte mrsync.txt qui n’est rien d’autre qu’une version simplifiée de la page man de rsync (environ 4 fois plus courte que l’originale, mais déjà assez longue). Cette spécification est donc complétée par l’utilitaire rsync lui-même: si vous vous demandez “que doit faire mrsync dans telle situation?”, la réponse est “la même chose que rsync”, à quelques simplifications près:

Il n’y aura par ailleurs aucune inter-opérabilité requise entre mrsync et rsync, et le format des échanges sur les canaux de communication sera laissé à votre convenance (même si quelques fonctions utilitaires vous seront suggérées plus loin) et pourra différer de celui de rsync (de toute façon assez peu documenté).

Conseils généraux pour éviter les désagréments

Ce projet va vous amener à faire courir des risques à vos données et à la sécurité de votre ordinateur (copies et effacement de fichiers, ouverture de port ssh et d’un port “exotique” avec communications non cryptées pouvant conduire à des injections de code…). Si vous suivez bien tous les conseils, il n’y aura pas de soucis, mais deux précautions valent mieux qu’une, aussi nous vous rappellons quelques principes très généraux qui s’appliquent à n’importe quel contexte:

Étape 0 : découverte de Rsync

Pour bien démarrer ce projet, commencez par lire le fichier mrsync.txt et testez l’effet des options dont vous ne comprenez pas le sens en exécutant rsync sur votre machine. Dans un terminal, tapez rsync pour vérifier que cet utilitaire est bien installé (c’est a priori le cas sous Mac OS et Linux); au besoin, installez-le (sudo apt-get install rsync).

Vous pouvez augmenter la verbosité des messages émis par rsync en répétant l’option -v plusieurs fois (avec une verbosité de 3, vous verrez apparaitre des détails sur le protocole expliqués juste après). Par exemple

mkdir SRC DST
# remplissage de SRC avec des fichiers et des répertoires (de plusieurs Mo si possible)
...
# test de rsync
rsync -rv SRC/ DST/
# ajouts/modifications/suppressions de fichiers dans SRC
...
# test de rsync
rsync -rvv SRC/ DST/

L’usage principal de rsync n’est pas de copier des fichiers entre deux arborescences d’un même système de fichiers, mais bien entre deux machines. Si vous avez deux machines à votre disposition dont une acceptant des connections ssh, vous pourrez vraiment tester rsync. Dans la suite, nous supposons plutôt que vous n’avez qu’une seule machine (localhost), mais avec deux comptes utilisateurs: celui sur lequel vous travaillez en général, et un autre (dans la suite il lui sera donné le nom d’utilisateur “distant”). Nous supposons également qu’un serveur ssh tourne sur cette machine. Cela suffira pour tester rsync en mode réseau. Il n’est pas nécessaire que votre ordinateur soit connecté à internet, et c’est même déconseillé pour des raisons de sécurité. Consultez le guide ssh que nous vous avons préparé pour configurer votre machine, puis essayez les commandes ci-dessous.

PUSH “local”

rsync -rv SRC/ distant@localhost:

PULL “local”

rsync -rv distant@localhost:SRC/ .

Vous pouvez aussi tester le mode “daemon” de rsync, mais il vous faudra créer un fichier de configuration rsyncd.conf et ainsi définir un module… comme dit précédemment, mrsync ne prendra pas en compte la notion de module dans le mode daemon. Le mode daemon de mrsync sera donc un peu différent du mode daemon original de rsync, en fait ce sera simplement une variante du mode SSH où le tunnel SSH est remplacé par des sockets, il n’est donc pas essentiel que vous testiez le mode daemon de rsync pour comprendre la suite.

Une fois que vous aurez eu un petit aperçu de Rsync, il sera temps d’étudier le fonctionnement général de l’utilitaire. Après la lecture de la page wikipedia, ils vous est conseillé de vous pencher sur une description un peu plus approfondie sur la page How Rsync works - a practical overview. Vous allez voir qu’il est question de différents rôles et différents processus. Comprenez bien ce à quoi correspond chaque rôle, comment les processus sont créés, comment ils changent de rôle au fil du temps, par quels types de canaux ils communiquent, quels types de messages sont échangés, etc. Le transfert incrémental et les sommes de contrôle ne seront abordés qu’à la toute fin du projet par les plus courageux, vous n’avez donc pas besoin de comprendre cet aspect en détail, au moins dans un premier temps. Plutôt que vous donner des informations redondantes et gacher l’opportunité de vous faire travailler votre anglais et votre agileté à lire des documentations techniques, nous vous laissons seulement quelques figures qui résument les grandes lignes de ce qui est dit.

Suite de créations de processus et d’échanges de messages dans le cas d’un Rsync local. Dans le cas d’un PULL, le client devient receiver et le server devient sender
Canaux de communication entre le client et le serveur dans le cas d’un Rsync local, et évolution suite à la création du générateur
Canaux de communication entre le client et le serveur dans le cas d’une session ssh. Aucune fonction de manipulation de socket n’est appelée par rsync
Canaux de communication entre le client et le serveur dans le cas d’une utilisation en mode “daemon”

Quelques règles et conseils avant de commencer

Travailler en équipe

Comme vous allez peut-être travailler en parallèle avec le reste de votre équipe (binôme ou trinôme) sur le code, nous vous suggérons un découpage de votre projet en différents modules. L’utilisation de git ou d’un autre gestionnaire de versions vous facilitera aussi la tâche, mais si vous ne voulez pas apprendre git pour le moment (vous aurez l’occasion de le faire en projet de L3), vous pouvez bien sûr échanger vos fichiers entre vous par mail. Nous rappellons également la règle suivante: vous ne devez pas échanger de fichiers avec d’autres personnes que celles de votre équipe. Les discussions sur moodle ou discord sont encouragées, c’est très bien si vous posez des questions, et c’est très bien si vous aidez quelqu’un qui a posé une question à la place de l’enseignant (si ce que vous dites n’est pas correct, on corrigera). Mais restez au niveau de la langue française (ou d’un dessin), ne montrez pas de code (occasionnellement, 1 ou 2 lignes de codes peuvent être divulguées si votre question ou votre réponse n’a pas été comprise, mais évitez autant que possible). Enfin, si vous utilisez un dépot dans le cloud (bitbucket, github, etc) paramétrez votre dépot pour qu’il ne soit pas public et que seule votre équipe y ait accès. Nous utilisons un logiciel de détection de plagiat et enlèverons des points aux plagieurs mais aussi aux plagiés (sans chercher à faire de distinction) en cas de plagiat détecté, même partiel.

Découpage en modules

Le découpage en modules est laissé à votre convenance et n’est pas obligatoire. Vous pouvez adopter le découpage suivant

Communications et Modularité.

Le but de cette section est de vous donner des idées afin que de vous simplifier le travail de communication et d’ajout de fonctionnalité. Avoir ces quelques idées en tête, vous aideront à réfléchir à l’architecture et au rôle des différents modules Python et de comment ils peuvent interagir. N’hésitez pas à revenir lire cette section après avoir lu le sujet une première fois.

Tout d’abord, j’attire votre attention sur options.py, son objectif est de construire une structure de donnée par exemple un dictionnaire qui contiendra toutes les informations nécessaires au bon déroulement d’une exécution. Ces informations sont importantes à partager entre les différentes parties. On peut le voir comme un état général qui contient tous les paramètres d’exécution. Ainsi, quand on initie une communication, on partage notre état. L’autre partie s’attendant à recevoir un état, adapte son exécution en fonction de ce dernier.

Supposons un état et une connexion avec un correspondant. Une fois transmis, vous pourrez organiser votre code de la manière suivante côté destinataire :

-Si je reçois quelque chose et l'état['Options1'] est vrai 
  -Alors je fais X et je sais que je peux utiliser l'état["info1"]
-Si je reçois quelque chose et l'état['Options2'] est vrai 
  -Alors je fais Y et je sais que je peux utiliser l'état["info2"]
-Si je reçois quelque chose et l'état['Options3] ou l'état['Options4] est vrai 
  -Alors je fais Z et je sais que je peux utiliser l'état["infoX"]
-...

Remarque : cela simplifie le côté émetteur qui peut “directement” envoyer les données importantes. En effet, l’émetteur sait que le destinataire connaît son état et par là même le type de message susceptible d’être envoyé.

Voici un autre point qui vous simplifiera la vie quand vous ferez communiquer des processus. La méthode socket.send() dispose d’un paramètre optionnel tag cette chaîne de caractères pourra être utilisée pour discriminer les messages. On pourra alors écrire : (tag,msg) = socket.receive(…) Vous pourrez alors avoir un code plus “simple”, qui fera des disjonctions de cas sur les tags. Il traitera le contenu des messages simplement, sans devoir perpétuellement analyser/parser le contenu des messages et ensuite faire des disjonctions de cas. Au-delà de tout ça, cela vous permet d’avoir une approche incrémentale pour la réalisation du projet.

-Je veux ajouter une nouvelle fonctionnalité **feature** à minirsync.py
-Il faut donc qu'il y ait un nouvel état associé dans options.py
-Ce qui vous permettra de rajouter dans les autres programmes un nouveau comportement d'exécution en fonction de cet état["feature"]

Travail à rendre

Vous aurez à rendre tous les fichiers qui constituent votre projet, plus un fichier qui explique ce que sait faire votre mrsync.py. Une façon de rédiger ce dernier fichier est simplement de reprendre le fichier mrsync.txt et de l’éditer pour qu’il reflète bien ce que sait faire votre programme (vous avez le droit d’écrire en français si vous préférez, mais l’anglais est privilégié). Il vous faudra indiquer quelque part d’assez facile à trouver les noms de toute l’équipe (par exemple dans la section “AUTHORS” du fichier mrsync.txt).

Étape 1 : mrsync en mode local list-only

Objectif

Comme premier jalon dans la réalisation de votre projet, nous vous proposons de réaliser une version de mrsync avec uniquement le support du mode local, de l’option -r, et en supposant que l’option –list-only est activée (elle est activée par défaut s’il n’y a qu’un seul argument non optionnel). Vous n’aurez donc à vous préoccuper pour le moment que des modules mrsync.py, sender.py, filelist.py, et options.py.

Le genre de commandes que vous devez savoir gérer à la fin de cette étape:

./mrsync.py -r .
./mrsync.py --list-only * DST/

(note: mon fichier mrsync.py commence par un shebang et a les droits d’exécution)

Analyse de la ligne de commande

Ce travail est effectué chez moi dans le module options.py. Nous utilisons la librairie argparse, plus puissante (mais aussi plus complexe) que la librairie getopt utilisée dans l’implémentation C de rsync. Il faut définir des variables pour les différentes options et arguments, et inférer les options impliquées par d’autres options ou le mode de transfert choisi. Affichez un message d’usage issu de mrsync.txt en cas d’erreur de format sur la ligne de commande.

Vous pouvez commencer par une version minimaliste du module options.py pour permettre à tout le monde d’avancer et laisser l’un de vous s’occuper de compléter le module pour la suite.

Il pourra prendre de l’avance sur les jalons suivants et déterminer aussi le nom de l’hôte distant et de l’utilisateur distant dans le cas où il s’agit d’un transfert non local.

En plus de la prise en compte des options, il s’agit de déterminer si on est en mode local, ou s’il s’agit d’un PUSH ou d’un PULL, quels sont les fichiers et répertoires racines à transmettre (la liste des sources), et quel est le répertoire destination. Notez qu’il peut y avoir plusieurs sources, par exemple avec ./mrsync.py * . dans le cas où vous êtes dans un répertoire contenant plusieurs fichiers.

Construction de la liste de fichiers

Le but est de parcourir l’ensemble des sources et pour chacune de construire une partie de la liste de fichiers. Une source peut être un “fichier” (fichier régulier ou répertoire) ou un “chemin”: comprenez bien la différence entre ./mrsync.py DIR et ./mrsync.py DIR/ (cf mrsync.txt) et notez aussi que ./mrsync.py DIR1/DIR2/a ne garde dans la liste que a et non DIR1/DIR2/a .

Pour construire la liste de fichiers, il faut pouvoir lister les fichiers d’un répertoire donné. Il y a plusieurs solutions, on peut par exemple lancer un processus fils qui exécute la commande ls -l et récupérer la sortie (cf subprocess.run), ce qui permettra assez facilement d’avoir un affichage avec l’option –list-only qui soit similaire à la commande ls. Nous n’avons pas utilisé subprocess.run mais utilisé les fonctions os.listdir, os.path.join, os.path.isfile, os.path.isdir, os.path.islink, os.path.basename, os.getcwd, et os.chdir pour construire une liste de “noms de fichiers”, et appellé os.stat pour récupérer toutes les informations utiles sur ces fichiers, qui font pleinement partie de la liste de fichiers.

Rôle de sender.py et mrsync.py

À cette étape, ces deux modules ne font pas grand chose: mrsync.py appelle options.py pour parser la ligne de commande puis appelle sender.py. De son côté sender.py appelle filelist.py pour construire la liste de fichiers et l’affiche (pour gagner du temps, vous pouvez vous simplifier le format d’affichage de la liste de fichiers).

Étape 2 : l’envoi de fichiers en local

Le but de cette étape est de pouvoir exécuter des commandes comme

./mrsync.py -r A/ B/
./mrsync.py * DST/

Cette étape est la plus longue de toutes, c’est celle où vous allez mettre en place le plus de choses qui serviront à nouveau après.

Mes nouveaux modules à cette étape sont server.py, receiver.py, generator.py, et message.py. mrsync.py doit en effet lancer le serveur en le reliant au client avec deux tubes. Une fois lancé, le serveur devient receveur, calcule la liste de fichiers dans le répertoire destination, puis attend la liste de fichiers envoyée par l’envoyeur. Une fois celle-ci reçue, le receveur forke le générateur. Ce dernier compare les deux listes et envoie une requête à l’envoyeur pour chaque fichier manquant ou nécessitant une mise à jour. L’envoyeur répond à chaque requête en envoyant le fichier au receveur. Le receveur écrit les données reçues dans le fichier. On ne s’occupe pas du transfert de fichier incrémental, on fait comme si l’option –whole-file était activée (ce qui est d’ailleurs le cas par défaut en local).

Lancement du serveur et connection via des tubes

Ceci devrait fortement vous rappeler ce que vous avez vu en TD/TP, en particulier l’exercice 3.1 du TP 4. Il vous faudra utiliser os.fork, os.pipe et os.close. Ce n’est pas complètement obligatoire, mais vous pouvez rediriger l’entrée standard et la sortie standard du serveur vers les tubes et faire par la suite tous les échanges de messages côté serveur en passant par les descripteurs de fichiers 0 et 1. Cela complique un peu les logs (le serveur ne peut pas faire de print directement mais doit envoyer un message au client pour qu’il fasse le print à sa place) mais cela vous prépare bien pour l’étape suivante où il ne sera de toute façon pas possible de faire de logs avec print côté serveur. Pour débugger côté serveur (si vous avez fait la redirection) il vous reste la possibilité de faire print(...,file=sys.stderr).

Envois et réceptions de messages : au coeur des entrées/sorties

Les envois et réceptions de messages se font chez moi par le biais de deux fonctions send(fd,tag,v) et receive(fd): fd est le descripteur de fichier utilisé pour envoyer/recevoir, tag est une chaine de caractères qui permet de donner une information sur la nature du message (pratique pour le débuggage, mais aussi pour savoir où on en est du protocole à un point de programme donné), et v est une valeur quelconque Python (une liste, un dictionnaire, une str, un bytearray, …). La fonction receive renvoie un couple (tag,v) correspondant à ce qui a été envoyé. Pour réaliser ces fonctions, nous utilisons os.read et os.write, mais aussi pickle.dumps et pickle.loads pour passer d’une valeur Python à une suite d’octets1, et enfin les méthodes from_bytes et to_bytes de la classe int pour convertir un entier Python en une représentation de cet entier sur 3 octets (pour donner dans l’en-tête la taille du message). Cela limite la taille d’un message à 23x8 octets, soit 16 Mo. Par ailleurs, les mémoires tampons utilisées en interne dans le système par les tubes et les sockets sont de taille plus petite, ce qui implique que :

Tout cela n’est pas si simple à faire de manière efficace en Python (le langage C serait ici un peu plus adapté), et pour éviter que vos entrées/sorties n’aient une complexité quadratique due à des recopies intempestives de vos bytearray, vous pourrez vous pencher sur ce billet de blog et jouer avec les memoryview. Nous avons suivi cette approche pour les envois, mais pour les réceptions nous utilisons os.read et utilisons la méthode join de bytearray (une recopie inutile, mais au moins la complexité reste linéaire). Rien ne vous interdit de vous affranchir de os.read et os.write et de travailler avec par exemple la méthode readinto des descripteurs de fichiers Python comme indiqué dans le billet de blog (qui ne sont pas les descripteurs de fichiers Unix manipulés par os.open, os.read, os.write, etc). Il y a aussi la fonction os.readv qui semble offrir une alternative intéressante.

Outre les deux fonctions send et receive, mon module message.py contient aussi d’autres petites fonctions utilitaires comme une fonction pour faire du log qui consulte les options (notamment -v et -q) pour décider si afficher un message ou non.

Pour aller plus loin avec les entrées/sorties

Vous aurez à gérer deux options liées aux entrées/sorties, que vous pouvez ignorer à cette étape, mais dont c’est probablement le meilleur moment de parler.

La première est l’option –timeout, qui fixe un timeout pour tout read/write. Il y a plusieurs façons de mettre en place un timeout:

L’autre option que vous aurez à gérer est –blocking-io. En fait si vous ne faites rien vous supposerez que cette option est toujours activée. Cependant il est recommandé de faire des entrées/sorties non bloquantes pour le transport SSH, vous pourrez donc utiliser la fonction os.setblocking pour gérer correctement cet aspect.

Le protocole à trois generator-sender-receiver

Ce protocole devrait déjà être clair pour vous si vous avez bien suivi les explications en anglais plus haut. Nous allons quand même le répéter un peu, en vous évitant les histoires de somme de contrôle et de numéro de blocs dont vous n’avez pas l’utilité pour le moment.

Au démarrage, avant le lancement du générateur, l’envoyeur et le receveur calculent chacun de leur côté leur liste de fichiers (le receveur fait un chdir vers le répertoire destination puis appelle la fonction du module filelist de l’étape précédente avec “*“). Puis l’envoyeur envoie la liste de fichiers au receveur, qui crée le générateur.

Le générateur trie la liste de fichiers de l’envoyeur par ordre croissant (de façon à traiter les répertoires avant les fichiers qu’ils contiennent) puis examine les fichiers un à un, et pour chacun détermine s’il faut ou non faire une requête à l’envoyeur. Il y a tout un tas de cas à regarder, suivant que le fichier existe seulement chez l’envoyeur ou pas, s’il s’agit d’un répertoire ou pas, etc… lisez bien la doc des options (notamment –size-only, -I, –force, –existing, –ignore-existing) pour comprendre ce qu’il faut faire, même si vous n’implémentez pas toutes ces options tout de suite.

Le générateur compare aussi les deux listes de fichiers pour déterminer quels sont les fichiers présents dans la destination qui ne sont pas à la source (ceux qui seront effacés lorsque l’option –delete est activée). Quand il a terminé, il envoie un message à l’envoyeur pour le prévenir (ou si on veut, il ferme simplement le tube). Enfin, chez moi le générateur gère aussi les options –perm et –times lorsqu’une version à jour du fichier est déjà présente et qu’il n’y aura pas de requête (ça me parait plus simple, mais on pourrait laisser faire cela au receveur).

L’envoyeur, de son côté, après avoir envoyé la liste de fichiers, se met en attente de messages de requête. Pour chaque message reçu, il obtient un nom de fichier qui doit être copié, et qui peut contenir un chemin. Cependant ce chemin est en général uniquement un suffixe du chemin du fichier en local4, il faut donc avoir sauvegardé l’association suffixe->chemin complet quelque part pendant la construction de la liste de fichiers. L’envoyeur ouvre ensuite le fichier, le lit, et l’envoie au receveur, possiblement en plusieurs messages (surtout si le fichier est plus gros que 16 Mo). Vous pouvez utiliser un tag pour débuter l’envoi du fichier et communiquer au receveur de quel fichier il s’agit, puis un tag “data” pour les messages qui contiennent des bytearray issus de la lecture du fichier, et un dernier tag “fin d’envoi” pour indiquer que l’envoi du fichier est fini (ce n’est pas absolument nécessaire, la taille du fichier étant connue du receveur, c’est plus simple même si cela prend un peu plus de temps en communications). Quand il a traité toutes les requêtes, l’envoyeur termine en prévenant le receveur que c’est fini.

Enfin, le receveur se met en attente de messages “début d’envoi”, récupère le nom du fichier, crée le fichier ou le remplace5, et le remplit avec les données qu’il reçoit.

3 - Fonctions avancées

Étape 3.1 : le transfert incrémental

Il ne s’agit pas à proprement parler de programmation système dans cette partie. Donc vous pouvez vous y lancer si vous aimez python davantage que l’UE Système 2.

Le but est de prendre en compte l’option –whole-file (ou plutôt, le fait que cette option n’est pas nécessairement activée).

C’est la grosse killer feature de rsync : le fait que par défaut, il ne transfère que les parties des fichiers qui ont été modifiées. Ce qui induit un gain de performance parfois considérable.

Tout est expliqué ici et ou encore et en fouillant un peu vous trouverez les fonctions de hachage qu’il vous faut dans la librairie standard de Python. En bonus, vous pouvez même implémenter l’option -z de rsync (c’est une indication d’où chercher).

À vous de jouer.

Remarque: l’idée est de faire du transfert incrémental, pas de copier exactement tout ce que fait la commande originale.

Étape 3.2 : le mode ssh

Pour cette troisième étape, vous aurez beaucoup moins de choses à coder. Le but est de pouvoir gérer des commandes comme

./mrsync.py localhost:
./mrsync.py distant@localhost:
./mrsync.py -a A/ distant@localhost:B/
./mrsync.py -a distant@localhost:A/ B/

Comprenez bien le fonctionnement de ssh: regardez à nouveau la figure présentée plus haut représentant le tunnel ssh. Essayez et comprenez les commandes suivantes

ssh distant@localhost ls
ssh localhost cat

Il va vous falloir installer mrsync.py (et les autres modules) dans un endroit accessible sur le compte distant (et sur votre compte local). Théoriquement il suffit de rajouter le répertoire qui contient vos fichiers dans le PATH, mais comme il faut modifier le fichier de config du serveur ssh pour que ce soit pris en compte, nous n’avons pas utilisé cette solution. Nous avons fait plus simple: nous avons placé mrsync.py dans le répertoire HOME de distant (mais rien ne vous interdit de faire plus propre).

scp *.py distant@localhost:

Vérifiez votre installation en tapant

ssh distant@localhost ./mrsync.py

et si vous avez bien mis en place un message d’usage dans le cas où il n’y a pas de paramètres, vous devriez le voir s’afficher.

Quand par la suite vous remettrez à jour vos fichiers, pensez à refaire une copie avec scp pour qu’ils soient bien à jour sur les deux comptes.

Maintenant que vous savez appeler mrsync.py via ssh depuis le shell, il va falloir le faire depuis le client mrsync.py. En mode ssh, le client forke un processus “serveur” connecté via des tubes, mais ce processus serveur va être recouvert par un processus ssh qui crée la connection à distance et lance le “vrai” processus serveur sur la machine distante. C’est là que l’option –server intervient.

Allez, un peu d’aide : le client appelle la fonction os.execvp pour exécuter la ligne de commande

ssh -e none -l distant localhost -- ./mrsync.py --server ...

... correspond à la suite de la ligne de commande initiale, et le -- est plus une bonne pratique qu’une nécessité. Lisez la page man de ssh pour comprendre les options -e et -l. Nous vous mentionnons aussi un point qui pourrait vous surprendre: ssh va bien vous demander votre mot de passe et le lire au clavier, bien que vous ayez redirigé l’entrée standard sur le tube avant de lancer ssh; en fait ssh ne se base pas sur le descripteur de fichier 0 pour lire le mot de passe (sinon il serait visible quand on le tape) mais il accède directement au terminal qui lui est rattaché. De même ssh affiche le prompt “Password” directement dans le terminal et ne va pas encombrer le tube. Ouf, une complication de moins à gérer!

Vous devriez avoir tout de même très peu de lignes nouvelles à écrire à cette étape (peut-être une vingtaine…), mais il vous faudra peut-être revoir la structure de votre code pour que tout s’emboite bien. La principale difficulté sera peut-être la gestion de commandes de pull list-only comme ./mrsync.py localhost:… repensez vos fonctions de log pour que client et serveur puissent faire du log, le serveur envoyant ses messages de log au client pour qu’il les affiche.

Étape 3.3 : Le mode daemon et les sockets

Pour cette étape, le but est de pouvoir exécuter les commandes suivantes

# depuis un shell sur le compte distant
./mrsync.py --daemon
# puis depuis un shell sur le compte perso
./mrsync.py localhost::
./mrsync.py -r A/ localhost::B/
./mrsync.py -r localhost::A/ B/

La première commande démarre le démon: c’est un processus qui se détache du terminal dans lequel il a été lancé et se comporte ensuite comme ce qu’on a appelé un serveur dans les TDs et TPs: le processus se met en attente sur le port 10873 (par défaut, voir option –port) de demandes de connections et les accepte. Le démon sera un serveur “multiprocessus” comme indiqué dans les parties 4 (rsh) et 5 du TD 7: à chaque nouvelle connexion, il crée un processus fils “serveur” qui commence un dialogue avec le client sur la socket de service selon le même protocole que précédemment. On ne limitera pas le nombre de processus/connections simultanées et on n’utilisera pas de threads (que vous n’avez de toute façon pas vu).

Il y a un certain nombre de recommandations standards pour créer un démon, le plus simple sera que vous lisiez la documentation du module daemon qui ne fait pas partie de la librairie standard de Python, mais que vous pouvez installer facilement avec pip si vous le souhaitez. Tout cela peut se faire directement avec les fonctions disponibles dans la librairie standard de Python, il faut seulement ne rien oublier… À vous de voir si vous préférez utiliser la librairie daemon ou en reprogrammer la partie qui vous intéresse. Ce que doit faire votre démon:

Vu que le démon n’aura pas de sortie standard ni de sortie d’erreur, vous pourrez faire du log en écrivant soit dans un fichier prédéfini, soit en utilisant syslog. Il vous est recommandé de rediriger la sortie d’erreur vers un fichier mrsync.err pour pouvoir débugger votre programme plus facilement (le champs stderr de l’objet DaemonContext vous permet de faire ça facilement).

Étape 3.4 : Un maximum d’options

Passez en revue mrsync.txt et implémentez un maximum d’options.

Documentation en ligne

Notes

[1] Comme mentionné dans la documentation de la librairie Pickle, l’usage de loads sur des données réseau non sécurisées (comme c’est le cas pour nous en mode daemon) expose à des attaques par injection de code. Si vous voulez tester votre mrsync.py en mode daemon sur un vrai réseau, utilisez plutôt Json pour sérialiser vos structures de données (tant que vous restez en mode ssh, il n’y a pas de problème à utiliser pickle).

[2] Notez que l’envoi simultané de données volumineuses par deux processus reliés par un canal full-duplex (comme une paire de tubes ou des sockets) peut conduire à un interblocage (appelé dans ce cas précis un “head-to-head” ou “send-send” deadlock): chacune des deux parties est bloquée en envoi et attend une réception de l’autre partie pour progresser. Pour éviter ce type de problème, on privilégie des protocoles de communication half-duplex, voire, comme c’est le cas dans ce projet, des communications simplex (c’est l’intérêt d’avoir un processus generateur distinct du processus receveur). L’utilisation d’entrées-sorties non-bloquantes permet aussi d’éviter ce type d’interbloquage, mais avec un code un peu naïf on risque de simplement remplacer le deadlock par de l’attente active sans progrès (spinlock).

[3] Sur Mac OS, par exemple, c’est au moins le cas pour un write de 4 Go dans un tube, donc certes au-dela de la taille limite des messages, mais rien n’interdit que cela se produise avec des messages plus petits dans d’autres circonstances, en particulier si on écrit sur une socket.

[4] Prenons un exemple: soit la commande mrsync -r A1/ A2/ B/ et supposons que A1/ contient un répertoire A3 qui contient le fichier a. La requête envoyée par le générateur est “A3/a” mais l’envoyeur devra y répondre en ouvrant le fichier “A1/A3/a”.

[5] Pour l’étape finale (le transfert incrémental), il faut garder une copie du fichier côté receveur pour pouvoir aller y piocher les blocs qui n’ont pas changé. Si vous voulez faire comme rsync, vous pouvez écrire le fichier reçu dans un fichier temporaire, et ne faire le remplacement qu’une fois que le fichier est entièrement reçu, ce qui permet à rsync de faire des choses assez avancées (cf options –partial ou –backup de rsync).