- Tener instalado:
-
- El editor VSCode. Viene en la VM pero se recomienda actualizar a la ultima version son soporte a 32bit
-
- La extensión C/C++ de VSC
-
- (Altamente recomendado) CUnit para no desvelarse retesteando a mano el dia anterior a la entrega 😄
-
- GNOME Terminal (sudo apt-get install gnome-terminal)
- Hacerse un buen cafecito antes de empezar
Proceso 1, 2, y 3 representan N procesos, y todos se setean de la misma forma. Se recomienda seguir los pasos para un solo proceso, copiar y pegar en los N procesos necesarios, y luego hacer un find and replace de los nombres. Shared sin embargo es distinto y debe hacerse siguiendo los pasos.
makefile:35: *** missing separator. Stop.
Es porque Markdown alteró los tabs. Necesitas sacar el espacio de las acciones, poner tab en cada uno, y probar nuevamente.
Primero creamos una carpeta para cada uno de los procesos que nos piden, más una llamada 'shared' donde va a estar el código compartido. (Vamos a hacer una trampita: no es realmente una biblioteca compartida.)
proceso1 proceso2 proceso3 shared
Vamos a abrir VSCode e ir a Archivo->Abrir Carpeta, y seleccionamos todas estas carpetas, con shift. Luego hacemos archivo->guardar área de trabajo como... y se nos va a crear un archivo .code-workspace en la ruta del TP.
La proxima vez que lo queramos abrir, vamos a hacer Archivo->Abrir Área de Trabajo, seleccionando ese .code-workspace, y nos va a abrir todas las carpetas de una con sus configuraciones.
En cada una de estas carpetas, creamos las subcarpetas:
obj src include cfg .vscode
En src van a estar nuestros .c, en include nuestros .h, en cfg nuestras configuraciones y logs, y en obj van a estar los .o (no vamos a interactuar con éstos). Shared no necesita cfg.
.vscode es una carpeta de VSC que va a ser más útil en breve.
A continuación, vamos a copiar el siguiente makefile en cada uno de los procesos excepto shared, reemplazando el valor de PROCESS_NAME con el de nuestro proceso
PROCESS_NAME=proceso1
IDIR =./include
SHARED_IDIR = ../shared/include
CC=gcc
CFLAGS=-I$(IDIR) -I$(SHARED_IDIR) -g -Wall
ODIR=./obj
SRCDIR =./src
LIBS=-lcommons -lpthread -lreadline -lcunit -lrt
SHARED_SRCDIR = ../shared/src
SHARED_ODIR = ../shared/obj
DEPS = $(wildcard $(IDIR)/*.h)
SRC = $(wildcard $(SRCDIR)/*.c)
OBJ = $(patsubst $(SRCDIR)/%.c,$(ODIR)/%.o,$(SRC))
$(ODIR)/%.o: $(SRCDIR)/%.c $(DEPS) $(SHARED_DEPS)
$(CC) -c -o $@ $< $(CFLAGS)
$(SHARED_ODIR)/%.o: $(SHARED_SRCDIR)/%.c $(SHARED_DEPS)
$(CC) -c -o $@ $< $(CFLAGS)
SHARED_DEPS = $(wildcard $(SHARED_IDIR)/*.h)
SHARED_SRC = $(wildcard $(SHARED_SRCDIR)/*.c)
SHARED_OBJ = $(patsubst $(SHARED_SRCDIR)/%.c,$(SHARED_ODIR)/%.o,$(SHARED_SRC))
$(PROCESS_NAME): $(OBJ) $(SHARED_OBJ)
$(CC) -o $@ $^ $(CFLAGS) $(LIBS)
.PHONY: clean
clean:
rm -f $(ODIR)/*.o *~ core $(INCDIR)/*~ $(PROCESS_NAME) $(SHARED_ODIR)/*.o
Los makefiles constan de macros y reglas. Las macros tienen el formato:
MACRO=valor
y podemos poner paths, comandos, nombres, includes y mucho más, evitando repetirlo después y referenciándolo con el formato $(MACRO). A su vez, los makefiles tienen reglas, que tienen el formato:
target: dependencias
acción
Donde acción debe tener un TAB sí o sí. Target es el nombre de la regla que podemos usar en la terminal (make clean, make install, etc), Dependencias son las que necesito para ejecutar la acción, como los .o que necesito para armar el ejecutable. Acción es el comando que quiero ejecutar, como gcc con los argumentos y las dependencias. Este makefile lo hice siguiendo esta guía de la prehistoria y si querés modificarlo a tu gusto te invito a leer la documentación.
LIBS es un macro donde van las librerías que vamos a usar. -lcommons es la de la cátedra, -cunit es para los tests unitarios (optativo), y los demás los necesité en su momento. Agregá o sacá de acuerdo a tus necesidades.
CFLAGS es un macro donde van los flags al compilar. -g es necesario para ejecutar en VSCode en modo debug. -Wall es para mostrar todos los warnings. Te recomiendo no sacar ninguno.
Este makefile nos va a permitir que el procesoN compile utilizando los .c y .h del proceso más los que están en la shared. Acá está la trampita; no es una biblioteca compartida sino un lugar donde los procesos saben que tienen que ir a buscar el resto de los archivos.
- Vamos a estar agregando archivos extra al compilado cuando quizá no las necesitemos, haciendo que 'make' haga de más.
- Actualmente no permite subcarpetas (Ej. ./src/misubcarp/foo.c no va a funcionar)
- Principalmente ésta.
- Ya no nos tenemos que preocupar por la configuración rompiendo el repo de nuestros compañeros, porque usamos paths relativos para todo.
- Vamos a estar usando el ejecutable para correr en VSCode, así que si anda en VSC anda en la terminal.
En /src/ vamos a crear nombreproceso.c y en /include/ nombreproceso.h.
#include "proceso1.h"
int main(){
t_log* logger = log_create("./cfg/proceso1.log", "PROCESO1", true, LOG_LEVEL_INFO);
log_info(logger, "Soy el proceso 1! %s", mi_funcion_compartida());
log_destroy(logger);
}
#ifndef PROCESO1_H
#define PROCESO1_H
#include <stdio.h>
#include <commons/log.h>
#include <stdbool.h>
#include "shared_utils.h"
#endif
Podes ver la docu si no entendés el código. Igualmente lo vas a ver en el TP0.
Vamos a agregar dos archivos más en la carpeta shared
#ifndef SHARED_UTILS_H
#define SHARED_UTILS_H
#include <stdio.h>
#include <commons/log.h>
#include <stdbool.h>
char* mi_funcion_compartida();
#endif
#include "shared_utils.h"
char* mi_funcion_compartida(){
return "Hice uso de la shared!";
}
Si hacemos make vamos a ver que se nos creó el ejecutable. Hurra!
>make && ./proceso1
make: 'proceso1' is up to date.
[INFO] 17:14:17:572 PROCESO1/(16977:16977): Soy el proceso 1! Hice uso de la shared!
Seguro te está desmotivando tener que hacer make y ./miproyecto cada vez que querés probar tu programa. Y el botoncito de Eclipse para correr con valgrind (o de manera normal!) ni está.
'Me están cagando'. Dijo, mientras perdía la esperanza.
Vamos a crear dos shell scripts en cada proceso, cambiando el valor de FILE por el nombre de nuestro proceso:
#!/bin/bash
FILE=proceso1
make $FILE
if test -f "./$FILE"; then
./$FILE
fi
#!/bin/bash
FILE=proceso1
make $FILE
if test -f "./$FILE"; then
valgrind --tool=memcheck --leak-check=yes --show-possibly-lost=no --show-reachable=no --num-callers=20 ./$FILE
fi
A partir de esto, en la terminal podemos escribir ./exec o ./vexec para Compilar y Ejecutar el proceso en un solo paso, ya sea normalmente o con valgrind. Optativamente podemos agregar un script para helgrind:
#!/bin/bash
FILE=proceso1
make $FILE
if test -f "./$FILE"; then
valgrind --tool=helgrind ./$FILE
fi
Si queremos que esté más ordenado, podemos agregar --log-file=a.out antes de ./$FILE y así lo importante va a irse a un archivo aparte
Para ejecutar estos scripts, pero vamos a tener que darnos permiso de ejecución. Para esto vamos a escribir
chmod 777 exec
y así con vexec y hexec. Pero en VSC todavía nos faltan algunas cosas. Nota: Ni se les ocurra hacer esto fuera de la VM o del TP
Habrás observado que, si bien compila y todo, VSCode te subraya en verde algunos imports. Esto es porque, si bien vos, yo y el makefile sabemos dónde están los headers (.h), VSCode no tiene ni idea de cómo organizamos el proyecto.
Dentro de /.vscode/, creamos el archivo c_cpp_properties.json con el siguiente contenido:
{
"configurations": [
{
"name": "Linux",
"includePath": [
"${workspaceFolder}",
"/usr/include/c++/5",
"/usr/include/i386-linux-gnu/c++/5",
"/usr/include/c++/5/backward",
"/usr/lib/gcc/i686-linux-gnu/5/include",
"/usr/local/include",
"/usr/lib/gcc/i686-linux-gnu/5/include-fixed",
"/usr/include/i386-linux-gnu",
"/usr/include",
"${workspaceFolder}/include",
"../shared/include"
],
"defines": ["_GNU_SOURCE"],
"intelliSenseMode": "clang-x86",
"browse": {
"path": [
"${workspaceFolder}",
"${workspaceFolder}/include",
"../shared/include",
"/usr/include/c++/5",
"/usr/include/i386-linux-gnu/c++/5",
"/usr/include/c++/5/backward",
"/usr/lib/gcc/i686-linux-gnu/5/include",
"/usr/local/include",
"/usr/lib/gcc/i686-linux-gnu/5/include-fixed",
"/usr/include/i386-linux-gnu",
"/usr/include"
],
"limitSymbolsToIncludedHeaders": true,
"databaseFilename": ""
},
"compilerPath": "/usr/bin/gcc",
"cStandard": "c11",
"cppStandard": "c++17"
}
],
"version": 3
}
Con esto le hacemos saber a VSC que tiene que buscar en el /include/ del proyecto y en el /include/ de la shared. Los warnings en verde deberían irse.
Todo bien, pero me dijiste que podía usar VSCode y tengo que usar exec, vexec y no sé qué. Al final sigo usando la terminal.
Vamos a la parte de Debug de VSC (bichito a la izquierda en el IDE), y arriba donde aparece "DEPURAR" vamos a abrir el combo box y seleccionar Agregar Configuración (procesoN). Seleccionamos C++ (GDB/LLDB), y "Launch".
Se nos va a abrir launch.json en /.vscode/ . Vamos a cambiar (gdb) Launch en name por Proceso1, y lo de program por ${workspaceFolder}/proceso1
Ahora ya podemos Debuggear nuestro proyecto! Si vas al bichito nuevamente y seleccionas Proceso1, apretando en el triángulo verde o f5 vas a poder debuggear. Podés poner breakpoints para ver lo que se imprime por consola y demás. Tené en cuenta que debuggear no compila tu aplicación, así que lo vas a tener que hacer manualmente. Despues del proximo paso vamos a ver como arreglar eso
Ahora te toca repetir esto mismo con los cuatro procesos. De esta forma vas a poder debuggear cualquiera de los cuatro.
Pero eso no es todo...
Vamos a ir a Tareas-> Configurar Tareas->Crear archivo tasks.json desde plantilla (proceso1)->Others
Se nos abre un tasks.json donde vamos a poder decirle a VSCode qué queremos hacer. Vamos a agregar tres tareas: una para compilar, una para ejecutar normal y otra para valgrind (4 si querés hacer para helgrind). Reemplazamos los labels por el nombre de cada tarea, shell se queda igual, y en command vamos a poner ./exec o ./vexec respectivamente. Nos queda algo así:
{
"version": "2.0.0",
"tasks": [
{
"label": "Make",
"type": "shell",
"command": "make",
"problemMatcher": [
"$gcc"
]
},
{
"label": "Proceso1 Normal",
"type": "shell",
"command": "./exec"
},
{
"label": "Proceso1 Valgrind",
"type": "shell",
"command": "./vexec"
}
]
}
Ahora, podemos ir a Tareas->Ejecutar Tarea->Proceso1 Normal->(gcc) para ejecutarlo directo desde el IDE, o Proceso1 Valgrind para ejecutarlo con Valgrind! Ah y se compila automáticamente cada vez que lo corrés 😄
Qué fiaca. son como 3 clicks.
Vamos a ir a Archivo->Preferencias->Métodos abreviados de teclado y apenas abajo del buscador hacemos click en keybindings.json.
Ahora vamos a linkear nuestras tareas creadas a keybinds. Copiamos lo siguiente:
[
{
"key": "ctrl+h",
"command": "workbench.action.tasks.runTask",
"args": "Proceso1 Normal"
},
{
"key": "ctrl+j",
"command": "workbench.action.tasks.runTask",
"args": "Proceso1 Valgrind"
},
]
En vez de ctrl+h y ctrl+j ponés tu combinación favorita. Command se queda como está, y en args ponemos el nombre exacto de la tarea, tal como aparece en label dentro de tasks.json.
Pumba! Ahora con una combinación de teclas podemos ejecutar el proceso las veces que queramos, desde el IDE. No más terminal, no más Eclipse, todos felices.
Podés hacer esto mismo con el resto de los procesos y decirle a tus compañeros que se hagan sus propios keybinds (ya que es probable que una persona se dedique a un sólo proceso en el TP)
Ahora tambien podemos hacer que se ejecute la tarea de compilacion que creamos cuando empezamos a debuggear. Abrimos para todos los procesos el archivo launch.json y agregamos esta linea:
"preLaunchTask": "Make",
Seré honesto contigo Lisa, jamás hice las pruebas.
Nadie te lo va a pedir. Nadie se lo va a esperar cuando caigas a la entrega con tests unitarios.
No lo hagas por la nota. Hacelo porque realmente te vas a ahorrar tiempo, frustración y noches de desvelo tratando de encontrar errores.
Si ya estás laburando te habrás dado cuenta de lo difícil que es tocar código de un programa que no tiene documentación ni tests, y de la bronca que da no entender por qué las cosas no funcionan.
Habiendo dicho eso:
Nuestro makefile ya tiene para usar CUnit. Vamos a hacer las siguientes modificaciones:
#include "proceso1.h"
int main(int argc, char ** argv){
if(argc > 1 && strcmp(argv[1],"-test")==0)
run_tests();
else{
t_log* logger = log_create("./cfg/proceso1.log", "PROCESO1", true, LOG_LEVEL_INFO);
log_info(logger, "Soy el proceso 1! %s", mi_funcion_compartida());
log_destroy(logger);
}
}
#ifndef PROCESO1_H
#define PROCESO1_H
#include <stdio.h>
#include <commons/log.h>
#include <stdbool.h>
#include "shared_utils.h"
#include "tests.h"
#endif
Creamos un nuevo archivo "tests.c" y "tests.h" en src e include, que nos quedarían así:
#ifndef TESTS_H
#define TESTS_H
#include <CUnit/Basic.h>
int run_tests();
void suma_proceso1();
#endif
#include "tests.h"
int run_tests(){
CU_initialize_registry();
CU_pSuite tests = CU_add_suite("PROCESO1 Suite",NULL,NULL);
CU_add_test(tests,"Probar Suma", suma_proceso1);
CU_basic_set_mode(CU_BRM_VERBOSE);
CU_basic_run_tests();
CU_cleanup_registry();
return CU_get_error();
}
void suma_proceso1(){
CU_ASSERT_EQUAL(2+2, 4);
}
De esta forma podemos correr ./proceso1 -test para correr nuestros tests. Te recomiendo empezar testeando que se serialicen / deserialicen bien tus mensajes 😄
De la misma forma, podemos ir a nuestro launch.json y en args agregar "-test" para correr nuestros tests en debug. También podemos crear tareas como en las ejecuciones normales y agregarle shortcuts. Lo que te haga feliz.
>./proceso1 -test
CUnit - A unit testing framework for C - Version 2.1-3
http://cunit.sourceforge.net/
Suite: PROCESO1 Suite
Test: Probar Suma ...passed
Run Summary: Type Total Ran Passed Failed Inactive
suites 1 1 n/a 0 0
tests 1 1 1 0 0
asserts 1 1 1 0 n/a
Seguro te dijeron que tenés 15 minutos para setear el proyecto en la entrega. Lo que probablemente no sepas (o te acuerdes) en la entrega es que tenés que instalar la commons y todo lo que uses en la máquina donde entregás! Y probablemente sean varias VMs conectadas por LAN, así que se te pueden ir los tiempos por este detalle. Te va a ser más fácil automatizar esto. Adapté un script de acá para que sea más fácil de usar. Vamos a ir a la carpeta del TP y creamos:
#!/bin/bash
length=$(($#-1))
OPTIONS=${@:1:$length}
REPONAME="${!#}"
CWD=$PWD
echo -e "\n\nInstalling commons libraries...\n\n"
COMMONS="so-commons-library"
git clone "https://github.com/sisoputnfrba/${COMMONS}.git" $COMMONS
cd $COMMONS
sudo make uninstall
make all
sudo make install
cd $CWD
echo -e "\n\nBuilding projects...\n\n"
make -C ./proceso1
make -C ./proceso2
make -C ./proceso3
echo -e "\n\nDeploy done!\n\n"
Reemplazamos en los 'make' por nuestras subcarpetas exceptuando shared que no tiene makefile.
Listo el pollo. Ejecutas esto en la VM nueva y ya estás listo para arrancar.