Question

Comment définir des variables d'environnement à partir de Java? Je vois que je peux le faire pour les sous-processus en utilisant ProcessBuilder . Cependant, j'ai plusieurs sous-processus à démarrer, alors je préfère modifier l'environnement du processus actuel et laisser les sous-processus l'hériter.

Il existe un System.getenv(String) moyen d’obtenir une seule variable d’environnement. Je peux également obtenir un Map ensemble complet de variables d’environnement avec System.getenv(). Mais appeler put() à ce sujet UnsupportedOperationException jette un System.setenv() - apparemment, cela signifie que l’environnement doit être en lecture seule. Et il n'y a pas de <=>.

Alors, y a-t-il un moyen de définir des variables d'environnement dans le processus en cours d'exécution? Si c'est le cas, comment? Si non, quelle est la raison? (Est-ce parce que c'est Java et que par conséquent je ne devrais pas faire de choses obsolètes non portables mal comme toucher mon environnement?) Et sinon, de bonnes suggestions pour gérer les changements de variables d'environnement que je vais devoir nourrir à plusieurs sous-processus?

Était-ce utile?

La solution

  

(Est-ce parce qu'il s'agit de Java et que je ne devrais donc pas faire des choses obsolètes non portables comme du mal, comme toucher mon environnement?)

Je pense que vous avez frappé le clou sur la tête.

Un moyen possible d’alléger le fardeau serait de factoriser une méthode

void setUpEnvironment(ProcessBuilder builder) {
    Map<String, String> env = builder.environment();
    // blah blah
}

et passez tous les ProcessBuilder s avant de les démarrer.

En outre, vous le savez probablement déjà, mais vous pouvez démarrer plusieurs processus avec le même <=>. Donc, si vos sous-processus sont les mêmes, vous n'avez pas besoin de faire cette configuration encore et encore.

Autres conseils

Pour une utilisation dans les scénarios où vous devez définir des valeurs d’environnement spécifiques pour les tests unitaires, le hack suivant peut être utile. Cela modifiera les variables d’environnement tout au long de la machine virtuelle (assurez-vous donc de réinitialiser tout changement après votre test), mais ne modifiera pas votre environnement système.

J’ai trouvé qu’une combinaison des deux "bidouilles sales" d’Edward Campbell et d’anonymes fonctionnait mieux, puisqu’une des deux ne fonctionnait pas sous linux, on ne fonctionnait pas sous Windows 7. Donc, pour obtenir un piratage multiplateforme, je les ai combinées:

protected static void setEnv(Map<String, String> newenv) throws Exception {
  try {
    Class<?> processEnvironmentClass = Class.forName("java.lang.ProcessEnvironment");
    Field theEnvironmentField = processEnvironmentClass.getDeclaredField("theEnvironment");
    theEnvironmentField.setAccessible(true);
    Map<String, String> env = (Map<String, String>) theEnvironmentField.get(null);
    env.putAll(newenv);
    Field theCaseInsensitiveEnvironmentField = processEnvironmentClass.getDeclaredField("theCaseInsensitiveEnvironment");
    theCaseInsensitiveEnvironmentField.setAccessible(true);
    Map<String, String> cienv = (Map<String, String>)     theCaseInsensitiveEnvironmentField.get(null);
    cienv.putAll(newenv);
  } catch (NoSuchFieldException e) {
    Class[] classes = Collections.class.getDeclaredClasses();
    Map<String, String> env = System.getenv();
    for(Class cl : classes) {
      if("java.util.Collections$UnmodifiableMap".equals(cl.getName())) {
        Field field = cl.getDeclaredField("m");
        field.setAccessible(true);
        Object obj = field.get(env);
        Map<String, String> map = (Map<String, String>) obj;
        map.clear();
        map.putAll(newenv);
      }
    }
  }
}

Cela fonctionne comme un charme. Remerciements complets aux deux auteurs de ces hacks.

public static void set(Map<String, String> newenv) throws Exception {
    Class[] classes = Collections.class.getDeclaredClasses();
    Map<String, String> env = System.getenv();
    for(Class cl : classes) {
        if("java.util.Collections$UnmodifiableMap".equals(cl.getName())) {
            Field field = cl.getDeclaredField("m");
            field.setAccessible(true);
            Object obj = field.get(env);
            Map<String, String> map = (Map<String, String>) obj;
            map.clear();
            map.putAll(newenv);
        }
    }
}

Ou pour ajouter / mettre à jour une seule variable et supprimer la boucle selon la suggestion de thejoshwolfe.

@SuppressWarnings({ "unchecked" })
  public static void updateEnv(String name, String val) throws ReflectiveOperationException {
    Map<String, String> env = System.getenv();
    Field field = env.getClass().getDeclaredField("m");
    field.setAccessible(true);
    ((Map<String, String>) field.get(env)).put(name, val);
  }
// this is a dirty hack - but should be ok for a unittest.
private void setNewEnvironmentHack(Map<String, String> newenv) throws Exception
{
  Class<?> processEnvironmentClass = Class.forName("java.lang.ProcessEnvironment");
  Field theEnvironmentField = processEnvironmentClass.getDeclaredField("theEnvironment");
  theEnvironmentField.setAccessible(true);
  Map<String, String> env = (Map<String, String>) theEnvironmentField.get(null);
  env.clear();
  env.putAll(newenv);
  Field theCaseInsensitiveEnvironmentField = processEnvironmentClass.getDeclaredField("theCaseInsensitiveEnvironment");
  theCaseInsensitiveEnvironmentField.setAccessible(true);
  Map<String, String> cienv = (Map<String, String>) theCaseInsensitiveEnvironmentField.get(null);
  cienv.clear();
  cienv.putAll(newenv);
}

sur Android, l'interface est exposée via Libcore.os comme une sorte d'API cachée.

Libcore.os.setenv("VAR", "value", bOverwrite);
Libcore.os.getenv("VAR"));

La classe Libcore ainsi que le système d'exploitation de l'interface sont publics. Seule la déclaration de classe est manquante et doit être affichée à l'éditeur de liens. Il n'est pas nécessaire d'ajouter les classes à l'application, mais cela ne fait pas de mal s'il est inclus.

package libcore.io;

public final class Libcore {
    private Libcore() { }

    public static Os os;
}

package libcore.io;

public interface Os {
    public String getenv(String name);
    public void setenv(String name, String value, boolean overwrite) throws ErrnoException;
}

Linux uniquement

Définition de variables d’environnement uniques (en fonction de la réponse d’Edward Campbell):

public static void setEnv(String key, String value) {
    try {
        Map<String, String> env = System.getenv();
        Class<?> cl = env.getClass();
        Field field = cl.getDeclaredField("m");
        field.setAccessible(true);
        Map<String, String> writableEnv = (Map<String, String>) field.get(env);
        writableEnv.put(key, value);
    } catch (Exception e) {
        throw new IllegalStateException("Failed to set environment variable", e);
    }
}

Utilisation:

Tout d’abord, placez la méthode dans la classe de votre choix, par exemple. SystemUtil.

SystemUtil.setEnv("SHELL", "/bin/bash");

Si vous appelez System.getenv("SHELL") après cela, vous récupérerez "/bin/bash".

Il s’avère que la solution de @ pushy / @ anonymous / @ Edward Campbell ne fonctionne pas sous Android car Android n’est pas vraiment Java. Plus précisément, Android n’a pas java.lang.ProcessEnvironment du tout. Mais cela s’avère plus simple sous Android, il suffit de faire un appel JNI vers POSIX setenv():

Dans C / JNI:

JNIEXPORT jint JNICALL Java_com_example_posixtest_Posix_setenv
  (JNIEnv* env, jclass clazz, jstring key, jstring value, jboolean overwrite)
{
    char* k = (char *) (*env)->GetStringUTFChars(env, key, NULL);
    char* v = (char *) (*env)->GetStringUTFChars(env, value, NULL);
    int err = setenv(k, v, overwrite);
    (*env)->ReleaseStringUTFChars(env, key, k);
    (*env)->ReleaseStringUTFChars(env, value, v);
    return err;
}

Et en Java:

public class Posix {

    public static native int setenv(String key, String value, boolean overwrite);

    private void runTest() {
        Posix.setenv("LD_LIBRARY_PATH", "foo", true);
    }
}

C’est une combinaison de la réponse de @ paul-blair convertie en Java, qui inclut certains nettoyages signalés par paul blair et des erreurs qui semblent avoir été contenues dans le code de @pushy, composé de @ Edward Campbell et anonyme.

Je ne saurais trop insister sur le fait que ce code doit UNIQUEMENT être utilisé pour les tests et qu’il est extrêmement bidirectionnel. Mais dans les cas où vous avez besoin de la configuration de l’environnement lors des tests, c’est exactement ce dont j'avais besoin.

Ceci inclut également quelques touches mineures qui permettent au code de fonctionner sur les deux systèmes Windows exécutés sur

.
java version "1.8.0_92"
Java(TM) SE Runtime Environment (build 1.8.0_92-b14)
Java HotSpot(TM) 64-Bit Server VM (build 25.92-b14, mixed mode)

ainsi que Centos fonctionnant sur

openjdk version "1.8.0_91"
OpenJDK Runtime Environment (build 1.8.0_91-b14)
OpenJDK 64-Bit Server VM (build 25.91-b14, mixed mode)

La mise en œuvre:

/**
 * Sets an environment variable FOR THE CURRENT RUN OF THE JVM
 * Does not actually modify the system's environment variables,
 *  but rather only the copy of the variables that java has taken,
 *  and hence should only be used for testing purposes!
 * @param key The Name of the variable to set
 * @param value The value of the variable to set
 */
@SuppressWarnings("unchecked")
public static <K,V> void setenv(final String key, final String value) {
    try {
        /// we obtain the actual environment
        final Class<?> processEnvironmentClass = Class.forName("java.lang.ProcessEnvironment");
        final Field theEnvironmentField = processEnvironmentClass.getDeclaredField("theEnvironment");
        final boolean environmentAccessibility = theEnvironmentField.isAccessible();
        theEnvironmentField.setAccessible(true);

        final Map<K,V> env = (Map<K, V>) theEnvironmentField.get(null);

        if (SystemUtils.IS_OS_WINDOWS) {
            // This is all that is needed on windows running java jdk 1.8.0_92
            if (value == null) {
                env.remove(key);
            } else {
                env.put((K) key, (V) value);
            }
        } else {
            // This is triggered to work on openjdk 1.8.0_91
            // The ProcessEnvironment$Variable is the key of the map
            final Class<K> variableClass = (Class<K>) Class.forName("java.lang.ProcessEnvironment$Variable");
            final Method convertToVariable = variableClass.getMethod("valueOf", String.class);
            final boolean conversionVariableAccessibility = convertToVariable.isAccessible();
            convertToVariable.setAccessible(true);

            // The ProcessEnvironment$Value is the value fo the map
            final Class<V> valueClass = (Class<V>) Class.forName("java.lang.ProcessEnvironment$Value");
            final Method convertToValue = valueClass.getMethod("valueOf", String.class);
            final boolean conversionValueAccessibility = convertToValue.isAccessible();
            convertToValue.setAccessible(true);

            if (value == null) {
                env.remove(convertToVariable.invoke(null, key));
            } else {
                // we place the new value inside the map after conversion so as to
                // avoid class cast exceptions when rerunning this code
                env.put((K) convertToVariable.invoke(null, key), (V) convertToValue.invoke(null, value));

                // reset accessibility to what they were
                convertToValue.setAccessible(conversionValueAccessibility);
                convertToVariable.setAccessible(conversionVariableAccessibility);
            }
        }
        // reset environment accessibility
        theEnvironmentField.setAccessible(environmentAccessibility);

        // we apply the same to the case insensitive environment
        final Field theCaseInsensitiveEnvironmentField = processEnvironmentClass.getDeclaredField("theCaseInsensitiveEnvironment");
        final boolean insensitiveAccessibility = theCaseInsensitiveEnvironmentField.isAccessible();
        theCaseInsensitiveEnvironmentField.setAccessible(true);
        // Not entirely sure if this needs to be casted to ProcessEnvironment$Variable and $Value as well
        final Map<String, String> cienv = (Map<String, String>) theCaseInsensitiveEnvironmentField.get(null);
        if (value == null) {
            // remove if null
            cienv.remove(key);
        } else {
            cienv.put(key, value);
        }
        theCaseInsensitiveEnvironmentField.setAccessible(insensitiveAccessibility);
    } catch (final ClassNotFoundException | NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
        throw new IllegalStateException("Failed setting environment variable <"+key+"> to <"+value+">", e);
    } catch (final NoSuchFieldException e) {
        // we could not find theEnvironment
        final Map<String, String> env = System.getenv();
        Stream.of(Collections.class.getDeclaredClasses())
                // obtain the declared classes of type $UnmodifiableMap
                .filter(c1 -> "java.util.Collections$UnmodifiableMap".equals(c1.getName()))
                .map(c1 -> {
                    try {
                        return c1.getDeclaredField("m");
                    } catch (final NoSuchFieldException e1) {
                        throw new IllegalStateException("Failed setting environment variable <"+key+"> to <"+value+"> when locating in-class memory map of environment", e1);
                    }
                })
                .forEach(field -> {
                    try {
                        final boolean fieldAccessibility = field.isAccessible();
                        field.setAccessible(true);
                        // we obtain the environment
                        final Map<String, String> map = (Map<String, String>) field.get(env);
                        if (value == null) {
                            // remove if null
                            map.remove(key);
                        } else {
                            map.put(key, value);
                        }
                        // reset accessibility
                        field.setAccessible(fieldAccessibility);
                    } catch (final ConcurrentModificationException e1) {
                        // This may happen if we keep backups of the environment before calling this method
                        // as the map that we kept as a backup may be picked up inside this block.
                        // So we simply skip this attempt and continue adjusting the other maps
                        // To avoid this one should always keep individual keys/value backups not the entire map
                        LOGGER.info("Attempted to modify source map: "+field.getDeclaringClass()+"#"+field.getName(), e1);
                    } catch (final IllegalAccessException e1) {
                        throw new IllegalStateException("Failed setting environment variable <"+key+"> to <"+value+">. Unable to access field!", e1);
                    }
                });
    }
    LOGGER.info("Set environment variable <"+key+"> to <"+value+">. Sanity Check: "+System.getenv(key));
}

En fouinant en ligne, il semble possible de le faire avec JNI. Vous devez ensuite passer un appel à putenv () à partir de C et le faire (vraisemblablement) d’une manière qui fonctionne à la fois sous Windows et sous UNIX.

Si tout pouvait être fait, il ne serait sûrement pas trop difficile pour Java lui-même de supporter cela au lieu de me mettre dans une veste droite.

Un ami Perl parlant ailleurs, suggère que cela est dû au fait que les variables d'environnement sont des processus globaux et que Java cherche à obtenir une bonne isolation pour une bonne conception.

J'ai essayé la réponse de Pushy ci-dessus et cela a fonctionné pour la plupart. Cependant, dans certaines circonstances, je verrais cette exception:

java.lang.String cannot be cast to java.lang.ProcessEnvironment$Variable

Cela se produit lorsque la méthode a été appelée plusieurs fois, en raison de l'implémentation de certaines classes internes de ProcessEnvironment. Si la méthode setEnv(..) est appelée plusieurs fois, lorsque les clés sont extraites de la <= > map, ce sont maintenant des chaînes (ayant été introduites comme chaînes par la première invocation de theEnvironment) et ne peuvent pas être converties dans le type générique de la carte, setEnv(...) qui est une classe interne privée de Variable,

Une version corrigée (en Scala) est ci-dessous. Espérons que ce ne soit pas trop difficile à transférer en Java.

def setEnv(newenv: java.util.Map[String, String]): Unit = {
  try {
    val processEnvironmentClass = JavaClass.forName("java.lang.ProcessEnvironment")
    val theEnvironmentField = processEnvironmentClass.getDeclaredField("theEnvironment")
    theEnvironmentField.setAccessible(true)

    val variableClass = JavaClass.forName("java.lang.ProcessEnvironment$Variable")
    val convertToVariable = variableClass.getMethod("valueOf", classOf[java.lang.String])
    convertToVariable.setAccessible(true)

    val valueClass = JavaClass.forName("java.lang.ProcessEnvironment$Value")
    val convertToValue = valueClass.getMethod("valueOf", classOf[java.lang.String])
    convertToValue.setAccessible(true)

    val sampleVariable = convertToVariable.invoke(null, "")
    val sampleValue = convertToValue.invoke(null, "")
    val env = theEnvironmentField.get(null).asInstanceOf[java.util.Map[sampleVariable.type, sampleValue.type]]
    newenv.foreach { case (k, v) => {
        val variable = convertToVariable.invoke(null, k).asInstanceOf[sampleVariable.type]
        val value = convertToValue.invoke(null, v).asInstanceOf[sampleValue.type]
        env.put(variable, value)
      }
    }

    val theCaseInsensitiveEnvironmentField = processEnvironmentClass.getDeclaredField("theCaseInsensitiveEnvironment")
    theCaseInsensitiveEnvironmentField.setAccessible(true)
    val cienv = theCaseInsensitiveEnvironmentField.get(null).asInstanceOf[java.util.Map[String, String]]
    cienv.putAll(newenv);
  }
  catch {
    case e : NoSuchFieldException => {
      try {
        val classes = classOf[java.util.Collections].getDeclaredClasses
        val env = System.getenv()
        classes foreach (cl => {
          if("java.util.Collections$UnmodifiableMap" == cl.getName) {
            val field = cl.getDeclaredField("m")
            field.setAccessible(true)
            val map = field.get(env).asInstanceOf[java.util.Map[String, String]]
            // map.clear() // Not sure why this was in the code. It means we need to set all required environment variables.
            map.putAll(newenv)
          }
        })
      } catch {
        case e2: Exception => e2.printStackTrace()
      }
    }
    case e1: Exception => e1.printStackTrace()
  }
}

Comme la plupart des personnes qui ont trouvé ce fil de discussion, j’écrivais des tests unitaires et je devais modifier les variables d’environnement pour définir les conditions correctes pour l’exécution du test. Cependant, j’ai trouvé que les réponses les plus votées posaient quelques problèmes et / ou étaient très cryptiques ou trop compliquées. Espérons que cela aidera les autres à résoudre la solution plus rapidement.

Tout d’abord, j’ai finalement trouvé la solution de @Hubert Grzeskowiak la plus simple et elle a fonctionné pour moi. J'aurais aimé venir à celui-là en premier. Il est basé sur la réponse de Edward Campbell, mais sans compliquer la recherche de boucle.

Cependant, j’ai commencé avec la solution de @ pushy, qui a reçu le plus de votes positifs. C'est un combo de @ anonyme et @ Edward Campbell. @pushy affirme que les deux approches sont nécessaires pour couvrir les environnements Linux et Windows. J'exécute sous OS X et constate que les deux fonctionnent (une fois qu'un problème avec @anonymous est résolu). Comme d'autres l'ont noté, cette solution fonctionne la plupart du temps, mais pas dans son intégralité.

Je pense que la confusion est en grande partie due à la solution de @ anonymous opérant sur le champ "theEnvironment". En regardant la définition de ProcessEnvironment ," theEnvironment "n’est pas une carte < String, String & Gt; mais c'est plutôt une carte < Variable, Valeur & Gt ;. Effacer la carte fonctionne bien, mais l'opération putAll reconstruit la carte en tant que Map & Lt; String, String & Gt ;, ce qui peut entraîner des problèmes lorsque des opérations ultérieures opèrent sur la structure de données à l'aide de l'API normale qui attend Map & Lt; Variable, Valeur & Gt ;. En outre, l'accès / la suppression d'éléments individuels est un problème. La solution consiste à accéder «à l'environnement» indirectement par le biais de «l'environnement non modifiable». Mais puisqu'il s'agit d'un type UnmodifiableMap l'accès doit être effectué via la variable privée 'm' du type UnmodifiableMap. Voir getModifiableEnvironmentMap2 dans le code ci-dessous.

Dans mon cas, je devais supprimer certaines des variables d'environnement de mon test (les autres ne devraient pas être modifiées). Ensuite, je voulais restaurer les variables d’environnement à leur état antérieur après le test. Les routines ci-dessous facilitent grandement cette tâche. J'ai testé les deux versions de getModifiableEnvironmentMap sur OS X, et les deux fonctionnent de manière équivalente. Bien que basé sur les commentaires de ce fil de discussion, l’un peut être un meilleur choix que l’autre en fonction de l’environnement.

Remarque: je n'ai pas inclus l'accès à 'theCaseInsensitiveEnvironmentField' car cela semble être spécifique à Windows et je n'avais aucun moyen de le tester, mais son ajout devrait être simple.

private Map<String, String> getModifiableEnvironmentMap() {
    try {
        Map<String,String> unmodifiableEnv = System.getenv();
        Class<?> cl = unmodifiableEnv.getClass();
        Field field = cl.getDeclaredField("m");
        field.setAccessible(true);
        Map<String,String> modifiableEnv = (Map<String,String>) field.get(unmodifiableEnv);
        return modifiableEnv;
    } catch(Exception e) {
        throw new RuntimeException("Unable to access writable environment variable map.");
    }
}

private Map<String, String> getModifiableEnvironmentMap2() {
    try {
        Class<?> processEnvironmentClass = Class.forName("java.lang.ProcessEnvironment");
        Field theUnmodifiableEnvironmentField = processEnvironmentClass.getDeclaredField("theUnmodifiableEnvironment");
        theUnmodifiableEnvironmentField.setAccessible(true);
        Map<String,String> theUnmodifiableEnvironment = (Map<String,String>)theUnmodifiableEnvironmentField.get(null);

        Class<?> theUnmodifiableEnvironmentClass = theUnmodifiableEnvironment.getClass();
        Field theModifiableEnvField = theUnmodifiableEnvironmentClass.getDeclaredField("m");
        theModifiableEnvField.setAccessible(true);
        Map<String,String> modifiableEnv = (Map<String,String>) theModifiableEnvField.get(theUnmodifiableEnvironment);
        return modifiableEnv;
    } catch(Exception e) {
        throw new RuntimeException("Unable to access writable environment variable map.");
    }
}

private Map<String, String> clearEnvironmentVars(String[] keys) {

    Map<String,String> modifiableEnv = getModifiableEnvironmentMap();

    HashMap<String, String> savedVals = new HashMap<String, String>();

    for(String k : keys) {
        String val = modifiableEnv.remove(k);
        if (val != null) { savedVals.put(k, val); }
    }
    return savedVals;
}

private void setEnvironmentVars(Map<String, String> varMap) {
    getModifiableEnvironmentMap().putAll(varMap);   
}

@Test
public void myTest() {
    String[] keys = { "key1", "key2", "key3" };
    Map<String, String> savedVars = clearEnvironmentVars(keys);

    // do test

    setEnvironmentVars(savedVars);
}

Ceci est la version perverse de Kotlin du pervers answer =)

@Suppress("UNCHECKED_CAST")
@Throws(Exception::class)
fun setEnv(newenv: Map<String, String>) {
    try {
        val processEnvironmentClass = Class.forName("java.lang.ProcessEnvironment")
        val theEnvironmentField = processEnvironmentClass.getDeclaredField("theEnvironment")
        theEnvironmentField.isAccessible = true
        val env = theEnvironmentField.get(null) as MutableMap<String, String>
        env.putAll(newenv)
        val theCaseInsensitiveEnvironmentField = processEnvironmentClass.getDeclaredField("theCaseInsensitiveEnvironment")
        theCaseInsensitiveEnvironmentField.isAccessible = true
        val cienv = theCaseInsensitiveEnvironmentField.get(null) as MutableMap<String, String>
        cienv.putAll(newenv)
    } catch (e: NoSuchFieldException) {
        val classes = Collections::class.java.getDeclaredClasses()
        val env = System.getenv()
        for (cl in classes) {
            if ("java.util.Collections\$UnmodifiableMap" == cl.getName()) {
                val field = cl.getDeclaredField("m")
                field.setAccessible(true)
                val obj = field.get(env)
                val map = obj as MutableMap<String, String>
                map.clear()
                map.putAll(newenv)
            }
        }
    }

Cela fonctionne au moins dans macOS Mojave.

L'implémentation de Kotlin que j'ai récemment réalisée à partir de la réponse d'Edward:

fun setEnv(newEnv: Map<String, String>) {
    val unmodifiableMapClass = Collections.unmodifiableMap<Any, Any>(mapOf()).javaClass
    with(unmodifiableMapClass.getDeclaredField("m")) {
        isAccessible = true
        @Suppress("UNCHECKED_CAST")
        get(System.getenv()) as MutableMap<String, String>
    }.apply {
        clear()
        putAll(newEnv)
    }
}

Vous pouvez passer des paramètres dans votre processus Java initial avec -D:

java -cp <classpath> -Dkey1=value -Dkey2=value ...
Licencié sous: CC-BY-SA avec attribution
Non affilié à StackOverflow
scroll top