#bugfix: problemi tra Laravel e curl

cURL è una nota libreria per trattare gli url e gestire chiamate http.

Utilizzo una libreria per Laravel che mi permette di fare delle richieste HTTP da server a sever che si chiama ixudra/curl.

La libreria funziona a meraviglia, se non che, per un problema di configurazione del mio SO, ogni volta che aggiorno la libreria, cURL smette di funzionare.

Il sintomo

Il sintomo del problema che Laravel esibisce è questo

Symfony\Component\Debug\Exception\FatalThrowableError
Call to undefined function Ixudra\Curl\curl_init()

In realtà il problema risiede ancora più in basso poiché nemmeno il client PHP è immune dagli effetti di questa errata configurazione; invocando semplicemente il client da riga di comando ho:

$ php -v
PHP Warning:  PHP Startup: Unable to load dynamic library 'curl.so' (tried: /usr/lib/php/20190902/curl.so (/usr/lib/php/20190902/curl.so: symbol curl_mime_addpart version CURL_OPENSSL_4 not defined in file libcurl.so.4 with link time reference), /usr/lib/php/20190902/curl.so.so (/usr/lib/php/20190902/curl.so.so: cannot open shared object file: No such file or directory)) in Unknown on line 0
PHP 7.4.6 (cli) (built: May 14 2020 10:02:44) ( NTS )
...

Il problema

La radice del problema si trova nella directory /usr/local/lib

$ cd /usr/local/lib/
$ ll
totale 12924
drwxr-xr-x  8 root root     4096 mag 27 08:48 ./
drwxr-xr-x 13 root root     4096 ott 17  2018 ../
-rw-r--r--  1 root root  1006444 mag 23  2017 libcurl.a
-rwxr-xr-x  1 root root     1042 mag 23  2017 libcurl.la*
lrwxrwxrwx  1 root root       16 mag 21 19:12 libcurl.so.4 -> libcurl.so.4.4.0*
-rwxr-xr-x  1 root root   542272 mag 23  2017 libcurl.so.4.4.0*
...

La “sconfigurazione” è dovuta alla presenza del link simbolico libcurl.so.4, la soluzione proposta è quella di interromperlo perché prenda il sopravvento la versione di sistema. Il fatto è che ogni volta che viene eseguito un aggiornamento di cURL, viene ripristinato anche questo link simbolico reintroducendo l’errore.

La soluzione

Nell’attesa di escogitare una soluzione più definitiva ho seguito il consiglio che si dava nel forum:

$ sudo mv libcurl.so.4.4.0 libcurl.so.4.4.0.OLD

Così facendo non ho più il problema al client:

$ php -v
PHP 7.4.6 (cli) (built: May 14 2020 10:02:44) ( NTS )
Copyright (c) The PHP Group
Zend Engine v3.4.0, Copyright (c) Zend Technologies
    with Zend OPcache v7.4.6, Copyright (c), by Zend Technologies
    with Xdebug v2.9.5, Copyright (c) 2002-2020, by Derick Rethans

Come si vede, l’errore è scomparso. Affinche anche Apache possa beneficiare della soluzione del problema occorre riavviarlo:

$ sudo service apache2 restart

A questo punto anche l’applicazione Laravel torna a funzionare.

Laravel avanzato: l’utilizzo di cron

Ho bisogno di eseguire un determinato metodo di un controller regolarmente una volta al giorno.

Avevo fatto un programma bash e l’avevo inserito nella crontab, ci dovevo fare un aggiornamento quotidiano del mio DB da una sorgente dati.

Questo database mi serve per una applicazione live, per cui devo rendermi autonomo da operazioni manuali.

Avrei potuto replicare la modalità anche in remoto, ma ho preferito farlo attraverso Laravel, tirando dentro all’applicazione tutta la logica eseguita nello script di shell.

Un primo livello è stato quello di scrivere dei programmi PHP che leggevano la sorgente e aggiornavano il db attraverso un nuovo metodo dei miei controller dedicati a quelle entità relazionali.

Quindi potevo lanciare manualmente invocando un URL la funzionalità e tutto filava meravigliosamente.

L’ultimo passo è rendere autonoma l’applicazione Laravel perché ad una certa ora del giorno esegua il task. Qui arriva l’utilissimo Scheduler di Laravel.

L’idea di fondo è quella di invocare in modalità cron la rotta URI che scatena il metodo del controller. Questa funzionalità di Laravel si basa comunque sul clock del sistema operativo: un cron batch che viene lanciato ogni minuto e che viene preso come riferimento da Laravel per temporizzazioni diverse. Quindi è comunque obbligatorio agire su due fronti:

  • sullo scheduler di Laravel e
  • sul crontab di sistema.

Vediamo come si fa.

Estendere Artisan creando un comando che chiama una rotta

Premetto che sto utilizzando questa versione di Laravel:

$ php artisan --version
Laravel Framework 6.18.8

Possiamo estendere le funzionalità di artisan che usiamo correntemente come ad esempio:

$ php artisan make:controller InvoiceController

per fare in modo che si possa invocare un nostro comando artisan; quindi la prima cosa da fare è creare un nuovo comando artisan che chiamo route:call. Ho bisogno inoltre che a questo comando venga affiancata una serie di parametri opzionali.

Esistono già dei comandi artisan per gestire le rotte; se provo a lanciare il mio comando ancora da definire ho questa risposta:

$ php artisan route:call
  Command "route:call" is not defined.
  Did you mean one of these?
      route:cache
      route:clear
      route:list

Per creare un nuovo comando artisan si fa così:

$ php artisan make:command RouteCall

Laravel creerà un nuovo comando che consiste in una classe che si chiamerà come il primo parametro (RouteCall) e la sintassi (signature) sarà definita all’interno del file contenente la nuova classe, che viene creata dentro la cartella app/Console/Commands ed estende la classe Command:

<?php

namespace App\Console\Commands;

use Illuminate\Console\Command;
use Illuminate\Support\Facades\Request;

class RouteCall extends Command
{
    /**
     * The name and signature of the console command.
     *
     * @var string
     */
    protected $signature = 'route:call {--uri=}';

    /**
     * The console command description.
     *
     * @var string
     */
    protected $description = 'Call route from CLI';

    /**
     * Create a new command instance.
     *
     * @return void
     */
    public function __construct()
    {
        parent::__construct();
    }

    /**
     * Execute the console command.
     *
     * @return mixed
     */
    public function handle()
    {
        $request = Request::create($this->option('uri'), 'GET');
        $this->info(app()['Illuminate\Contracts\Http\Kernel']->handle($request));
    }
    
    protected function getOptions()
    {
        return [
            ['uri', null, InputOption::VALUE_REQUIRED, 'The path of the route to be called', null],
        ];
    }

}

In particolare osserviamo la sintassi del comando contenuta nella proprietà $signature che è quella che abbiamo definito nel creare la class nel comando sopra a cui però ho aggiunto il parametro {--uri} che è l’URI del metodo da chiamare. All’interno della classe interessata aggiungerò anche una gestione del parametro in modo tale da renderlo opzionale: in pratica il parametro è una data in formato YYYY-MM-DD: se non la passo nell’URI, sottointendo data odierna.

Il metodo chiave è handle() che trasforma un comando Linux in una request HTTP. Come si vedrà adesso il controllo passa al modulo Kernel.php.

Schedulazione del comando

Il secondo passaggio è quello di definire la temporizzazione del comando:

<?php
// file app/Console/Kernel.php

namespace App\Console;

use Illuminate\Console\Scheduling\Schedule;
use Illuminate\Foundation\Console\Kernel as ConsoleKernel;

class Kernel extends ConsoleKernel
{
    /**
     * The Artisan commands provided by your application.
     *
     * @var array
     */
    protected $commands = [
        'App\Console\Commands\CallRoute',
    ];

    /**
     * Define the application's command schedule.
     *
     * @param  \Illuminate\Console\Scheduling\Schedule  $schedule
     * @return void
     */
    protected function schedule(Schedule $schedule)
    {
        $schedule->command('call:route')->dailyAt('19:00');
    }

    /**
     * Register the commands for the application.
     *
     * @return void
     */
    protected function commands()
    {
        $this->load(__DIR__.'/Commands');

        require base_path('routes/console.php');
    }
}

Il nodo centrale è il metodo schedule() che contiene uno o più comandi, ognuno con la temporizzazione desiderata, in questo caso voglio che il Kernel dello scheduler avvii il comando route:call ogni giorno alle 19:00.

Avvio dello scheduler

La terza operazione è quella di creare una nuova voce nella crontab che avvia lo scheduler:

$ crontab -e
...
# For more information see the manual pages of crontab(5) and cron(8)
# 
# m h  dom mon dow   command
* * * * * cd path/to/project/ && php artisan schedule:run >> /dev/null 2>&1

questo job è chiamato con la risoluzione massimo di 1 volta al minuto, poi la logica di Laravel farà in modo che i singolo job vengano eseguiti su temporizzazioni diverse – come sono state definite nel metodo Kernel->schedule()– ma sempre con il clock di riferimento dettato dalla crontab.

Quindi riassumendo, a partire dalla causa primaria, fino all’effetto ultimo:

  • crontab invoca lo scheduler di Laravel ogni minuto;
  • lo scheduler controlla se c’è quacosa da eseguire in base alle diretive everyMinute(), dailyAt(), daily() eccetera definite per i comandi invocati nel metodo schedule() del Kernel;
  • il Kernel avvia tutti i comandi registrati nel metodo schedule();
  • ognuno di questi metodi è invocato con un comando artisan(es. route:call) ma in generale potrebbe anche essere una chamata a Eloquent per scrivere qualcosa nel database;
  • il singolo comando artisan è definito in una classe specifica (es. RouteCall) contenuta nella directory app/Console/Commands/
  • il metodo che esegue quanto si desidera (nel mio caso la trasformazione di un comando shell come php artisan route:call in una request HTTP) è il metodo handle()

È tutto.

Creare ed eliminare una singola tabella database con Laravel

In fase di progetto, o ancora più spesso in fase di prototipazione, ci si trova a dover ridisegnare una tabella (ad esempio per dover modificare il tipo dei campi o la dimensione, o aggiungere / togliere campi).

E spesso non è nemmeno richiesto di conservare una storia di queste modifiche perché semplicemente si sta modellando una versione finale non ancora soddisfacente.

Quindi invece di procedere con modifiche puntuali sul campo, ad esempio con un equivalente di

ALTER TABLE RENAME COLUMN myField AS Field1;

è più immediato fare un drop e ricreare la tabella, eventualmente salvando i dati o preparando uno script di alimentazione da lanciare all’occorrenza.

Sebbene si possano effettuare entrambe le operazioni (più soft con l’alter table o più drastica con il drop/create) vediamo come si fa a “droppare” e ricreare la tabella con i comandi di migrazione di Laravel.

Lo script di creazione tabella contiene già per default i due metodi per costruire e distruggere la tabella, rispettivamente up() e donw():

<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

class CreateDatiRegioniTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('dati_regioni', function (Blueprint $table) {
            $table->bigIncrements('id');
            ...
         });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('dati_regioni');
    }
}

Il metodo up() viene invocato quando si esegue la migrazione:

$ php artisan migrate

ed il file ad essere eseguito è l’ultimo (il più recente) che si trova nella directory database/migrations. Per droppare la tabella si esegue:

$ php artisan migrate:rollback

e successivamente, per ricrearla:

$ php artisan migrate

Per eseguire operazioni su una determinata tabella, individuare il file contente le migrazioni per quella tabella e modificarne il nome in modo tale che la stringa sia in senso lessicografico la più grande di tutte

2020_04_17_154056_create_regioni_table.php 
2020_04_17_155500_create_dati_regioni_table.php

Impiego della tecnica push-to-deploy con Git

Questo articolo molto vecchio l’ho trovato originariamente nel blog di Kris Jordan ma mi è tornato utile adesso. Lo accomodo per il caso di mio interesse.

Ho un progetto in un host locale, un ambiente di produzione e un ambiente di staging al quale vorrei applicare in tempo reale le modifiche appena consegnate (commit) alla versione di sviluppo.

Questo aticolo, per arrivare a questo obiettivo, propone di creare un repository “bare” in remoto che serve solo per fare da tramite tra cartella di sviluppo in localhost e la cartella di deploy/stage in remoto, come illustrato nella figura 1

Fig. 1 Illustrazione della tecnica push – post – receive

Questa metodologia è stata inizalmente sviluppata da Heroku.

Preparazione dei repository

In questa versione personalizzata, a differenza dell’articolo di riferimento (nel quale si mette tutto in localhost):

  • lasceremo il repository git “myProject” in localhost;
  • creeremo il repository git “push” in mydomain.com
  • la directory “stage” ovviamente in mydomain.com

come illustrato in Fig. 1.

me@localhost:$ cd IdeaProjects/myProject

Ora dobbiamo spostarci in remoto e qui è indispensabile avere l’accesso shell al server.

Qui inizializziamo un repository detto “bare” (nudo) che avrà solo la funzione di passamano tra il Git locale e la directory di deploy in remoto

me@localhost:$ ssh myUser@mydomain.com
Password:
myUser@remotehost:$ git init --bare myProjectPush.git

Questo repository myProjectPush.git non conterrà il codice sorgente, bensì farà da passamano tra il client Git a bordo del nostro server di sviluppo e la cartella di staging “stage” nel server di staging

Ora nel Git locale dobbiamo creare un riferimento al repository remoto di push che chiameremo “push” (magari non è la scelta più felice… ma basta fare attenzione):

me@localhost:$  git remote add push myUser@mydomain.com:myProjectPush.git

Il nuovo repository remoto lo troviamo scritto nel file .git/config:

...
[remote "push"]
        url = myUser@mydomain.com:public_html/myProjectPush.git
        fetch = +refs/heads/*:refs/remotes/push/*

A questo dobbiamo istruire il Git remoto perché copi i file nella directory stage

Impostazione Push-to-Stage

Dopo aver creato il repository Git remoto dobbiamo scrivere un programma/script che istruisce git su come trattare i file che arrivano dal Git locale. Nella directory hooks della cartella myProjectPush.git ci sono gli script che Git lancia al verificarsi di un particolare evento. Ce ne sono già parecchi di esempio scritti in linguaggio bash, ma per questo scopo si può usare un qualsiasi linguaggio, nell’esempio che ho trovato è scritto in Ruby. Ma prima di tutto creiamo il file:

myUser@remotehost:$ cd myProjectPush.git/.git/hooks
myUser@remotehost:$ touch post-receive
myUser@remotehost:$ chmod +x post-receive

Il “gancio” che ci serve è quello che ci collega all’evento post-receive che è l’evento che accade quando il repository remoto riceve e accetta un push da parte di un repository di sviluppo. Quindi è necessario che il sistema operativo veda il file come uno script eseguibile.

Poi apriamo con l’editor che vogliamo il file post-receive e copiamo incolliamo questo programma Ruby (accertiamoci che l’interprete ruby sia installato con un comando “which ruby”)

#!/usr/bin/env ruby
# post-receive

# 1. Read STDIN (Format: "from_commit to_commit branch_name")
from, to, branch = ARGF.read.split " "

# 2. Only deploy if master branch was pushed
if (branch =~ /master$/) == nil
    puts "Received branch #{branch}, not deploying."
    exit
end

# 3. Copy files to deploy directory
deploy_to_dir = File.expand_path('../stage')
`GIT_WORK_TREE="#{deploy_to_dir}" git checkout -f master`
puts "DEPLOY: master(#{to}) copied to '#{deploy_to_dir}'"

# 4.TODO: Deployment Tasks
# i.e.: Run Puppet Apply, Restart Daemons, etc

Vediamo cosa fa questo script:

  1. Quando viene attivato lo script post-receive, Git gli passa in STDIN tre parametri: ID commit HEAD precedente, ID commit HEAD presente e il nome del branch da trasferire. Assegnamo questi tre valori alle variabili from, to e branch.
  2. Facciamo in modo che vengano pushati solo commit sul master branch (quello principale); questo sarebbe ragionevole per un push finale verso un server di produzione, ma io adotterò questa tecnica solo per lo staging, quindi rilasso questa parte (posso fare anche push di branch di bugfix per esempio).
  3. La prossima cosa da fare è fare un checkout dal branch attuale alla cartelle “stage”, cioè una copia dei file fisici nella cartella stage.
  4. Il lavoro è finito; sarebbe possibile completare lo script con comandi che dovessero rendersi necessari, come un restart del web server (se ne abbiamo la facoltà) o cancellare file di cache. Ma intanto mi fermo qui.

Salviamo il file e vediamo cosa succede.

Test con Pushing

Possiamo testare facendo un commit e un push verso il remote “push”:

me@localhost:$ git commit -m 'Test push-to-stage'
me@localhost:$ git push push master
muser@mydomain.com's password: 
Counting objects: 510, done.
Delta compression using up to 4 threads.
Compressing objects: 100% (477/477), done.
Writing objects: 100% (510/510), 18.16 MiB | 148.00 KiB/s, done.
Total 510 (delta 260), reused 0 (delta 0)
remote: Already on 'master'
remote: DEPLOY: master(915c38ce6cd49905ccff2abf4d9fea9a6ad61a3d) copied to '/home/myUser/domains/mydomain.com/public_html/stage'
To mydomain.com:public_html/myProjectPush.git
 * [new branch]      master -> master

Nell’output, le righe che iniziano con “remote:” sono quelle generate del nostro script! In particolare vediamo che è stata fatta la copia nella cartella stage.

That’s all folks!!

Monitoraggio COVID-19

Ho realizzato questo semplice cruscotto che riporta sia in forma tabellare che grafica (per solo alcuni indicatori) i dati sull’epidemia di COVID-19.

La situazione viene aggiornata verso le 18 di ogni giorno con i nuovi dati in arrivo dal Dipartimento della Protezione Civile.

Note

Si noti il grafico della derivata seconda: questo da’ una stima dell’inversione di tendenza: quando la derivata seconda è negativa, la curva tende a trivolgere la concavità verso il basso (o la “pancia” verso l’alto), cioè il contagio rallenta (pur continuando ad aumentare).

L’inversione di tendenza come si vede si è verificata più volte in modo anche più pronunciato di questi giorni però ha avuto carattere occasionale. In questi giorni invece la derivata secondna si mantiene negativa per il terzo giorno, anche se di poco, ad indivduare forse una tendenza più stabile.

Link all’applicazione.

Pillole MySQL – come l’utility apparmor può darci problemi

Situazione: ho due schemi database. Attenzione: nello stesso server.

Nel primo importo i dati in una tabella da un file csv utilizzando l’istruzione LOAD DATA senza problemi.

Nel secondo voglio fare la stessa cosa, sullo stesso server, ma ottengo questo errore:

Error Code: 1290. The MySQL server is running with the --secure-file-priv option so it cannot execute this statement

Cioò che dice la documentazione di MySQL e ciò che trovo in tutti i forum è di modificare il file di configurazione

/etc/mysql/my.cnf

per aggiungere una riga che specifichi l’opzione secure-file-priv, per dichiarare qual è la directory preposta ai file da caricare, ma ci sono due problemi

  • l’opzione è già specificata e anche se metto i file in quella cartella, l’errore non scomapre; anche se cambio la cartella e riavvio il server (e posiziono i file csv in quest’ultima cartella), l’errore non scompare
  • il server è lo stesso

Il problema è l’applicazione apparmor di cui ho già parlato in un altro articolo. In sostanza la soluzione era far rileggere ad apparmor il suo file di configurazione (operazione che di tanto intanto si rende necesssaria per cui ho raccolto i comandi uno script Bash):

#!/bin/bash
apparmor_parser -r /etc/apparmor.d/usr.sbin.mysqld
service mysql stop
service mysql start

E tutto va a posto. Non mi spiego ancora, però, come fosse possibile eseguire il LOAD DATA per uno schema e non per un’altro.

Stay tuned.

Pillole PHP: PHP + Oracle

Questo è un compito piuttosto ingrato perché ogni volta trovo delle novità. Come regola aurea non utiizzo PECL (PHP Extensions Communiti Library) perché qualcosa va regolarmente storto. Scarico i sorgenti della Oracle Call Interface (oci) e compilo a mano: funziona e si fa prima

$ mkdir ~/Scaricati/php/ & cd ~/Scaricati/php/
$ wget https://pecl.php.net/get/oci8-2.2.0.tgz
$ tar -xzvf oci8–2.2.0.tgz
$ cd oci8–2.2.0
$ phpize
$ ./configure — with-oci8=instantclient,/usr/lib/oracle/12.2/client64/lib
...
$ make
$ sudo make install

Per non modificare altri parametri di configurazione, sostituisco il file oggetto appena ottenuto al posto di quello di riferimento

$ sudo mv /usr/lib/php/20190902/oci8.so oci8.so.old
$ sudo cp modules/oci8.so /usr/lib/php/20190902/

Ora configuriamo Apache perché carichi questa libreria: non serve (come scritto in altri tutorial) modificare i file /etc/php/7.4/*/php.ini, ma è sufficiente aggiungere il modulo nella directory /etc/php/7.4/mods-available:

$ less /etc/php/7.4/mods-available:/oci8.ini
; configuration for php OCI8 module
; priority=20
extension=/usr/lib/php/20190902/oci8.so

Ultimo passaggio: modificare le variabili di ambiente di Apache perché ritrovi le librerie:

$ sudo nano /etc/apache2/envvars
...
[Aggiungere]
export PATH=”/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/home/marcob/Scaricati/php/oci8-2.2.0/modules:/usr/lib/oracle/12.2/client64:/usr/lib/oracle/12.2/client64/lib
export LD_LIBRARY_PATH=/usr/lib/oracle/12.2/client64/lib
#export ORACLE_HOME=/usr/lib/oracle/12.2/client64

A questo punto riavvio il servizio

$ sudo service apache2 restart
$

E provo a far girare lo script che doveva leggere SYSDATE da una istanza Oracle e stamparlo su una pagina web:

SQL = select sysdate from dual
PRM = marcob/marcob@(DESCRIPTION=(ADDRESS_LIST=(ADDRESS=(PROTOCOL=TCP)(HOST=orahost.myserver.it)(PORT=1521)))(CONNECT_DATA=(SERVICE_NAME=oradb)(SERVER=DEDICATED)))
Oracle correctly connected!

/var/www/html/oracle/index.php:8:
array (size=1)
  0 => 
    array (size=1)
      'SYSDATE' => string '06-FEB-20' (length=9)

Per fare un po’ di sintesi, tre sono le directory importanti per il funzionamento di questa estensione:

PathFile
/usr/lib/oracle/12.2/client64/lib Instant client nativi Oracle
/usr/lib/php/20190902/libreria Oracle client compilata
/etc/php/7.4/configurazione per agganciare il client ai comandi (riga di comando, Apache, Fast CGI)

E anch’io termino con un bel

Fine!

Pillole PHP: gestire le estensioni

Per vedere dove sono installate le estensioni (i codici oggetto .so che realizzano i plugin come i connettori a database, l’uso delle funzioni cURL etc):

marcob@jsbach[12:15:08]:php$ php-config --extension-dir
/usr/lib/php/20190902

Per abilitare il modulo curl per la versione 7.4

$ phpenmod -v 7.4 curl

Ho però un problema, l’installazione della libreria curl mi ha provocato una serie di errori che vengono stapati ad ogni invocazione dell’interprete php client o ad ogni riavvio di Apache:

root@jsbach[11:26:44]:apache2$ php -v
PHP Warning:  PHP Startup: Unable to load dynamic library 'curl' (tried: /usr/lib/php/20190902/curl (/usr/lib/php/20190902/curl: cannot open shared object file: No such file or directory), /usr/lib/php/20190902/curl.so (/usr/lib/php/20190902/curl.so: symbol curl_mime_addpart version CURL_OPENSSL_4 not defined in file libcurl.so.4 with link time reference)) in Unknown on line 0
PHP Warning:  PHP Startup: Unable to load dynamic library 'curl.so' (tried: /usr/lib/php/20190902/curl.so (/usr/lib/php/20190902/curl.so: symbol curl_mime_addpart version CURL_OPENSSL_4 not defined in file libcurl.so.4 with link time reference), /usr/lib/php/20190902/curl.so.so (/usr/lib/php/20190902/curl.so.so: cannot open shared object file: No such file or directory)) in Unknown on line 0
PHP 7.4.2 (cli) (built: Feb  1 2020 17:49:29) ( NTS )
Copyright (c) The PHP Group<br> Zend Engine v3.4.0, Copyright (c) Zend Technologies     with Zend OPcache v7.4.2, Copyright (c), by Zend Technologies with Xdebug v2.9.1, Copyright (c) 2002-2020, by Derick Rethans

Seguo questo howto su Github

root@jsbach[11:25:47]:apache2$ sudo ldd /usr/lib/php/20190902/curl.so
/usr/lib/php/20190902/curl.so: /usr/local/lib/libcurl.so.4: no version information available (required by /usr/lib/php/20190902/curl.so)
    linux-vdso.so.1 (0x00007fffea95f000)<br>
    libcurl.so.4 => /usr/local/lib/libcurl.so.4 (0x00007f5924bd3000)

Come evidenziato dal comando esiste un conflitto tra la versione di libcurl installata in passato (/usr/local/lib/libcurl.so.4) e quella nuova (/usr/lib/php/20190902/curl.so); decido di rinominare quella vecchia:

root@jsbach[11:34:45]:apache2# cd /usr/local/lib/
root@jsbach[11:35:58]:lib# ls -l | grep 'curl'
-rw-r--r-- 1 root root  1006444 mag 23  2017 libcurl.a
-rwxr-xr-x 1 root root     1042 mag 23  2017 libcurl.la
lrwxrwxrwx 1 root root       16 mag 23  2017 libcurl.so -> libcurl.so.4.4.0
lrwxrwxrwx 1 root root       16 mag 23  2017 libcurl.so.4 -> libcurl.so.4.4.0
-rwxr-xr-x 1 root root   542272 mag 23  2017 libcurl.so.4.4.0
root@jsbach[11:36:04]:lib# mv libcurl.so.4.4.0 libcurl.so.4.4.0.OLD
root@jsbach[11:38:53]:lib# ldd /usr/lib/php/20190902/curl.so
	linux-vdso.so.1 (0x00007fff9610f000)
	libcurl.so.4 => /usr/lib/x86_64-linux-gnu/libcurl.so.4 (0x00007f8d92a92000)
	libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f8d926a1000)

Adesso il comando ldd (List Dynamic Library, che ci dice quali sono le librerie collegate ad un executable) non mostra più l’errore

root@jsbach[11:38:53]:lib# ldd /usr/lib/php/20190902/curl.so
	linux-vdso.so.1 (0x00007fff9610f000)
	libcurl.so.4 => /usr/lib/x86_64-linux-gnu/libcurl.so.4 (0x00007f8d92a92000)
	libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f8d926a1000)
...

Altro problema. Eseguento il comando php -m (o qualsiasi comando che usa l’interprete php client) ho un Warning:

root@jsbach[11:55:43]:lib# php -m
PHP Warning:  Module 'curl' already loaded in Unknown on line 0
[PHP Modules]
calendar
Core
ctype
curl
date

Il problema è che carico due volte la libreria, una specificando nominalmente il modulo curl nelle extensions

root@jsbach[12:10:32]:cli# pwd
/etc/php/7.4/cli
root@jsbach[12:10:34]:cli# cat php.ini | grep curl
extension=curl
[curl]
;curl.cainfo =

e una a causa della presenza del file singolo di load nelle configurazioni:

root@jsbach[12:11:00]:cli# ll conf.d/ | grep curl
lrwxrwxrwx 1 root root   36 feb  4 16:23 20-curl.ini -> /etc/php/7.4/mods-available/curl.ini

Commento la riga extensions=curl del file /etc/php/7.4/cli/php.ini. Il comando ora da un ouput pulito:

root@jsbach[12:11:45]:cli# php -m
[PHP Modules]
calendar
Core
ctype
curl
date
...

Anche il riavvio di Apache ora è pulito:

marcob@jsbach[12:15:23]:php$ sudo service apache2 restart
marcob@jsbach[12:24:48]:php$ 

Inoltre ora phpinfo() mostra il modulo correttamente caricato:

Modulo cURL correttamente caricato

Pillole di GNOME

Ho dovuto fabbricarmi un allarme visivo per una attività che devo svolgere a intervalli regolari durante la giornata.

Ho realizzato questo compito utilizzando

  • Gnome
  • cron

Gnome

Gnome open source free spftware linux desktop environment
Gnome open source free software linux desktop environment

Gnome è uno degli ambienti grafici desktop utiizzati da molte distribuzioni Linux (RedHat, Ubuntu, Fedora e altri) per gestire il sistema grafico a finestre. Esso si basa sul server X (come gli altri sistemi, ad esempio KDE). Non ho sviluppato molto in questo ambiente tipicamente desktop, essendomi occupato più di web.

In ogni caso lo trovo molto divertente; in pratica si programma in Javascript e si usa uno speciale interprete che utilizza le API del server, gjs.

Ho preso spunto dal questo tutorial (Gnome developer) ed l’ho personalizzato. In sostanza si tratta di aprire una finestra di X da linea di comando: essa contiene una semplice immagine e un messaggio, l’aspetto è questo:

Finestra che si apre eseguendo il programma Gnome listato qui sotto

Il codice Javascript è questo di seguito, lo descrivo sommariamente, per i dettagli si veda il tutorial di Gnome:

#!/usr/bin/gjs

imports.gi.versions.Gtk = '3.0';
const Gtk = imports.gi.Gtk;

class MyGrid {
    // Crea l'applicazione
    constructor() {
        this.application = new Gtk.Application();

        // Collega i segnali 'activate' e 'startup' alle funzioni di callback
        this.application.connect('activate', this._onActivate.bind(this));
        this.application.connect('startup', this._onStartup.bind(this));
    }

    // La funzione di callback per il segnale 'activate' presenta la finestra quando attiva
    _onActivate() {
        this._window.present();
    }

    // La funzione di callback per il segnale 'startup' costruisce la UI
    _onStartup() {
        this._buildUI ();
    }

    // Costruisci la UI dell'applicazione
    _buildUI() {

        // Crea la finestra applicativa
        this._window = new Gtk.ApplicationWindow({
            application: this.application,
            window_position: Gtk.WindowPosition.CENTER,
            border_width: 10,
            title: "Ora di bere!"});

        // Crea una immagine
        this.path = '/home/marcob/gnome';
        this._image = new Gtk.Image ({ file: this.path + '/bottiglia.png' });

        // Crea una etichetta
        this._label = new Gtk.Label ({ label: "Questo per ricordarti di bere!" });

        // Crea la griglia
        this._grid = new Gtk.Grid ();

        // Posiziona immagine ed etichetta nella griglia
        this._grid.attach (this._image, 0, 0, 1, 1);
        this._grid.attach (this._label, 0, 1, 1, 1);

        // Aggiungi la griglia alla finestra
        this._window.add (this._grid);

        // Mostra la finestra e tutti gli oggetti figli
        this._window.show_all();
    }

};

// Lancia l'applicazione
let app = new MyGrid ();
app.application.run (ARGV);

Leggiamo brevemente il sorgente: con il preambolo importiamo le funzioni dello Gnome Toolkit (GTK+) e definiamo la classe MyGrid che ci consente di progettare la finestra come una griglia.

Si collegano (bind) gli eventi a dei metodi che definiamo subito dopo (present() e _buildUI()).

Il core del programma è il metodo this._buildUI() che costruisce la User Interface utilizzano i metodi della libreria GTK+:

  • si definisce una nuova finestra GTK: new Gtk.ApplicationWindow();
  • si definiscono varie proprietà come posizione e titolo;
  • si definiscono una immagine caricata da file system e un titolo della finestra più altre proprietà geometriche;
  • si definisce la griglia all’interno della quale collocare gli oggetti (new Gtk.Grid ());
  • e si posizionano gli oggetti (this._grid.attach());
  • Finalmente si istanzia questa nuova classe e si lancia il metodo application.run();

Se si vogliono maggiori dettagli si veda il tutorial di Gnome

Ora questo programmino si può semplicemente lanciare da shell

$ gjs native.js

Il passo successivo è eseguirlo in automatico con periodicità (ad esempio ogni 20 minuti). Per fare questo utilizzo cron.

Modifico il mio cron personale tramite il comando

$ crontab -e

Il contenuto del file è il seguente (mi limito alla riga che definisce il comando):

# 
# m h  dom mon dow   command
0,20,40 /usr/bin/gjs /home/marcob/gnome/native.js

Questa impostazione provoca il ripetersi del comando ogni venti minuti.

Tuttavia incappo in un problema: in realtà il programma non parte e viene generato un errore nel syslog

Unable to init server: Impossibile connettersi: Connessione rifiutata

(gjs:25833): Gtk-WARNING **: 16:00:01.480: cannot open display: 

Questo è perché il processo cron esegue i comandi in uno spazio “virtuale” che non ha display. Quindi occorre indicare a cron quale display usare:

# 
# m h  dom mon dow   command
0,20,40 export DISPLAY=:0 && /usr/bin/gjs /home/marcob/gnome/native.js

L’indicazione della direttiva export DISPLAY=:0 serve per poter agganciare il server X. Il risultato è quello voluto ed io sono forzato a bere un po’ d’acqua ogni venti minuti per evitare la ritenzione idrica! 🙂

Pillole Python/Linux: pretty printing json files

Esaminando il contenuto di un file json con il comando less, mi veniva tutto su una linea

[{"id":"5d070cce-a504-4375-94c3-403f71eb4212","name":"prod36.mirth.mysite.it:8443","address":"https://prod36.mirth.mysite.it:8443","javaHome":"BUNDLED","heapSize":"512m","icon":"egyptian_pyramid.png","showJavaConsole":false,"sslProtocolsCustom":false,"sslProtocols":"","sslCipherSuitesCustom":false,"sslCipherSuites":"","useLegacyDHSettings":false},{"id":"7431b71c-7a2d-4be6-a7f1-6f1129e3a8ce","name":"https://test36.mirth.mysite.it:8443","address":"https://test36.mirth.mysite.it:8443","javaHome":"BUNDLED","heapSize":"512m","icon":"pepper.png","showJavaConsole":false,"sslProtocolsCustom":false,"sslProtocols":"","sslCipherSuitesCustom":false,"sslCipherSuites":"","useLegacyDHSettings":false}]

Piuttosto scomodo. Avrei potuto aprirlo con un editor con filtri di interpretazione, ma ho trovato questo metodo comodissimo:

$ less connections.json | python -m json.tool 

Il risultato è molto più confortevole

[
    {
        "address": "https://prod36.mirth.mysite.it:8443",
        "heapSize": "512m",
        "icon": "egyptian_pyramid.png",
        "id": "5d070cce-a504-4375-94c3-403f71eb4212",
        "javaHome": "BUNDLED",
        "name": "prod36.mirth.sanita.padova.it:8443",
        "showJavaConsole": false,
        "sslCipherSuites": "",
        "sslCipherSuitesCustom": false,
        "sslProtocols": "",
        "sslProtocolsCustom": false,
        "useLegacyDHSettings": false
    },
    {
        "address": "https://test36.mirth.mysite.it:8443",
        "heapSize": "512m",
        "icon": "pepper.png",
        "id": "7431b71c-7a2d-4be6-a7f1-6f1129e3a8ce",
        "javaHome": "BUNDLED",
        "name": "https://test36.mirth.sanita.padova.it:8443",
        "showJavaConsole": false,
        "sslCipherSuites": "",
        "sslCipherSuitesCustom": false,
        "sslProtocols": "",
        "sslProtocolsCustom": false,
        "useLegacyDHSettings": false
    }
]