Je souhaite empêcher ou gérer un StackOverflowException
que j'obtiens d'un appel au XslCompiledTransform.Transform
méthode dans un Xsl Editor
Je suis en train d'écrire. Le problème semble être que l'utilisateur peut écrire un Xsl script
qui est infiniment récursif, et qui explose juste lors de l'appel à la méthode Transform
. (C'est-à-dire que le problème n'est pas seulement l'erreur de programmation typique, qui est généralement la cause d'une telle exception.)
Existe-t-il un moyen de détecter et/ou de limiter le nombre de récursions autorisées? Ou d'autres idées pour empêcher ce code de simplement exploser sur moi?
De Microsoft:
À partir de .NET Framework version 2.0, un objet StackOverflowException ne peut pas être intercepté par un bloc try-catch et le processus correspondant est interrompu par défaut. Par conséquent, il est conseillé aux utilisateurs d'écrire leur code pour détecter et empêcher un débordement de pile. Par exemple, si votre application dépend de la récursivité, utilisez un compteur ou une condition d'état pour terminer la boucle récursive.
Je suppose que l'exception se produit dans une méthode interne .NET, et non dans votre code.
Vous pouvez faire quelques choses.
Vous pouvez utiliser la classe Process pour charger l'assembly qui appliquera la transformation dans un processus distinct et avertir l'utilisateur de l'échec s'il meurt, sans tuer votre application principale.
EDIT: Je viens de tester, voici comment le faire:
Processus principal:
// This is just an example, obviously you'll want to pass args to this.
Process p1 = new Process();
p1.StartInfo.FileName = "ApplyTransform.exe";
p1.StartInfo.UseShellExecute = false;
p1.StartInfo.WindowStyle = ProcessWindowStyle.Hidden;
p1.Start();
p1.WaitForExit();
if (p1.ExitCode == 1)
Console.WriteLine("StackOverflow was thrown");
Processus ApplyTransform:
class Program
{
static void Main(string[] args)
{
AppDomain.CurrentDomain.UnhandledException += new UnhandledExceptionEventHandler(CurrentDomain_UnhandledException);
throw new StackOverflowException();
}
// We trap this, we can't save the process,
// but we can prevent the "ILLEGAL OPERATION" window
static void CurrentDomain_UnhandledException(object sender, UnhandledExceptionEventArgs e)
{
if (e.IsTerminating)
{
Environment.Exit(1);
}
}
}
[~ # ~] note [~ # ~] La question dans la prime de @WilliamJockusch et la question d'origine sont différentes.
Cette réponse concerne StackOverflow dans le cas général des bibliothèques tierces et ce que vous pouvez/ne pouvez pas faire avec elles. Si vous recherchez le cas spécial avec XslTransform, consultez la réponse acceptée.
Les débordements de pile se produisent car les données sur la pile dépassent une certaine limite (en octets). Les détails du fonctionnement de cette détection peuvent être trouvés ici .
Je me demande s'il existe un moyen général de retrouver les StackOverflowExceptions. En d'autres termes, supposons que j'ai une récursion infinie quelque part dans mon code, mais je ne sais pas où. Je veux le retrouver par un moyen plus facile que de parcourir le code partout jusqu'à ce que je le voie. Je me fiche du hack.
Comme je l'ai mentionné dans le lien, la détection d'un débordement de pile à partir de l'analyse de code statique nécessiterait de résoudre le problème d'arrêt qui est indécidable. Maintenant que nous avons établi que il n'y a pas de solution miracle, je peux vous montrer quelques astuces qui, je pense, aident à dépister le problème.
Je pense que cette question peut être interprétée de différentes manières, et comme je m'ennuie un peu :-), je vais la décomposer en différentes variantes.
Détection d'un débordement de pile dans un environnement de test
Fondamentalement, le problème ici est que vous disposez d'un environnement de test (limité) et que vous souhaitez détecter un débordement de pile dans un environnement de production (étendu).
Au lieu de détecter le SO lui-même, je résous ce problème en exploitant le fait que la profondeur de la pile peut être définie. Le débogueur vous donnera toutes les informations dont vous avez besoin. La plupart des langues vous permettent de spécifier la pile taille ou la profondeur de récursivité max.
Fondamentalement, j'essaie de forcer un SO en rendant la profondeur de pile aussi petite que possible. Si elle ne déborde pas, je peux toujours l'agrandir (= dans ce cas: plus sûr) pour la production Dès que vous obtenez un débordement de pile, vous pouvez décider manuellement s'il est "valide" ou non.
Pour ce faire, passez la taille de la pile (dans notre cas: une petite valeur) à un paramètre Thread et voyez ce qui se passe. La taille de pile par défaut dans .NET est de 1 Mo, nous allons utiliser une valeur beaucoup plus petite:
class StackOverflowDetector
{
static int Recur()
{
int variable = 1;
return variable + Recur();
}
static void Start()
{
int depth = 1 + Recur();
}
static void Main(string[] args)
{
Thread t = new Thread(Start, 1);
t.Start();
t.Join();
Console.WriteLine();
Console.ReadLine();
}
}
Remarque: nous allons également utiliser ce code ci-dessous.
Une fois qu'il déborde, vous pouvez le définir sur une valeur plus élevée jusqu'à ce que vous obteniez un SO qui a du sens.
Création d'exceptions avant vous SO
StackOverflowException
n'est pas capturable. Cela signifie que vous ne pouvez pas faire grand-chose une fois que cela s'est produit. Donc, si vous pensez que quelque chose va mal dans votre code, vous pouvez faire votre propre exception dans certains cas. La seule chose dont vous avez besoin pour cela est la profondeur de pile actuelle; il n'y a pas besoin de compteur, vous pouvez utiliser les vraies valeurs de .NET:
class StackOverflowDetector
{
static void CheckStackDepth()
{
if (new StackTrace().FrameCount > 10) // some arbitrary limit
{
throw new StackOverflowException("Bad thread.");
}
}
static int Recur()
{
CheckStackDepth();
int variable = 1;
return variable + Recur();
}
static void Main(string[] args)
{
try
{
int depth = 1 + Recur();
}
catch (ThreadAbortException e)
{
Console.WriteLine("We've been a {0}", e.ExceptionState);
}
Console.WriteLine();
Console.ReadLine();
}
}
Notez que cette approche fonctionne également si vous traitez avec des composants tiers qui utilisent un mécanisme de rappel. La seule chose requise est que vous pouvez intercepter les appels certains dans la trace de la pile.
Détection dans un thread séparé
Vous l'avez explicitement suggéré, alors voici celui-ci.
Vous pouvez essayer de détecter un SO dans un thread séparé .. mais cela ne vous fera probablement rien de bien. Un débordement de pile peut se produire rapide, même avant vous obtenez un changement de contexte. Cela signifie que ce mécanisme n'est pas fiable du tout ... Je ne recommanderais pas réellement de l'utiliser . C'était amusant à construire, alors voici le code :-)
class StackOverflowDetector
{
static int Recur()
{
Thread.Sleep(1); // simulate that we're actually doing something :-)
int variable = 1;
return variable + Recur();
}
static void Start()
{
try
{
int depth = 1 + Recur();
}
catch (ThreadAbortException e)
{
Console.WriteLine("We've been a {0}", e.ExceptionState);
}
}
static void Main(string[] args)
{
// Prepare the execution thread
Thread t = new Thread(Start);
t.Priority = ThreadPriority.Lowest;
// Create the watch thread
Thread watcher = new Thread(Watcher);
watcher.Priority = ThreadPriority.Highest;
watcher.Start(t);
// Start the execution thread
t.Start();
t.Join();
watcher.Abort();
Console.WriteLine();
Console.ReadLine();
}
private static void Watcher(object o)
{
Thread towatch = (Thread)o;
while (true)
{
if (towatch.ThreadState == System.Threading.ThreadState.Running)
{
towatch.Suspend();
var frames = new System.Diagnostics.StackTrace(towatch, false);
if (frames.FrameCount > 20)
{
towatch.Resume();
towatch.Abort("Bad bad thread!");
}
else
{
towatch.Resume();
}
}
}
}
}
Exécutez cela dans le débogueur et amusez-vous de ce qui se passe.
Utilisation des caractéristiques d'un débordement de pile
Une autre interprétation de votre question est: "Où sont les morceaux de code qui pourraient potentiellement provoquer une exception de dépassement de pile?". Évidemment, la réponse est: tout le code avec récursivité. Pour chaque morceau de code, vous pouvez ensuite effectuer une analyse manuelle.
Il est également possible de le déterminer à l'aide d'une analyse de code statique. Ce que vous devez faire pour cela est de décompiler toutes les méthodes et de déterminer si elles contiennent une récursion infinie. Voici un code qui fait cela pour vous:
// A simple decompiler that extracts all method tokens (that is: call, callvirt, newobj in IL)
internal class Decompiler
{
private Decompiler() { }
static Decompiler()
{
singleByteOpcodes = new OpCode[0x100];
multiByteOpcodes = new OpCode[0x100];
FieldInfo[] infoArray1 = typeof(OpCodes).GetFields();
for (int num1 = 0; num1 < infoArray1.Length; num1++)
{
FieldInfo info1 = infoArray1[num1];
if (info1.FieldType == typeof(OpCode))
{
OpCode code1 = (OpCode)info1.GetValue(null);
ushort num2 = (ushort)code1.Value;
if (num2 < 0x100)
{
singleByteOpcodes[(int)num2] = code1;
}
else
{
if ((num2 & 0xff00) != 0xfe00)
{
throw new Exception("Invalid opcode: " + num2.ToString());
}
multiByteOpcodes[num2 & 0xff] = code1;
}
}
}
}
private static OpCode[] singleByteOpcodes;
private static OpCode[] multiByteOpcodes;
public static MethodBase[] Decompile(MethodBase mi, byte[] ildata)
{
HashSet<MethodBase> result = new HashSet<MethodBase>();
Module module = mi.Module;
int position = 0;
while (position < ildata.Length)
{
OpCode code = OpCodes.Nop;
ushort b = ildata[position++];
if (b != 0xfe)
{
code = singleByteOpcodes[b];
}
else
{
b = ildata[position++];
code = multiByteOpcodes[b];
b |= (ushort)(0xfe00);
}
switch (code.OperandType)
{
case OperandType.InlineNone:
break;
case OperandType.ShortInlineBrTarget:
case OperandType.ShortInlineI:
case OperandType.ShortInlineVar:
position += 1;
break;
case OperandType.InlineVar:
position += 2;
break;
case OperandType.InlineBrTarget:
case OperandType.InlineField:
case OperandType.InlineI:
case OperandType.InlineSig:
case OperandType.InlineString:
case OperandType.InlineTok:
case OperandType.InlineType:
case OperandType.ShortInlineR:
position += 4;
break;
case OperandType.InlineR:
case OperandType.InlineI8:
position += 8;
break;
case OperandType.InlineSwitch:
int count = BitConverter.ToInt32(ildata, position);
position += count * 4 + 4;
break;
case OperandType.InlineMethod:
int methodId = BitConverter.ToInt32(ildata, position);
position += 4;
try
{
if (mi is ConstructorInfo)
{
result.Add((MethodBase)module.ResolveMember(methodId, mi.DeclaringType.GetGenericArguments(), Type.EmptyTypes));
}
else
{
result.Add((MethodBase)module.ResolveMember(methodId, mi.DeclaringType.GetGenericArguments(), mi.GetGenericArguments()));
}
}
catch { }
break;
default:
throw new Exception("Unknown instruction operand; cannot continue. Operand type: " + code.OperandType);
}
}
return result.ToArray();
}
}
class StackOverflowDetector
{
// This method will be found:
static int Recur()
{
CheckStackDepth();
int variable = 1;
return variable + Recur();
}
static void Main(string[] args)
{
RecursionDetector();
Console.WriteLine();
Console.ReadLine();
}
static void RecursionDetector()
{
// First decompile all methods in the Assembly:
Dictionary<MethodBase, MethodBase[]> calling = new Dictionary<MethodBase, MethodBase[]>();
var Assembly = typeof(StackOverflowDetector).Assembly;
foreach (var type in Assembly.GetTypes())
{
foreach (var member in type.GetMembers(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.Instance).OfType<MethodBase>())
{
var body = member.GetMethodBody();
if (body!=null)
{
var bytes = body.GetILAsByteArray();
if (bytes != null)
{
// Store all the calls of this method:
var calls = Decompiler.Decompile(member, bytes);
calling[member] = calls;
}
}
}
}
// Check every method:
foreach (var method in calling.Keys)
{
// If method A -> ... -> method A, we have a possible infinite recursion
CheckRecursion(method, calling, new HashSet<MethodBase>());
}
}
Maintenant, le fait qu'un cycle de méthode contient une récursivité n'est en aucun cas une garantie qu'un débordement de pile se produira - c'est juste la condition la plus probable pour votre exception de débordement de pile. En bref, cela signifie que ce code déterminera les morceaux de code où un débordement de pile peut se produira, ce qui devrait réduire considérablement la plupart du code.
Encore d'autres approches
Vous pouvez essayer d'autres approches que je n'ai pas décrites ici.
Je suggérerais de créer un wrapper autour de l'objet XmlWriter, de sorte qu'il compterait le nombre d'appels à WriteStartElement/WriteEndElement, et si vous limitez le nombre de balises à un certain nombre (fe 100), vous seriez en mesure de lever une exception différente, par exemple - Opération invalide.
Cela devrait résoudre le problème dans la majorité des cas
public class LimitedDepthXmlWriter : XmlWriter
{
private readonly XmlWriter _innerWriter;
private readonly int _maxDepth;
private int _depth;
public LimitedDepthXmlWriter(XmlWriter innerWriter): this(innerWriter, 100)
{
}
public LimitedDepthXmlWriter(XmlWriter innerWriter, int maxDepth)
{
_maxDepth = maxDepth;
_innerWriter = innerWriter;
}
public override void Close()
{
_innerWriter.Close();
}
public override void Flush()
{
_innerWriter.Flush();
}
public override string LookupPrefix(string ns)
{
return _innerWriter.LookupPrefix(ns);
}
public override void WriteBase64(byte[] buffer, int index, int count)
{
_innerWriter.WriteBase64(buffer, index, count);
}
public override void WriteCData(string text)
{
_innerWriter.WriteCData(text);
}
public override void WriteCharEntity(char ch)
{
_innerWriter.WriteCharEntity(ch);
}
public override void WriteChars(char[] buffer, int index, int count)
{
_innerWriter.WriteChars(buffer, index, count);
}
public override void WriteComment(string text)
{
_innerWriter.WriteComment(text);
}
public override void WriteDocType(string name, string pubid, string sysid, string subset)
{
_innerWriter.WriteDocType(name, pubid, sysid, subset);
}
public override void WriteEndAttribute()
{
_innerWriter.WriteEndAttribute();
}
public override void WriteEndDocument()
{
_innerWriter.WriteEndDocument();
}
public override void WriteEndElement()
{
_depth--;
_innerWriter.WriteEndElement();
}
public override void WriteEntityRef(string name)
{
_innerWriter.WriteEntityRef(name);
}
public override void WriteFullEndElement()
{
_innerWriter.WriteFullEndElement();
}
public override void WriteProcessingInstruction(string name, string text)
{
_innerWriter.WriteProcessingInstruction(name, text);
}
public override void WriteRaw(string data)
{
_innerWriter.WriteRaw(data);
}
public override void WriteRaw(char[] buffer, int index, int count)
{
_innerWriter.WriteRaw(buffer, index, count);
}
public override void WriteStartAttribute(string prefix, string localName, string ns)
{
_innerWriter.WriteStartAttribute(prefix, localName, ns);
}
public override void WriteStartDocument(bool standalone)
{
_innerWriter.WriteStartDocument(standalone);
}
public override void WriteStartDocument()
{
_innerWriter.WriteStartDocument();
}
public override void WriteStartElement(string prefix, string localName, string ns)
{
if (_depth++ > _maxDepth) ThrowException();
_innerWriter.WriteStartElement(prefix, localName, ns);
}
public override WriteState WriteState
{
get { return _innerWriter.WriteState; }
}
public override void WriteString(string text)
{
_innerWriter.WriteString(text);
}
public override void WriteSurrogateCharEntity(char lowChar, char highChar)
{
_innerWriter.WriteSurrogateCharEntity(lowChar, highChar);
}
public override void WriteWhitespace(string ws)
{
_innerWriter.WriteWhitespace(ws);
}
private void ThrowException()
{
throw new InvalidOperationException(string.Format("Result xml has more than {0} nested tags. It is possible that xslt transformation contains an endless recursive call.", _maxDepth));
}
}
Cette réponse est pour @WilliamJockusch.
Je me demande s'il existe un moyen général de retrouver les StackOverflowExceptions. En d'autres termes, supposons que j'ai une récursion infinie quelque part dans mon code, mais je ne sais pas où. Je veux le retrouver par un moyen plus facile que de parcourir le code partout jusqu'à ce que je le voie. Je me fiche du hack. Par exemple, ce serait génial d'avoir un module que je pourrais activer, peut-être même à partir d'un autre thread, qui interrogerait la profondeur de la pile et se plaindrait s'il atteignait un niveau que je considérais "trop élevé". Par exemple, je pourrais définir "trop haut" à 600 images, pensant que si la pile était trop profonde, cela devait être un problème. Est-ce que quelque chose comme ça est possible. Un autre exemple serait d'enregistrer chaque 1000e appel de méthode dans mon code à la sortie de débogage. Les chances que cela obtienne des preuves du débordement seraient plutôt bonnes et cela ne ferait probablement pas trop exploser la sortie. La clé est qu'il ne peut pas impliquer d'écrire un chèque partout où le débordement se produit. Parce que tout le problème est que je ne sais pas où c'est. De préférence, la solution ne devrait pas dépendre de l'apparence de mon environnement de développement; c'est-à-dire, il ne faut pas supposer que j'utilise C # via un ensemble d'outils spécifique (par exemple VS).
Il semble que vous souhaitiez entendre quelques techniques de débogage pour attraper ce StackOverflow, alors j'ai pensé que je partagerais quelques-unes pour vous d'essayer.
Pro : les vidages de mémoire sont un moyen sûr de déterminer la cause d'un débordement de pile. Un MVP C # et moi avons travaillé ensemble pour dépanner un SO et il a ensuite blogué à ce sujet ici .
Cette méthode est le moyen le plus rapide de rechercher le problème.
Cette méthode ne vous obligera pas à reproduire les problèmes en suivant les étapes vues dans les journaux.
Contre : les vidages mémoire sont très importants et vous devez attacher AdPlus/procdump au processus.
Pro's : C'est probablement le moyen le plus simple pour vous d'implémenter du code qui vérifie la taille de la pile d'appels à partir de n'importe quelle méthode sans écrire de code dans chaque méthode de votre application. Il y a un tas de AOP Frameworks qui vous permettent d'intercepter avant et après les appels.
Vous indiquera les méthodes qui provoquent le débordement de la pile.
Vous permet de vérifier la StackTrace().FrameCount
à l'entrée et à la sortie de toutes les méthodes de votre application.
Con : Cela aura un impact sur les performances - les crochets sont intégrés dans l'IL pour chaque méthode et vous ne pouvez pas vraiment "désactiver".
Cela dépend quelque peu de votre ensemble d'outils d'environnement de développement.
Il y a une semaine, j'essayais de traquer plusieurs problèmes difficiles à reproduire. J'ai posté ce QA Journalisation des activités utilisateur, télémétrie (et variables dans les gestionnaires d'exceptions globales) . La conclusion à laquelle je suis arrivé était un enregistreur d'actions utilisateur très simple pour voir comment reproduire les problèmes dans un débogueur lorsqu'une exception non gérée se produit.
Pro : Vous pouvez l'activer ou le désactiver à volonté (c'est-à-dire en vous abonnant à des événements).
Le suivi des actions de l'utilisateur ne nécessite pas d'intercepter toutes les méthodes.
Vous pouvez également compter le nombre d'événements auxquels les méthodes sont souscrites beaucoup plus simplement qu'avec AOP.
Les fichiers journaux sont relativement petits et se concentrent sur les actions que vous devez effectuer pour reproduire le problème.
Il peut vous aider à comprendre comment les utilisateurs utilisent votre application.
Con : ne convient pas à un service Windows et je suis sûr qu'il existe de meilleurs outils comme celui-ci pour les applications Web =.
nécessairement ne vous indique pas les méthodes qui provoquent le débordement de la pile.
Vous oblige à parcourir les journaux reproduisant manuellement les problèmes plutôt qu'un vidage de la mémoire où vous pouvez les obtenir et les déboguer immédiatement.
Vous pourriez peut-être essayer toutes les techniques que je mentionne ci-dessus et certaines que @atlaste a publiées et nous dire laquelle vous avez trouvé la plus facile/rapide/sale/la plus acceptable à exécuter dans un environnement PROD/etc.
Quoi qu'il en soit, bonne chance pour retrouver ce SO.
Si votre application dépend du code tiers (dans les scripts Xsl), vous devez d'abord décider si vous voulez vous défendre contre les bogues ou non. Si vous voulez vraiment vous défendre, je pense que vous devriez exécuter votre logique sujette à des erreurs externes dans des domaines d'application séparés. La capture de StackOverflowException n'est pas bonne.
Vérifiez également ceci question .
J'ai eu un stackoverflow aujourd'hui et j'ai lu certains de vos messages et j'ai décidé d'aider le garbage collector.
J'avais l'habitude d'avoir une boucle presque infinie comme ceci:
class Foo
{
public Foo()
{
Go();
}
public void Go()
{
for (float i = float.MinValue; i < float.MaxValue; i+= 0.000000000000001f)
{
byte[] b = new byte[1]; // Causes stackoverflow
}
}
}
Au lieu de cela, laissez la ressource hors de portée comme ceci:
class Foo
{
public Foo()
{
GoHelper();
}
public void GoHelper()
{
for (float i = float.MinValue; i < float.MaxValue; i+= 0.000000000000001f)
{
Go();
}
}
public void Go()
{
byte[] b = new byte[1]; // Will get cleaned by GC
} // right now
}
Cela a fonctionné pour moi, j'espère que cela aide quelqu'un.
Avec .NET 4.0, vous pouvez ajouter l'attribut HandleProcessCorruptedStateExceptions
de System.Runtime.ExceptionServices à la méthode contenant le bloc try/catch. Cela a vraiment fonctionné! Peut-être pas recommandé mais fonctionne.
using System;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Runtime.ExceptionServices;
namespace ExceptionCatching
{
public class Test
{
public void StackOverflow()
{
StackOverflow();
}
public void CustomException()
{
throw new Exception();
}
public unsafe void AccessViolation()
{
byte b = *(byte*)(8762765876);
}
}
class Program
{
[HandleProcessCorruptedStateExceptions]
static void Main(string[] args)
{
Test test = new Test();
try {
//test.StackOverflow();
test.AccessViolation();
//test.CustomException();
}
catch
{
Console.WriteLine("Caught.");
}
Console.WriteLine("End of program");
}
}
}
@WilliamJockusch, si j'ai bien compris votre inquiétude, il n'est pas possible (d'un point de vue mathématique) d'identifier toujours une récursion infinie car cela signifierait résoudre le Halting problem . Pour le résoudre, vous auriez besoin d'un algorithme super-récursif (comme prédicats d'essai et d'erreur par exemple) ou d'une machine capable hypercalcul (un exemple est expliqué dans la section suivante - disponible en aperçu - de ce livre ).
D'un point de vue pratique, il faudrait savoir:
Gardez à l'esprit que, avec les machines actuelles, ces données sont extrêmement modifiables en raison du multitâche et je n'ai pas entendu parler d'un logiciel qui fait la tâche.
Faites-moi savoir si quelque chose n'est pas clair.
À première vue, à part démarrer un autre processus, il ne semble pas y avoir de moyen de gérer un StackOverflowException
. Avant que quelqu'un d'autre ne le demande, j'ai essayé d'utiliser AppDomain
, mais cela n'a pas fonctionné:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading;
namespace StackOverflowExceptionAppDomainTest
{
class Program
{
static void recrusiveAlgorithm()
{
recrusiveAlgorithm();
}
static void Main(string[] args)
{
if(args.Length>0&&args[0]=="--child")
{
recrusiveAlgorithm();
}
else
{
var domain = AppDomain.CreateDomain("Child domain to test StackOverflowException in.");
domain.ExecuteAssembly(Assembly.GetEntryAssembly().CodeBase, new[] { "--child" });
domain.UnhandledException += (object sender, UnhandledExceptionEventArgs e) =>
{
Console.WriteLine("Detected unhandled exception: " + e.ExceptionObject.ToString());
};
while (true)
{
Console.WriteLine("*");
Thread.Sleep(1000);
}
}
}
}
}
Cependant, si vous finissez par utiliser la solution de processus séparé, je vous recommande d'utiliser Process.Exited
et Process.StandardOutput
et gérez les erreurs vous-même, pour offrir une meilleure expérience à vos utilisateurs.