Quellcode durchsuchen

Merge pull request #16 from Loquicom/myps

Myps
Loquicom vor 6 Jahren
Ursprung
Commit
99ba91aef1
10 geänderte Dateien mit 535 neuen und 49 gelöschten Zeilen
  1. 1 1
      .gitignore
  2. 7 13
      README.md
  3. 1 3
      command.c
  4. 19 0
      execute.h
  5. 4 3
      makefile
  6. 57 4
      myls.c
  7. 0 23
      myls.h
  8. 416 0
      myps.c
  9. 4 2
      mysh.c
  10. 26 0
      str.h

+ 1 - 1
.gitignore

@@ -57,4 +57,4 @@ dkms.conf
 *.log
 /bug.txt
 /myls
-
+/myps

+ 7 - 13
README.md

@@ -1,13 +1,13 @@
 # Projet SEC (MySh) Master 1 Artois 2018-2019
-### Brandao Arthur (75%) & Maxence Bacquet (25%)
+### Brandao Arthur (60%) & Maxence Bacquet (40%)
 
 
 
 ## Utilisation
 
-La compilation du programme s'effectue avec le commande `make` dans le dossier du projet. Il est possible de recréer les dépendances avec la commande `make depend`, et de supprimer les logs avec `make rm-log`. Pour lancer le programme il suffit d'exécuter le fichier ./mysh.
+La compilation du programme s'effectue avec la commande `make` dans le dossier du projet. Il est possible de recréer les dépendances avec la commande `make depend`, et de supprimer les logs avec `make rm-log`. Pour lancer le programme il suffit d'exécuter le fichier ./mysh.
 
-Les commandes externes (myls et myps) sont exécutable sans passer par le programme mysh, mais sont aussi utilisable comme des commandes internes de ce dernier. Il suffit juste de taper le nom de la commande dans un terminal mysh pour l'exécuter.
+Les commandes externes (myls et myps) sont exécutables sans passer par le programme mysh, mais sont aussi utilisables comme des commandes internes de ce dernier. Il suffit juste de taper le nom de la commande dans un terminal mysh pour l'exécuter.
 
 
 
@@ -35,21 +35,15 @@ Absentes :
 
 
 
-## Bug / Fonctionalitées partiels
+## Bugs / Fonctionalitées partiels
 
-La commande myps liste les processus en cours d'exécution avec leur PID, le nom de la commande et l'utilisateur qui l'a lancé. Elle ne récupère qu'une partie des infos présente dans `ps -aux`.
-
-La commande myls n'indique pas la valeur de la 2éme colonne du `ls -l` (un nombre), et affiche X à la place.
+Il manque 3 champs dans la commande myps : %CPU, %MEM, TIME. De plus le champ TTY est sous la forme d'un nombre et non d'un texte comme dans la commande `ps -aux`.
 
 
 
 ## Blagounette
 
-Qu'est-ce qu'un canife ?
-
-Un petit fien.
-
-
-
+Comment appelle-t-on un combat entre un petit pois et une carotte ?
 
+Un bon duel !
 

+ 1 - 3
command.c

@@ -367,7 +367,6 @@ int myls(int argc, char** argv){
     path[length + 4] = 's';
     //Execution
     return exec_file(path, argv);
-    //return 0;
 }
 
 int myps(int argc, char** argv){
@@ -382,8 +381,7 @@ int myps(int argc, char** argv){
     path[length + 3] = 'p';
     path[length + 4] = 's';
     //Execution
-    //return exec_file(path, argv);
-    return 0;
+    return exec_file(path, argv);
 }
 
 int myjobs(int argc, char** argv){

+ 19 - 0
execute.h

@@ -17,8 +17,27 @@ extern pid_t active;
 extern pid_t last;
 
 /* --- Fonctions publiques --- */
+/**
+ * Indique si un fichier est executable
+ * @param char* Chemin vers le fichier
+ * @return Vrai/Faux
+ */
 boolean is_executable_file(const char *);
+
+/**
+ * Execute une commande shell
+ * @param char* Nom de la commande
+ * @param char** Arguments (argv)
+ * @return Status de la commande
+ */
 int exec_shell(char*, char**);
+
+/**
+ * Execute un programme
+ * @param char* Nom du fichier executable
+ * @param char** Arguments (argv)
+ * @return Status du programme
+ */
 int exec_file(char*, char**);
 
 #endif /* EXECUTE_H */

+ 4 - 3
makefile

@@ -2,7 +2,7 @@
 # CONFIGURATION GENERALE
 #
 
-EXEC = mysh myls
+EXEC = mysh myls myps
 OBJETS = error.o str.o parser.o wildcard.o command.o execute.o sem.o shm.o subdiv.o ipc.o expreg.o variable.o
 NOM_PROJET = mini-shell
 
@@ -113,5 +113,6 @@ ipc.o: ipc.c ipc.h constante.h sem.h shm.h subdiv.h variable.h
 expreg.o: expreg.c expreg.h constante.h
 variable.o: variable.c str.h expreg.h constante.h variable.h subdiv.h
 mysh.o: mysh.c error.h str.h parser.h constante.h mysh.h command.h ipc.h \
- sem.h shm.h subdiv.h execute.h
-myls.o: myls.c error.h color.h myls.h constante.h
+ sem.h shm.h subdiv.h execute.h color.h
+myls.o: myls.c error.h color.h constante.h
+myps.o: myps.c error.h color.h constante.h

+ 57 - 4
myls.c

@@ -11,14 +11,47 @@
 #include <pwd.h>
 #include "error.h"
 #include "color.h"
-#include "myls.h"
+#include "constante.h"
 
+int get_total(char* path, struct dirent** dir, int nb, boolean hidden){
+    struct stat info;
+    char* completePath;
+    int length, total = 0, i = 0;
+    //Si besoins passe les fichiers cachés
+    while(i < nb && !hidden && *dir[i]->d_name == '.') i++;
+    //Parcours tous les fichiers
+    for(; i < nb; i++){
+        //Création chemin vers le dossier
+        length = strlen(path) + strlen(dir[i]->d_name) + 2;
+        completePath = malloc(sizeof(char) * length);
+        memset(completePath, 0, length);
+        if(path[strlen(path)-1] != '/'){
+            snprintf(completePath, length, "%s/%s", path, dir[i]->d_name);
+        } else {
+            snprintf(completePath, length, "%s%s", path, dir[i]->d_name);
+        }
+        //Recup info fichier
+        if(stat(completePath, &info) == ERR){
+            free(completePath);
+            continue;
+        }
+        //Ajoute au total
+        total += info.st_blocks;
+    }
+    return total / 2;
+}
+
+/**
+ * Affiche sous la forme ls -l les infos d'un fichier/dossier
+ * @param char* Le chemin vers le fichier (avec ou sans le nom du fichier)
+ * @param char* Le nom du fichier ou NULL si il est déjà dans le chemin
+ */
 void printls(char* path, char* filename){
         int length;
         char* completePath;
         char permission[11];
         char mois[5];
-        char heure[3], minute[3];
+        char jour[3], heure[3], minute[3];
         struct stat info;
         struct group* grp;
         struct passwd* user;
@@ -89,7 +122,13 @@ void printls(char* path, char* filename){
 
         //Recup le groupe et l'utilisateur
         grp = getgrgid(info.st_gid);
+        if(grp == NULL){
+            return;
+        }
         user = getpwuid(info.st_uid);
+        if(user == NULL){
+            return;
+        }
 
         //Recup la date
         memset(mois, 0, 5);
@@ -132,6 +171,13 @@ void printls(char* path, char* filename){
                 strcpy(mois, "dec.");
                 break;
         }
+        //Met le jour sur 2 chiffre
+        memset(jour, 0, 3);
+        if(date->tm_mday < 10){
+            snprintf(jour, 3, "0%d", date->tm_mday);
+        } else {
+            snprintf(jour, 3, "%d", date->tm_mday);
+        }
         //Met l'heure sur 2 chiffre
         memset(heure, 0, 3);
         if(date->tm_hour < 10){
@@ -148,7 +194,7 @@ void printls(char* path, char* filename){
         }
 
         //Affiche
-        printf("%s X %s %s %ld %s  %d %s:%s ", permission, user->pw_name, grp->gr_name, info.st_size, mois, date->tm_mday, heure, minute);
+        printf("%s %ld %s %s %ld %s  %s %s:%s ", permission, info.st_nlink, user->pw_name, grp->gr_name, info.st_size, mois, jour, heure, minute);
 
         //color the name
         if(permission[0] == 'd'){
@@ -162,6 +208,12 @@ void printls(char* path, char* filename){
         }
 }
 
+/**
+ * Affiche tous le contenu d'un dossier
+ * @param char* Le chemin vers le dossier
+ * @param boolean Affiche ou non les sous dossiers
+ * @param boolean Affiche ou non les fichiers/dossiers cachés
+ */
 void printdir(char* path, boolean subdir, boolean hidden){
     struct dirent** contentsDir;
     int nbFile;
@@ -175,6 +227,7 @@ void printdir(char* path, boolean subdir, boolean hidden){
     if(subdir){
         printf("%s :\n", path);
     }
+    printf("total %d\n", get_total(path, contentsDir, nbFile, hidden));
     //Si besoins passe les fichiers cachés
     while(j < nbFile && !hidden && *contentsDir[j]->d_name == '.') j++;
     //Parcours les fichiers du dossier
@@ -204,7 +257,7 @@ void printdir(char* path, boolean subdir, boolean hidden){
             if(path[strlen(path)-1] != '/'){
                 snprintf(completePath, length, "%s/%s", path, contentsDir[j]->d_name);
             } else {
-                 snprintf(completePath, length, "%s%s", path, contentsDir[j]->d_name);
+                snprintf(completePath, length, "%s%s", path, contentsDir[j]->d_name);
             }
             //Recup info fichier
             if(stat(completePath, &info) == ERR){

+ 0 - 23
myls.h

@@ -1,23 +0,0 @@
-#ifndef MYLS_H
-#define MYLS_H
-
-/* --- Include --- */
-#include "constante.h"
-
-/* --- Fonctions --- */
-/**
- * Affiche sous la forme ls -l les infos d'un fichier/dossier
- * @param char* Le chemin vers le fichier (avec ou sans le nom du fichier)
- * @param char* Le nom du fichier ou NULL si il est déjà dans le chemin
- */
-void printls(char*, char*);
-
-/**
- * Affiche tous le contenu d'un dossier
- * @param char* Le chemin vers le dossier
- * @param boolean Affiche ou non les sous dossiers
- * @param boolean Affiche ou non les fichiers/dossiers cachés
- */
-void printdir(char*, boolean, boolean);
-
-#endif /* MYLS_H */

+ 416 - 0
myps.c

@@ -0,0 +1,416 @@
+#define _POSIX_C_SOURCE 200809L
+
+#include <stdlib.h>
+#include <stdio.h>
+#include <dirent.h>
+#include <string.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <unistd.h>
+#include <fcntl.h>
+#include <pwd.h>
+#include <time.h>
+#include "error.h"
+#include "color.h"
+#include "constante.h"
+
+#define MAX_CMD_SIZE 50
+
+typedef long long int llint;
+typedef unsigned long long ull;
+
+typedef struct{
+	char* path; //Chemin vers le dossier du processus /./proc/[pid]/
+	int pid; //Le pid
+	int uid; //Le uid du proprietaire
+	char state; //L'etat
+	char* cmd; //La commande
+	llint tty;
+	llint cpu;
+	ull start;
+	llint vsz;
+	llint rss;
+}processus;
+
+/**
+ * Indique si une string est un nombre
+ * @param char* Le string
+ * @return Vrai/Faux
+ */
+boolean is_numeric(const char* str){
+	while(*str){
+		if(*str < '0' || *str > '9'){
+			return false;
+		}
+		str++;
+	}
+	return true;
+}
+
+/**
+ * Tri les processus par pid croissant
+ * @param processus** Le tableau de processus
+ */
+void sort_pid(processus** proc, int size){
+	processus* tmp;
+	int index;
+	for(int i = 1; i < size; i++){
+		index = i;
+		while(index > 0 && proc[index]->pid < proc[index - 1]->pid){
+			tmp = proc[index];
+			proc[index] = proc[index - 1];
+			proc[index - 1] = tmp;
+			index--;
+		}
+	}
+}
+
+int get_uptime() {
+  	FILE* procUptime;
+  	int sec, ssec;
+  	procUptime = fopen("/proc/uptime", "r");
+  	int tmp = fscanf(procUptime, "%d.%ds", &sec, &ssec);
+  	tmp++; //Pour ne pas avoir de warning
+  	fclose(procUptime);
+  	return (sec * sysconf(_SC_CLK_TCK)) + ssec;
+}
+
+/**
+ * Liste les processus actifs
+ * @param int* Le nombre de processus actif
+ * @return processus** La liste
+ */
+processus** list_process(int* nb){
+	struct dirent** dir;
+	struct stat info;
+	int nbRes;
+	int length, compteur = 0;
+	char* path;
+	processus** result;
+	//Recup les dossiers
+    if((nbRes = scandir("/./proc/", &dir, 0, alphasort)) == ERR){
+        addperror("Impossible de scanner le dossier");
+        return NULL;
+    }
+    //Compte le nombre de resultat
+    for(int i = 0; i < nbRes; i++){
+    	//Création chemin
+    	length = strlen(dir[i]->d_name) + 10;
+	   	path = malloc(sizeof(char) * length);
+	   	memset(path, 0, length);
+	   	snprintf(path, length, "/./proc/%s/", dir[i]->d_name);
+    	//Recup info
+        if(stat(path, &info) == ERR){
+            //Pas un dossier
+            continue;
+        }
+        //Ne regarde que les dossiers dont le nom est un nombre
+        if(S_ISDIR(info.st_mode) && is_numeric(dir[i]->d_name)){
+        	compteur++;
+        }
+        free(path);
+    }
+    //Allocation resultat
+    result = malloc(sizeof(processus*) * compteur);
+    //Ajout des resultats
+    compteur = 0;
+    for(int i = 0; i < nbRes; i++){
+    	//Création chemin
+    	length = strlen(dir[i]->d_name) + 10;
+	    path = malloc(sizeof(char) * length);
+	    memset(path, 0, length);
+	    snprintf(path, length, "/./proc/%s/", dir[i]->d_name);
+    	//Recup info
+        if(stat(path, &info) == ERR){
+        	//Pas un dossier
+            continue;
+        }
+        //Ne regarde que les dossiers dont le nom est un nombre
+        if(S_ISDIR(info.st_mode) && is_numeric(dir[i]->d_name)){
+        	result[compteur] = malloc(sizeof(processus));
+        	result[compteur]->path = path;
+        	result[compteur]->pid = atoi(dir[i]->d_name);
+        	result[compteur]->uid = info.st_uid;
+        	compteur++;
+        	continue;
+        }
+        free(path);
+    }
+    //Libere memoire
+    while(nbRes--){
+    	free(dir[nbRes]);
+    }
+    free(dir);
+    //Retour
+    if(nb != NULL){
+    	*nb = compteur;
+    }
+    return result;
+}
+
+/**
+ * Lecture du fichier status d'un processus
+ * @param processus* Le processus
+ * @return Reussite
+ */
+boolean read_status(processus* proc){
+	char* file;
+	FILE* f;
+	int length, tmp;
+	char buffer[BUFFER_SIZE];
+	llint trash;
+	//Recup nom du fichier
+	length = strlen(proc->path) + 5;
+	file = malloc(sizeof(char) * length);
+	memset(file, 0, length);
+	snprintf(file, length, "%sstat", proc->path);
+	//Ouverture fichier
+	f = fopen(file, "r");;
+	if(!f){
+		free(file);
+		return  false;
+	}
+	free(file);
+	//Lecture des informations
+	tmp = fscanf(f, "%lld ", &trash);
+	tmp = fscanf(f, "%s ", buffer);
+	tmp = fscanf(f, "%c ", &proc->state);
+	for (int i = 0; i < 3; i++){
+		tmp = fscanf(f, "%lld ", &trash);
+	}
+	tmp = fscanf(f, "%lld ", &proc->tty);
+	for (int i = 0; i < 6; i++){
+		tmp = fscanf(f, "%lld ", &trash);
+	}
+	tmp = fscanf(f, "%lld ", &proc->cpu);
+	for (int i = 0; i < 7; i++){
+		tmp = fscanf(f, "%lld ", &trash);
+	}
+	tmp = fscanf(f, "%llu ", &proc->start);
+	tmp = fscanf(f, "%lld ", &proc->vsz);
+	tmp = fscanf(f, "%lld ", &proc->rss);
+	tmp++; //Pour ne pas avoir de warning
+	fclose(f);
+	return true;
+}
+
+/**
+ * Lecture du fichier comm d'un processus
+ * @param processus* Le processus
+ * @return Reussite
+ */
+boolean read_comm(processus* proc){
+	char* file;
+	int length, fd;
+	int size = 0;
+	char buf = ' ';
+	char* buffer;
+	boolean first = true;
+	//Recup nom du fichier
+	length = strlen(proc->path) + 5;
+	file = malloc(sizeof(char) * length);
+	memset(file, 0, length);
+	snprintf(file, length, "%scomm", proc->path);
+	//Ouverture fichier
+	fd = open(file, O_RDONLY);
+	if(fd == ERR){
+		free(file);
+		return  false;
+	}
+	free(file);
+	//Compte la taille du mot
+	while(buf != '\n' && buf != '\0' && buf != EOF){
+		if(read(fd, &buf, sizeof(char)) == ERR){
+			return false;
+		}
+		//Au 1er tour
+		if(first){
+			//Si fichier vide
+			if(buf == ' '){
+				if(close(fd) == ERR){
+				//Rien de particulier
+				}
+				return false;
+			}
+			first = false;
+		}
+		size++;
+	}
+	size--;
+	//Revient au debut
+	if(lseek(fd, 0L, SEEK_SET) == ERR){
+		return false;
+	}
+	//Lecture commande
+	buffer = malloc(sizeof(char) * (size + 3));
+	memset(buffer, 0, size + 3);
+	buffer[0] = '[';
+	if(read(fd, buffer + 1, size) == ERR){
+		return false;
+	}
+	buffer[strlen(buffer)] = ']';
+	proc->cmd = buffer;
+	//Fermeture
+	if(close(fd) == ERR){
+		//Rien de particulier
+	}
+	return true;
+}
+
+/**
+ * Lecture du fichier cmdline d'un processus
+ * @param processus* Le processus
+ * @return Reussite
+ */
+boolean read_cmd(processus* proc){
+	char* file;
+	int length, fd;
+	int size = 0;
+	char buf = ' ';
+	char* buffer;
+	boolean first = true;
+	//Recup nom du fichier
+	length = strlen(proc->path) + 8;
+	file = malloc(sizeof(char) * length);
+	memset(file, 0, length);
+	snprintf(file, length, "%scmdline", proc->path);
+	//Ouverture fichier
+	fd = open(file, O_RDONLY);
+	if(fd == ERR){
+		free(file);
+		return  false;
+	}
+	free(file);
+	//Compte la taille du mot
+	while(buf != '\n' && buf != '\0' && buf != EOF){
+		if(read(fd, &buf, sizeof(char)) == ERR){
+			return false;
+		}
+		//Au 1er tour
+		if(first){
+			//Si fichier vide
+			if(buf == ' '){
+				if(close(fd) == ERR){
+				//Rien de particulier
+				}
+				return read_comm(proc);
+			}
+			first = false;
+		}
+		size++;
+		//On evite les boucles infini
+		if(size > MAX_CMD_SIZE){
+			break;
+		}
+	}
+	size--;
+	//Revient au debut
+	if(lseek(fd, 0L, SEEK_SET) == ERR){
+		return false;
+	}
+	//Lecture commande
+	buffer = malloc(sizeof(char) * (size + 1));
+	memset(buffer, 0, size + 1);
+	if(read(fd, buffer, size) == ERR){
+		return false;
+	}
+	proc->cmd = buffer;
+	//Fermeture
+	if(close(fd) == ERR){
+		//Rien de particulier
+	}
+	return true;
+}
+
+/**
+ * Affiche les infos d'un processus
+ * @param processus* Le processus
+ */
+void printps(processus* proc){
+	struct passwd* user;
+	boolean color = false;
+	llint vsize;
+	char start[BUFFER_SIZE];
+	//Recup le nom de l'utilisateur
+	user = getpwuid(proc->uid);
+	if(user == NULL){
+		addperror("Impossible de récupérer le nom de l'utilisateur");
+		return;
+	}
+	//VSZ
+	vsize = proc->vsz / 1024;
+	//Debut
+	int uptime = get_uptime();
+  	int running = uptime - proc->start;
+  	time_t runningTime = time(NULL) - (running / sysconf(_SC_CLK_TCK));
+  	strftime(start, sizeof(start), "%H:%M", localtime(&runningTime));
+	//Couleur
+	if(proc->state == 's'){
+		printf(RED);
+	}
+	if(proc->state == 'r'){
+		printf(MAGENTA);
+	}
+	switch(proc->state){
+		case 'S':
+			printf(RED);
+			color = true;
+			break;
+		case 'R':
+			printf(YELLOW);
+			color = true;
+			break;
+		case 'N':
+			printf(BLUE);
+			color = true;
+			break;
+		case 'Z':
+			printf(GREEN);
+			color = true;
+			break;
+		case 'T':
+			printf(MAGENTA);
+			color = true;
+			break;
+		case 'D':
+			printf(CYAN);
+			color = true;
+			break;
+	}
+	//Affiche (Manque %CPU, %MEM, TIME)
+	printf("%s %d %lld %lld %lld %c %s %s\n", user->pw_name, proc->pid, vsize, proc->rss, proc->tty, proc->state, start, proc->cmd);
+	if(color){
+		printf(RESET);
+	}
+}
+
+int main(){
+	int total;
+	processus** proc = list_process(&total);
+	//Tri des processus par rapport à leur pid
+	sort_pid(proc, total);
+	if(total > 0){
+		printf("USER PID VSZ RSS TTY STAT START COMMAND \n");
+	}
+	for(int i = 0; i < total; i++){
+		//Recup info manquante
+		if(!read_status(proc[i])){
+			free(proc[i]->path);
+			free(proc[i]);
+			continue;
+		}
+		if(!read_cmd(proc[i])){
+			free(proc[i]->path);
+			free(proc[i]);
+			continue;
+		}
+		//Affiche
+		printps(proc[i]);
+		//Supprime
+		free(proc[i]->path);
+		free(proc[i]->cmd);
+		free(proc[i]);
+	}
+	free(proc);
+	return EXIT_SUCCESS;
+}

+ 4 - 2
mysh.c

@@ -20,6 +20,7 @@
 #include "mysh.h"
 #include "ipc.h"
 #include "execute.h"
+#include "color.h"
 
 /* --- Extern --- */
 extern Error error;
@@ -94,7 +95,7 @@ int main(int argc, char* argv[], char* envp[]) {
         error.exit_err();
     }
     //Preparation affichage
-    sprintf(before, "\x1b[32m%s:\x1b[36m", getlogin());
+    sprintf(before, GREEN "%s:" CYAN, getlogin());
     //Traitement des signeaux
     result = sigemptyset(&sigs_new);
     if(result == ERR){	
@@ -129,7 +130,7 @@ int main(int argc, char* argv[], char* envp[]) {
                 job--;
     	}
         //Affichage repertoire
-        show_current_dir(before, ">\x1b[0m ");      
+        show_current_dir(before, ">" RESET " ");      
         //Lecture ligne
         if(get_line(line) == SHELL_ERR){
             //error.print("Impossible de lire les commandes\n");
@@ -334,6 +335,7 @@ void handler(int sig){
         return;
     }
     //Sinon demande comfirmation pour finir le programme
+    printf("\n");
     while(reponse != 'o' && reponse != 'O' && reponse != 'n' && reponse != 'N'){
         //Recup la valeur
         printf("Voulez vous vraiment quitter ? [O/N] ");

+ 26 - 0
str.h

@@ -3,9 +3,35 @@
 
 #include <string.h>
 
+/**
+ * Supprime les espaces avant et après une chaine
+ * @param char* La chaine à modifier
+ * @return La nouvelle chaine
+ */
 char* trim(char*);
+
+/**
+ * Supprime une suite d'un char avant et après une chaine
+ * @param char* La chaine à modifier
+ * @param char Le char à supprimer
+ * @return La nouvelle chaine
+ */
 char* mtrim(char*, char);
+
+/**
+ * Supprime une suite d'un char avant une chaine
+ * @param char* La chaine à modifier
+ * @param char Le char à supprimer
+ * @return La nouvelle chaine
+ */
 char* ltrim(char*, char);
+
+/**
+ * Supprime une suite d'un char après une chaine
+ * @param char* La chaine à modifier
+ * @param char Le char à supprimer
+ * @return La nouvelle chaine
+ */
 char* rtrim(char*, char);
 
 #endif /* STR_H */