Aller au contenu

Détecter les erreurs et protéger l'exécution d'un script LUA avec pcall()


Messages recommandés

Posté(e)

Détecter les erreurs et protéger l'exécution d'un script LUA avec pcall()

 

 

Il existe déjà un vieux sujet de @Shad, mais je vais essayer d'être un peu plus exhaustif, en prenant en compte les nouveautés apportées par les scènes sur HC2 puis les QuickApp sur HC3 : l"utilisation de la librairie net.HTTPClient() et l'exécution asynchrone du code LUA.

 

Autre sujet détaillant l'utilisation de net.HTTPClient() à lire au préalable :

 

 

Durant l'exécution d'un script LUA, une erreur peut survenir, susceptible de planter le script, celui-ci s'arrête alors brutalement et la suite du code n'est jamais exécutée.

 

Je paraphrase l'explication de @Krikroff :

 

Pour faire simple: La fonction pcall() permet l’exécution du code en mode "protégé" ou "encapsulé", c'est à dire qu'il ne lèvera pas d' erreur dans le processus de votre box si jamais le code provoquait une erreur. Ainsi, le fil d'exécution des Scènes et des QuickApps est protégé.

 

Aussi à savoir: pcall() retourne true ou false en fonction de la réussite du code mais peut aussi retourner un résultat issu de la fonction en utilisant la méthode interne error().

 

La fonction pcall() peut-être utilisée pour faire en LUA l'équivalent du try...catch pour ceux qui connaissent.

 

Des exemples ici pour comprendre :

 

 

 

Exemple n°1 : protection de http:request()

 

Le premier usage de pcall() est pour protéger l'exécution de la fonction http:request() car celle-ci peut planter, par exemple si l'URL est mal formée :

local http = net.HTTPClient()
local url = "http://192.168.1.1/chemin/page?argument=valeur"

local status, err = pcall(function()

	http:request(url, {
		success = function(response)
			-- Suite des traitements...
		end,
		error = function(err)
			-- Gestion de l'erreur (connexion impossible)
		end,
		options = {
			-- options éventuelles...
		}
	}) -- http:request()

end) -- pcall()

if not status then
	-- Gestion de l'erreur attrapée par pcall()
	print(err)
end

 

 

 

Exemple n°2 : protection de json.decode()

 

De plus, pcall() est également très utile (voire indispensable) pour une autre fonction qui a la fâcheuse habitude de planter : json.decode() si le JSON donné en argument est mal formaté.

Exemple :

local status, jsonTable = pcall(function() return json.decode(response.data) end)
if status then
	-- Suite des traitements...
else
	print(jsonTable or "json.decode() failed")
end

Dans cet exemple, la variable jsonTable contiendra soit le tableau décodé (résultat de json.decode()), soit le message d'erreur (résultat de pcall())

 

 

 

Exemple n°3 : protection complète de http:request() et json.decode()

 

Par ailleurs, il faut noter que dans le premier exemple avec http:request(), les fonctions success() et error() sont des fonctions de callback appelées après l'exécution de la requête, donc elles sont asychrones.

De ce fait, leur contenu n'est plus protégé par la fonction pcall().

Par conséquent, si on combine les 2 exemples précédents, à savoir la requête HTTP, puis le décodage du résultat JSON, cela donne une structure de code comme suit :

local http = net.HTTPClient()

local status, err = pcall(function()

	http:request(url, {
		success = function(response)

			local status, jsonTable = pcall(function() return json.decode(response.data) end)
			if status then
				-- Suite des traitements...
			else
				print(jsonTable or "json.decode() failed")
			end

		end,
		error = function(err)
			-- Gestion de l'erreur (connexion impossible)
		end,
		options = {
			-- options éventuelles...
		}
	}) -- http:request()

end) -- pcall()

if not status then
	-- Gestion de l'erreur attrapée par pcall()
	print(err)
end

De cette façon, le code LUA est parfaitement protégé.

 

 

 

Exemple n°4 : interruption conditionnelle de l'exécution avec assert()

 

La fonction assert() permet de tester une condition. Si la résultat est false, dans ce cas elle déclenche l'erreur qui sera attrapée par pcall() :

local http = net.HTTPClient()

local status, err = pcall(function()

	-- Ici mon code s'exécute et effectue plein d'actions...

	local device = api.get("/devices/127")
	assert(type(device) == "table", "Le module 127 est introuvable")

	-- Suite du code si tout se passe bien...

end) -- pcall()

if not status then
	-- Gestion de l'erreur attrapée par pcall()
	print(err)
end

Dans cet exemple j'ai testé si le résultat d'un appel à api.get() s'est bien passé, mais on pourrait tester n'importe quel autre cas de figure.

 

 

 

Exemple n°5 : interruption inconditionnelle de l'exécution avec error()

 

La fonction error() permet de forcer le déclenchement d'une erreur qui sera attrapée par pcall() :

local http = net.HTTPClient()

local status, err = pcall(function()

	-- Ici mon code s'exécute et effectue plein d'actions...

	if ma_condition then
		error("Un message d'erreur")
	end

	-- Suite du code si tout se passe bien...

end) -- pcall()

if not status then
	-- Gestion de l'erreur attrapée par pcall()
	print(err)
end

 

 

 

J'espère que ce petit tutoriel sera utile, vous pouvez maintenant utiliser pcall() dans vos code, combiner les différents exemples ci-dessus, etc.

 

 

  • Like 6
  • 2 semaines après...
Posté(e) (modifié)

Hello,

J'ai lu attentivement cela et je l'ai implémenté au sein d'un QA.

Je me pose une question sur l'utilisation des variables déclarées "local status, err": doit-on les déclarer en self.status et self.err ?

-> Serait-il possible de donner un exemple dans le contexte des QA, si on reprend l'exemple 3 donné ci-dessus (protection de http:request() + json.decode() ) ?

 

Second point qui n'est pas clair pour moi: si dans le init du QA on appel une fonction du QA avec un SetTimeout (pour boucler sur des http:requests et les exécuter chaque x secondes), si un appel pète, avec le pcall cela permettra au QA de reboucher et poursuivre son exécution en boucle?

 

Merci

Modifié par razowski
Posté(e)

Non en local c'est le mieux, il n'y a aucune raison de mettre ces variables dans l'objet quickApp (désigné par self)

En effet, si le contenu du pcall() crash, cela sera intercepté et traité dans le code LUA juste en dessous (if not status then ...), donc on est bien dans le même "scope" de portée des variables locales.

 

De manière générale, une bonne pratique en programmation est de toujours réduire au maximum la portée des variables utilisées.

Ici la variable n'est exploitée qu'en locale, donc déclaration avec local xxx.

Si à l'inverse on aurait eu besoin de traiter les variables dans une autre fonction du QuickApp, alors on peut au choix passer la variable en argument lors de l'appel de la fonction, ou bien si la variable est utilisée par plusieurs fonctions (potentiellement toutes les fonctions du QuickApp), alors on affecte cette variable à l'objet quickApp (donc déclaration avec self.xxx)

 

Je ne vois pas le besoin de donner un exemple dans le contexte d'un QA, car c'est bien le sens de mon tuto (il est même plus général que ça en fait).

Le code que tu vas protéger avec pcall(), il est forcément dans une fonction. Que cette fonction fasse partie du QuickApp, que ça soit une fonction locale, une fonction globale, ou bien encore une fonction d'un autre objet, cela ne change absolument rien à la syntaxe décrite dans le tuto.

Bref, tu rajoutes ceci et tu as ton exemple :

function QuickApp:maChereFonction(avec, de, beaux, arguments)

-- ici n'importe lequel de mes exemples avec pcall()

end

 

 

Ta 2nde question est pertinente, j'ai oublié de le préciser dans le tuto, même si on peut le deviner quand je parle d'asynchronisme.

Le setTimeout appelle une fonction en asynchrone (même en mettant un délai = 0), du coup tout ce qui s'exécute après n'est plus protégé par le pcal() ... C'est exactement comme pour l'appel de la fonction http:request(), la fonction success() appelle en asynchrone n'est pas protégée par le pcall(), donc on emploie un nouveau pcall() pour protéger le code LUA à l'intérieur, comme je l'ai montré avec le json.decode()

 

Donc dans le cas d'une boucle infinie, on appelle dont une fonction à intervalle régulier avec fibaro.setTimeout(), par conséquent c'est le code de la fonction appelée qui doit contenir le pcall(), et non pas le code de la fonction appelante.

Cela dit, une boucle infinie, elle s'appelle elle-même, du coup, bah.... il y a un pcall() à chaque fois.

Exemple plus loin pour une meilleure compréhension.
 

Mais avant, il faut aussi savoir que la fonction onInit() devrait faire le minimum de choses et rendre la main le plus vite possible au système.
Cela implique que cette fonction onInit() doit appeler un minimum de fonction en synchrone, puis continuer l'exécution en asynchrone.

 

Cela étant dire, un exemple de squelette de code :

function QuickApp:onInit()

	-- Ici on effectue quelques initialisations : affichage de log, chargement des variables du QA, initialisaiton de variables, etc
	self:debug("Salut")
	self.refreshInterval = tonumber(self:getVariable("Refresh")) or 60

	-- Maintenant on se prépare à rendre la main au système, donc on va lancer la boucle infinie en asynchrone :
	fibaro.setTimeout(0, function() self:loop() end)
	-- On constate que dans cet exemple, on appelle la loop avec un délai à 0.
	-- Donc elle s'exécutera tout de suite, mais en ashynchrone.
	-- Cela permet de rendre la main au système qui va pouvoir finir l'initilisation du QA juste avant d'exécuter loop()
end

function QuickApp:loop()

	local status, err = pcall(function()

		-- Ici le code fait tout plein de choses utiles
		-- ...

	end)

	if not status then
		-- Gestion de l'erreur attrapée par pcall()
		self:error(err)
	end

	-- Prochaine boucle ena asynchrone dans 1 minute (60000 millisecondes)
	fibaro.setTimeout(math.floor(self.refreshInterval*1000), function() self:loop() end)

end

Ne pas oublier que si la loop() appelle d'autres fonctions en asynchrone, alors celles-ci ne seront plus protégées par le pcall() de la loop()

 

Mais si elle appelle des fonctions en synchrone, alors elles seront bien protégées.

Exemples de fonctions synchrones :

- toutes les fonctions natives du LUA : string.* math.*, etc

- potentiellement n'importe laquelle des autres fonctions de notre QuickApp. Mais je dis bien potentiellement, car on peut aussi vouloir appeler ces autres fonctions en asynchrone, selon l'effet recherché.

  • Like 1
Posté(e)

@Lazer, juste histoire de polémiquer un peu ;) :

 

il y a 5 minutes, Lazer a dit :

Si à l'inverse on aurait eu besoin de traiter les variables dans une autre fonction du QuickApp, alors on peut au choix passer la variable en argument lors de l'appel de la fonction, ou bien si la variable est utilisée par plusieurs fonctions (potentiellement toutes les fonctions du QuickApp), alors on affecte cette variable à l'objet quickApp (donc déclaration avec self.xxx)

Totalement d'accord avec ton propos, aussi, dans ton exemple, il est inutile d'utiliser une variable self.refreshInterval, quand une variable refreshInterval déclarée locale avant la déclaration de la QuickApp:onInit().

 

Dans le même ordre d'idée pour limiter la portée des variables, à quoi bon déclarer function QuickApp:loop() quand function loop(self) permet le même usage en restreignant, de plus, la visibilité de la fonction loop à du code externe, se qui ne peut que concourir à la robustesse du code ?

Posté(e)

Merci pour les retours. Il faut que je prenne du temps au calme pour me mettre dedans mais je pense que c’est clair dans mon esprit maintenant.

C’est dans tous les cas un sujet intéressant...

Bonne soirée

Posté(e)

N'hésite pas à poser des questions, et expérimenter par toi même.

Il est aussi très instructif de créer volontairement des erreurs dans le code pour voir comment ça réagit.

 

 

@Barelle

 

Oui tu as raison pour le premier point, on peut aussi passer l’intervalle en argument de la fonction loop()

 

Quant aux fonctions locales c'est vrai aussi car cela évite d'exposer les fonctions aux appels externes, cela dit ça complexifie significativement le code LUA. En effet, plus question d'utiliser self, ou alors il faut le passer en argument, ou bien encore appeler directement l'objet quickApp (sans la majuscule... QuickApp est la classe, quickApp est l'objet instancié à partir de la classe)

Mais tout cela est beaucoup trop compliqué pour le néophyte, je pense que ce sont des notions réservées aux développeurs un minimum expérimentés.

En tout cas c'est l'une des raisons pour lesquelles j'utilise beaucoup les fichiers des QA, j'y range ce que j'appelle des librairies, qui permettent d'une part de "cacher" les fonctions qui ne sont plus exposées dans le QuickApp, mais également de mieux structurer le code, de facilité de réemploi dans d'autres QuickApp (à la façon programmation objet), et de faciliter les migrations futures. Si le QuickApp disparait ou est remplacé à nouveau par autre chose, seule la partie interface IHM sera à réécrire, tout le reste sera repris tel quel. C'est le principe que j'avais commencé à adopter dans mes derniers développements sur HC2, si bien que j'ai migré mon QA Onduleur Eaton en un temps record, bien qu'il soit le plus complexe de tous ceux que j'ai réalisé : ma librairie SNMP a été migrée quasiment telle quelle, à quelques détails près.

Posté(e)

Oui on peut passer l'intervalle en paramètre, mais cela est inutile s'il est déclaré local au quickapp, étant visible par l'ensemble du code.

 

Pour la seconde partie de ta réponse il me semble que tu crées de la confusion entre la visibilité des variables et la structure modulaire du code qui me semble être deux notions distinctes :

  • Déclarer QuickApp:fonction(a, b) ou fonction(self, a, b), la seconde écriture demande moins de caractères... Et lors de l'appel self:fonction(a,b) et totalement comparable à fonction(self, a, b). Encore faut avoir le besoin du self dans la fonction.
  • L'utilisation de fichiers pour améliorer la modularité du code est un choix du développeur, pour avoir par le passé travailler sur des fichiers sources de plus d'un million de lignes, si ceuc-ci sont bien structurés, cela ne présente pas de difficultés particulières. Je crois que ce choix de la multitude de petits fichiers (pas de de 1000 lignes par fichier, une fonction ne doit pas dépasser 24 lignes...) est issue du monde Unix à l'époque de machines ne disposant que de mémoire dont la taille était exprimée en ko (dans le même ordre d'idée l'emploi d'accolades pour délimiter les blocs en langage C, en lieu et place des BEGIN et END de l'Algol et de ses dérivés - Pascal, Modula 2... - n'avait pour objectif que l'économie de mémoire pour l'éditeur).
    Je suis septique quand à la différence de rapidité du copier/coller entre un fichier complet et une partie de fichier, je pense que dans le second cas, on est plus vigilant.
Posté(e)

Tiens, je ne connaissais pas du tout ces (anciennes ?) règles de 1000 lignes max par fichier / 24 lignes par fonctions.

 

Disons que pour moi, séparer en fichiers, me permet de structurer mon code et surtout faciliter le réemploi dans les autres QA. Je copie/colle un fichier complet sans réfléchir, c'est plus facile, je trouve, que de concaténer les multiples fonctions à la suite.

 

il y a 6 minutes, Barelle a dit :

mais cela est inutile s'il est déclaré local au quickapp, étant visible par l'ensemble du code

 

...

 

Et lors de l'appel self:fonction(a,b) et totalement comparable à fonction(self, a, b).

Je rebondit sur ces 2 phrases qui sont liées indirectement.

 

Si tu déclares 3 fonctions ainsi :

function xxx()
end

local function y()
end

function QuickApp:zzz()
end

On aura :

  • La 1ère :
    • accessible dans tout le code (tous les fichiers du QA)
    • sera globale, donc plus lente d'accès (LUA devrait parcourir la super table _G pour la retrouver) (ça peut devenir un critère important s'il s'agit d'une fonction appelée souvent dans une boucle, ou pire encore dans le cas d'une récursivité)
  • La 2nde :
    • accessible uniquement dans le fichier en cours du QA
    • sera locale, donc très rapide d'accès
  • La 3ème :
    • sera globale, c'est à dire appelable avec self:zzz() depuis une fonction appartenant également à QuickApp, mais également depuis n'importe où dans le code si self a été passé en paramètre ou bien en appelant l'objet instancié quickApp:zzz() (sans la majuscule initiale donc)
    • sera moyennement rapide si appelée depuis QuickApp (car on recherche un élément de la table self en cours d'utilisation) et sera lente en cas d'appel depuis ailleurs car on va rechercher la variable globale quickApp

Donc il y a quand même quelques petites différences, qui peuvent être importantes dans certains cas.

Après on peut facilement contourner le "problème" de la relative lenteur des variables/fonctions globales, en les redéclarant localement :

local mafonctionlocale = quickApp.zzz

 

Posté(e)

Je suis d'accord sur tout sauf sur 

il y a 17 minutes, Lazer a dit :

Je copie/colle un fichier complet sans réfléchir,

N'oublions pas le fameux vol d'Ariane 5, je cite wikipedia : 

Citation

Un copier-coller a été utilisé afin de dupliquer des données volumétriques (qui avaient valu leur succès selon leur optique scientifique), mais pourtant, Ariane 5 était beaucoup plus vaste et plus lourde.

 

Posté(e)

Bon bah je suis grillé, je ne serai jamais embauché chez Space X alors

 

Quoi que... et si..... les multiples explosions à l’atterrissage... ? :98:

  • Haha 1
Posté(e)

Une dernière remarque, si l'on déclare une fonction locale, elle n'est visible dans un bloc de niveau inférieur que si elle est déclarée avant. Ainsi :

 

Ce code fonctionne :

local function toto(self)
    self:trace("Je suis Toto");
end

function QuickApp:onInit()
    toto(self);
end

Quand celui-ci ne fonctionne pas :

function QuickApp:onInit()
    toto(self);
end

local function toto(self)
    self:trace("Je suis Toto");
end

On obtient l'erreur :

[18.03.2021] [22:07:23] [ERROR] [QUICKAPP92]: QuickApp crashed
[18.03.2021] [22:07:23] [ERROR] [QUICKAPP92]: main.lua:20: attempt to call a nil value (global 'toto')

Il convient alors d'écrire :

local toto;

function QuickApp:onInit()
    toto(self);
end

local function totofct(self)
    self:trace("Je suis Toto");
end
toto = totofct;

Pour obtenir le bon fonctionnement de cet indispensable QA. :wacko:

Posté(e)

Merci de ce rappel, toutefois, quand une fonction n'est pas déclarée locale, elle est visible depuis l'intérieur des autres fonctions déclarées avant. Alors que comme le montre mon exemple ce n'est plus vrai dans le cas d'une fonction locale qui ne devient visible que de l'intérieur des fonctions déclarées après.

Après vérification, ce comportement se retrouve pour tous les types de variables, ce qui est parfaitement logique.

  • 4 mois après...
Posté(e)

je me pose la question suivante, suite à des essais : (alors j'ai peut-être pas tout compris - en fait c'est certain :))

 

est ce que c'est logique de mettre un pcall() dans ce cas : (j'ai ultra abrégé le code...)

 

ce code est dans un QA dit "appelant"...

il exécute une fonction nommée "myFunc" dans un QA dit "appelé" avec comme ID : myID

function QuickApp:mainLoop()

	local status, err = true, ""
	
	status, err = pcall(function() fibaro.call(myID, "myFunc") end)
	
	if not status then self:error("Detail error : ", err) end

	setTimeout(function() self:mainLoop() end, self.timeLoop)
	
end

Je comprends pas trop l'intérêt de ce pcall() ici.

 

Car quand je fais ce qu'il faut pour faire planter la fonction du QA appelée (donc myFunc()), l'erreur vient bien du QA appelé (myID) !

Dans le QA appelant, j'ai rien... donc status = true ??

Logiquement, je ne devrais pas avoir status = false et l'erreur qui remonte dans le QA appelant via la variable "err" ?

 

La mainLoop continue de tourner...

 

Merci pour vos lumières...

Posté(e)

If you do

fibaro.call(self.id,"test") 

the QA will crash if

function QuickApp:test() ...end 

throws an error.

The pcall will not protect it as the fibaro.call itself doesn't return an error. It's the test function that crashes and kills the QA (and that function is in the same QA as the fibaro.call code). However "test" runs in another thread so pcall doesn't catch it.

 

If you call another QA the fibaro.call always return true unless the id is non-existent. On the other hand the other QA may crash if it has an error.

 

To make things more confusing. Setting

__fibaroUseAsyncHandler(false)

it will hang for a while and still crashes fibaro.call(self.id,"test")

but doesn't crash the whole QA....

Posté(e)
il y a 1 minute, jang a dit :

The pcall will not protect it as the fibaro.call itself doesn't return an error

ok !!

il y a 2 minutes, jang a dit :

However "test" runs in another thread so pcall doesn't catch it.

I thought it ;) 

 

Thanks for explains.

I understand...

 

Posté(e)
Il y a 1 heure, jang a dit :

__fibaroUseAsyncHandler(false)

it will hang for a while and still crashes fibaro.call(self.id,"test")

but doesn't crash the whole QA....

Interesting... what would be a use case for disabling asynchronous calls ?

Is there any side effect affecting functions other than fibaro.call ?

Posté(e)
il y a 43 minutes, Lazer a dit :

Interesting ... what would be a use case for disabling asynchronous calls?

Is there any side effect affecting functions other than fibaro.call?

No it only seems to affect fibaro.call(self.id,"...",...)

There was complaints that the QuickApp hung for ~5s when doing that (QAs are single-threaded so it blocked itself) so they added __fibaroUseAsyncHandler(bool) and set it to true by default. I assume they check if it's the same QA and in that case call it in another setTimeout thread.

I'm not sure how really useful that was, as one should call your own QA methods with self:...

 

I would have preferred that they fixed the whole fibaro.call mechanism as it causes hard to detect bugs in people's code - and we got fibaro.calls that could return values .... as outlined here

https://forum.fibaro.com/topic/49113-hc3-quickapps-coding-tips-and-tricks/?do=findComment&comment=233772

 

  • 3 mois après...
Posté(e)

Bon deux fois que je lis ce post. Pas évident…

J’y retourne car ça m’intéresse de comprendre et l’appliquer à mes QA :D

Surtout l’exemple 3 :P

×
×
  • Créer...