Domanda

A volte vedo molte applicazioni come msn, Windows Media Player ecc. che sono applicazioni a istanza singola (quando l'utente esegue mentre l'applicazione esegue una nuova istanza dell'applicazione non verrà creata).

In C #, uso la classe Mutex ma non so come farlo in Java.

È stato utile?

Soluzione

Se credo in questo articolo , di:

  

avendo il primo tentativo di aprire un socket di ascolto sull'interfaccia localhost. Se è in grado di aprire il socket, si presume che questa sia la prima istanza dell'applicazione da avviare. In caso contrario, si presuppone che un'istanza di questa applicazione sia già in esecuzione. La nuova istanza deve notificare all'istanza esistente che è stato tentato un avvio, quindi uscire. L'istanza esistente subentra dopo aver ricevuto la notifica e genera un evento per l'ascoltatore che gestisce l'azione.

Nota: Ahe menziona nel commento che l'uso di InetAddress.getLocalHost () può essere ingannevole:

  
      
  • non funziona come previsto nell'ambiente DHCP perché l'indirizzo restituito dipende dal fatto che il computer abbia accesso alla rete.
      La soluzione era aprire la connessione con InetAddress.getByAddress (nuovo byte [] {127, 0, 0, 1}) ;
      Probabilmente correlato a bug 4435662 .
  •   
  • Ho anche trovato bug 4665037 che riporta risultati attesi di getLocalHost : restituisce l'indirizzo IP della macchina, rispetto ai risultati effettivi: restituisce 127.0.0.1 .
  

è sorprendente che getLocalHost restituisca 127.0.0.1 su Linux ma non su Windows.


Oppure puoi usare oggetto ManagementFactory . Come spiegato qui :

  

Il metodo getMonitoredVMs (int processPid) riceve come parametro il PID dell'applicazione corrente e rileva il nome dell'applicazione chiamato dalla riga di comando, ad esempio l'applicazione è stata avviata da c: percorso \ java \ app \ test.jar , quindi la variabile valore è " c: \\ java \\ app \\ test.jar " ;. In questo modo, cattureremo solo il nome dell'applicazione sulla riga 17 del codice seguente.
  Successivamente, cerchiamo un altro processo con JVM con lo stesso nome, se lo abbiamo trovato e l'applicazione PID è diversa, significa che è la seconda istanza dell'applicazione.

JNLP offre anche un SingleInstanceListener

Altri suggerimenti

Uso il seguente metodo nel metodo principale. Questo è il metodo più semplice, più robusto e meno invadente che abbia mai visto, quindi ho pensato di condividerlo.

private static boolean lockInstance(final String lockFile) {
    try {
        final File file = new File(lockFile);
        final RandomAccessFile randomAccessFile = new RandomAccessFile(file, "rw");
        final FileLock fileLock = randomAccessFile.getChannel().tryLock();
        if (fileLock != null) {
            Runtime.getRuntime().addShutdownHook(new Thread() {
                public void run() {
                    try {
                        fileLock.release();
                        randomAccessFile.close();
                        file.delete();
                    } catch (Exception e) {
                        log.error("Unable to remove lock file: " + lockFile, e);
                    }
                }
            });
            return true;
        }
    } catch (Exception e) {
        log.error("Unable to create and/or lock file: " + lockFile, e);
    }
    return false;
}

Se l'app. ha una GUI, avviarla con JWS e utilizzare SingleInstanceService . Guarda la demo. del SingleInstanceService per (demo. e) codice di esempio.

Sì, questa è una risposta davvero decente per l'applicazione eclissi a singola istanza RCP eclipse sotto è il mio codice

in application.java

if(!isFileshipAlreadyRunning()){
        MessageDialog.openError(display.getActiveShell(), "Fileship already running", "Another instance of this application is already running.  Exiting.");
        return IApplication.EXIT_OK;
    } 


private static boolean isFileshipAlreadyRunning() {
    // socket concept is shown at http://www.rbgrn.net/content/43-java-single-application-instance
    // but this one is really great
    try {
        final File file = new File("FileshipReserved.txt");
        final RandomAccessFile randomAccessFile = new RandomAccessFile(file, "rw");
        final FileLock fileLock = randomAccessFile.getChannel().tryLock();
        if (fileLock != null) {
            Runtime.getRuntime().addShutdownHook(new Thread() {
                public void run() {
                    try {
                        fileLock.release();
                        randomAccessFile.close();
                        file.delete();
                    } catch (Exception e) {
                        //log.error("Unable to remove lock file: " + lockFile, e);
                    }
                }
            });
            return true;
        }
    } catch (Exception e) {
       // log.error("Unable to create and/or lock file: " + lockFile, e);
    }
    return false;
}

Usiamo il blocco dei file per questo (prendiamo un blocco esclusivo su un file magico nella directory dei dati delle app dell'utente), ma siamo principalmente interessati a impedire che più istanze vengano mai eseguite.

Se stai cercando di far passare args da riga di comando alla seconda istanza, ecc ... alla prima istanza, usare una connessione socket su localhost ucciderà due uccelli con una fava. Algoritmo generale:

  • All'avvio, prova ad aprire il listener sulla porta XXXX su localhost
  • se fallisce, apri un writer su quella porta su localhost e invia la riga di comando args, quindi arresta
  • altrimenti, ascolta sulla porta XXXXX su localhost. Quando ricevi argomenti della riga di comando, elaborali come se l'app fosse stata avviata con quella riga di comando.

Ho trovato una soluzione, una spiegazione un po 'da cartone animato, ma nella maggior parte dei casi funziona ancora. Usa il semplice vecchio file di blocco per creare cose, ma in una visione abbastanza diversa:

http://javal landscape.blogspot.com/ 2008/07 / single-instance-da-tua-application.html

Penso che sarà di aiuto per coloro che hanno una rigorosa impostazione del firewall.

Puoi usare la libreria JUnique. Fornisce supporto per l'esecuzione di applicazioni java a istanza singola ed è open source.

http://www.sauronsoftware.it/projects/junique/

  

La libreria JUnique può essere utilizzata per impedire a un utente di eseguire lo stesso   più volte istanze della stessa applicazione Java.

     

JUnique implementa blocchi e canali di comunicazione condivisi tra tutti   le istanze JVM lanciate dallo stesso utente.

public static void main(String[] args) {
    String appId = "myapplicationid";
    boolean alreadyRunning;
    try {
        JUnique.acquireLock(appId, new MessageHandler() {
            public String handle(String message) {
                // A brand new argument received! Handle it!
                return null;
            }
        });
        alreadyRunning = false;
    } catch (AlreadyLockedException e) {
        alreadyRunning = true;
    }
    if (!alreadyRunning) {
        // Start sequence here
    } else {
        for (int i = 0; i < args.length; i++) {
            JUnique.sendMessage(appId, args[0]));
        }
    }
}

Sotto il cofano, crea blocchi di file nella cartella% USER_DATA% /. junique e crea un socket del server a porta casuale per ogni ID app unico che consente di inviare / ricevere messaggi tra applicazioni java.

Su Windows, puoi utilizzare launch4j .

Classe ManagementFactory supportata in J2SE 5.0 o successive dettagli

ma ora uso J2SE 1.4 e ho trovato questo http://audiprimadhanty.wordpress.com/2008/06/30/ensuring-one-instance-of-application-running-at-one-time/ ma Non ho mai testato. Cosa ne pensi?

Potresti provare a usare l'API Preferenze. È indipendente dalla piattaforma.

Un modo più generico di limitare il numero di istanze su una singola macchina, o anche su un'intera rete, è usare un socket multicast.

L'utilizzo di un socket multicast consente di trasmettere un messaggio a qualsiasi numero di istanze dell'applicazione, alcune delle quali possono trovarsi su macchine fisicamente remote attraverso una rete aziendale.

In questo modo puoi abilitare molti tipi di configurazioni, per controllare cose come

  • Una o più istanze per macchina
  • Una o più istanze per rete (ad es. controllo delle installazioni su un sito client)

Il supporto multicast di Java è tramite pacchetto java.net con MulticastSocket & amp; DatagramSocket è lo strumento principale.

Nota : MulticastSocket non garantisce la consegna di pacchetti di dati, quindi è necessario utilizzare uno strumento basato su socket multicast come JGroups . JGroups non garantisce la consegna di tutti i dati. È un singolo file jar, con un'API molto semplice.

JGroups esiste da un po 'di tempo e ha alcuni usi impressionanti nel settore, ad esempio sostiene il meccanismo di clustering di JBoss che trasmette dati a tutte le istanze di un cluster.

Usare JGroups, limitare il numero di istanze di un'app (su una macchina o una rete, diciamo: al numero di licenze acquistate da un cliente) è concettualmente molto semplice:

  • All'avvio dell'applicazione, ogni istanza tenta di unirsi a un gruppo con nome, ad es. "My Great App Group". Avrai configurato questo gruppo per consentire 0, 1 o N membri
  • Quando il conteggio dei membri del gruppo è maggiore di quello che hai configurato per esso .. l'app dovrebbe rifiutarsi di avviarsi.

È possibile aprire un file mappato in memoria e quindi vedere se quel file è già APERTO. se è già aperto, puoi tornare dalla schermata principale.

Altri modi è usare i file di blocco (pratica unix standard). Un altro modo è quello di mettere qualcosa negli Appunti quando main inizia dopo aver verificato se qualcosa è già negli Appunti.

Altrimenti, è possibile aprire un socket in modalità di ascolto (ServerSocket). Prima prova a connetterti alla presa hte; se non riesci a connetterti, apri un serverocket. se ti connetti, allora sai che è già in esecuzione un'altra istanza.

Quindi, praticamente qualsiasi risorsa di sistema può essere utilizzata per sapere che un'app è in esecuzione.

BR, ~ A

Ho usato i socket per questo e, a seconda che l'applicazione sia sul lato client o lato server, il comportamento è leggermente diverso:

  • lato client: se esiste già un'istanza (non riesco ad ascoltare su una porta specifica) passerò i parametri dell'applicazione e uscirò (potresti voler eseguire alcune azioni nell'istanza precedente) se no avvierò l'applicazione.
  • lato server: se esiste già un'istanza, stamperò un messaggio e uscirò, altrimenti avvierò l'applicazione.
public class SingleInstance {
    public static final String LOCK = System.getProperty("user.home") + File.separator + "test.lock";
    public static final String PIPE = System.getProperty("user.home") + File.separator + "test.pipe";
    private static JFrame frame = null;

    public static void main(String[] args) {
        try {
            FileChannel lockChannel = new RandomAccessFile(LOCK, "rw").getChannel();
            FileLock flk = null; 
            try {
                flk = lockChannel.tryLock();
            } catch(Throwable t) {
                t.printStackTrace();
            }
            if (flk == null || !flk.isValid()) {
                System.out.println("alread running, leaving a message to pipe and quitting...");
                FileChannel pipeChannel = null;
                try {
                    pipeChannel = new RandomAccessFile(PIPE, "rw").getChannel();
                    MappedByteBuffer bb = pipeChannel.map(FileChannel.MapMode.READ_WRITE, 0, 1);
                    bb.put(0, (byte)1);
                    bb.force();
                } catch (Throwable t) {
                    t.printStackTrace();
                } finally {
                    if (pipeChannel != null) {
                        try {
                            pipeChannel.close();
                        } catch (Throwable t) {
                            t.printStackTrace();
                        }
                    } 
                }
                System.exit(0);
            }
            //We do not release the lock and close the channel here, 
            //  which will be done after the application crashes or closes normally. 
            SwingUtilities.invokeLater(
                new Runnable() {
                    public void run() {
                        createAndShowGUI();
                    }
                }
            );

            FileChannel pipeChannel = null;
            try {
                pipeChannel = new RandomAccessFile(PIPE, "rw").getChannel();
                MappedByteBuffer bb = pipeChannel.map(FileChannel.MapMode.READ_WRITE, 0, 1);
                while (true) {
                    byte b = bb.get(0);
                    if (b > 0) {
                        bb.put(0, (byte)0);
                        bb.force();
                        SwingUtilities.invokeLater(
                            new Runnable() {
                                public void run() {
                                    frame.setExtendedState(JFrame.NORMAL);
                                    frame.setAlwaysOnTop(true);
                                    frame.toFront();
                                    frame.setAlwaysOnTop(false);
                                }
                            }
                        );
                    }
                    Thread.sleep(1000);
                }
            } catch (Throwable t) {
                t.printStackTrace();
            } finally {
                if (pipeChannel != null) {
                    try {
                        pipeChannel.close();
                    } catch (Throwable t) {
                        t.printStackTrace();
                    } 
                } 
            }
        } catch(Throwable t) {
            t.printStackTrace();
        } 
    }

    public static void createAndShowGUI() {

        frame = new JFrame();
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.setSize(800, 650);
        frame.getContentPane().add(new JLabel("MAIN WINDOW", 
                    SwingConstants.CENTER), BorderLayout.CENTER);
        frame.setLocationRelativeTo(null);
        frame.setVisible(true);
    }
}

MODIFICA : invece di utilizzare questo approccio WatchService, è possibile utilizzare un semplice thread del timer da 1 secondo per verificare se l'indicatoreFile.exists (). Eliminalo, quindi porta l'applicazione su Front ().

MODIFICA : vorrei sapere perché è stato sottoposto a downgrade. È la migliore soluzione che ho visto finora. Per esempio. l'approccio socket server non riesce se un'altra applicazione sta già ascoltando la porta.

Basta scaricare Microsoft Windows Sysinternals TCPView (o utilizzare netstat) , avvialo, ordina per " Stato " ;, cerca il blocco di linea che dice " ASCOLTO " ;, scegli uno il cui indirizzo remoto dice il nome del tuo computer, inserisci quella porta nella tua nuova soluzione Socket (). Nella mia implementazione, posso sempre produrre errori. Ed è logico , perché è il vero fondamento dell'approccio. O cosa non sto ottenendo riguardo a come implementarlo?

Per favore informami se e come sbaglio su questo!

La mia opinione - che ti sto chiedendo di confutare, se possibile - è che agli sviluppatori viene consigliato di utilizzare un approccio nel codice di produzione che fallirà in almeno 1 su circa 60000 casi. E se questo punto di vista sembra corretto, allora può assolutamente non che una soluzione presentata che non presenta questo problema sia declassata e criticata per la sua quantità di codice.

Svantaggi dell'approccio socket rispetto:

  • Non riesce se viene scelto il biglietto della lotteria (numero di porta) errato.
  • Errore nell'ambiente multiutente: solo un utente può eseguire l'applicazione contemporaneamente. (Il mio approccio dovrebbe essere leggermente modificato per creare i file nella struttura utente, ma è banale.)
  • Non riesce se le regole del firewall sono troppo rigide.
  • Fa sì che gli utenti sospetti (che ho incontrato in natura) si chiedano quali shenanigans stai facendo quando il tuo editor di testo rivendica un socket del server.

Ho appena avuto una bella idea su come risolvere il problema di comunicazione Java da nuova istanza a istanza esistente in un modo che dovrebbe funzionare su ogni sistema. Così, ho montato questa lezione in circa due ore. Funziona come un fascino: D

Si basa sull'approccio di blocco dei file di Robert (anche in questa pagina), che ho usato da allora. Per dire all'istanza già in esecuzione che un'altra istanza ha tentato di avviarsi (ma non l'ha fatto) ... un file viene creato e immediatamente eliminato, e la prima istanza utilizza WatchService per rilevare questa modifica del contenuto della cartella. Non riesco a credere che apparentemente questa sia una nuova idea, dato quanto sia fondamentale il problema.

Questo può essere facilmente modificato in solo crea e non eliminare il file, quindi possono essere inserite informazioni che l'istanza appropriata può valutare, ad es. gli argomenti della riga di comando - e l'istanza corretta può quindi eseguire la cancellazione. Personalmente, dovevo solo sapere quando ripristinare la finestra della mia applicazione e inviarla in primo piano.

Esempio di utilizzo:

public static void main(final String[] args) {

    // ENSURE SINGLE INSTANCE
    if (!SingleInstanceChecker.INSTANCE.isOnlyInstance(Main::otherInstanceTriedToLaunch, false)) {
        System.exit(0);
    }

    // launch rest of application here
    System.out.println("Application starts properly because it's the only instance.");
}

private static void otherInstanceTriedToLaunch() {
    // Restore your application window and bring it to front.
    // But make sure your situation is apt: This method could be called at *any* time.
    System.err.println("Deiconified because other instance tried to start.");
}

Ecco la classe:

package yourpackagehere;

import javax.swing.*;
import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.channels.FileLock;
import java.nio.file.*;




/**
 * SingleInstanceChecker v[(2), 2016-04-22 08:00 UTC] by dreamspace-president.com
 * <p>
 * (file lock single instance solution by Robert https://stackoverflow.com/a/2002948/3500521)
 */
public enum SingleInstanceChecker {

    INSTANCE; // HAHA! The CONFUSION!


    final public static int POLLINTERVAL = 1000;
    final public static File LOCKFILE = new File("SINGLE_INSTANCE_LOCKFILE");
    final public static File DETECTFILE = new File("EXTRA_INSTANCE_DETECTFILE");


    private boolean hasBeenUsedAlready = false;


    private WatchService watchService = null;
    private RandomAccessFile randomAccessFileForLock = null;
    private FileLock fileLock = null;


    /**
     * CAN ONLY BE CALLED ONCE.
     * <p>
     * Assumes that the program will close if FALSE is returned: The other-instance-tries-to-launch listener is not
     * installed in that case.
     * <p>
     * Checks if another instance is already running (temp file lock / shutdownhook). Depending on the accessibility of
     * the temp file the return value will be true or false. This approach even works even if the virtual machine
     * process gets killed. On the next run, the program can even detect if it has shut down irregularly, because then
     * the file will still exist. (Thanks to Robert https://stackoverflow.com/a/2002948/3500521 for that solution!)
     * <p>
     * Additionally, the method checks if another instance tries to start. In a crappy way, because as awesome as Java
     * is, it lacks some fundamental features. Don't worry, it has only been 25 years, it'll sure come eventually.
     *
     * @param codeToRunIfOtherInstanceTriesToStart Can be null. If not null and another instance tries to start (which
     *                                             changes the detect-file), the code will be executed. Could be used to
     *                                             bring the current (=old=only) instance to front. If null, then the
     *                                             watcher will not be installed at all, nor will the trigger file be
     *                                             created. (Null means that you just don't want to make use of this
     *                                             half of the class' purpose, but then you would be better advised to
     *                                             just use the 24 line method by Robert.)
     *                                             <p>
     *                                             BE CAREFUL with the code: It will potentially be called until the
     *                                             very last moment of the program's existence, so if you e.g. have a
     *                                             shutdown procedure or a window that would be brought to front, check
     *                                             if the procedure has not been triggered yet or if the window still
     *                                             exists / hasn't been disposed of yet. Or edit this class to be more
     *                                             comfortable. This would e.g. allow you to remove some crappy
     *                                             comments. Attribution would be nice, though.
     * @param executeOnAWTEventDispatchThread      Convenience function. If false, the code will just be executed. If
     *                                             true, it will be detected if we're currently on that thread. If so,
     *                                             the code will just be executed. If not so, the code will be run via
     *                                             SwingUtilities.invokeLater().
     * @return if this is the only instance
     */
    public boolean isOnlyInstance(final Runnable codeToRunIfOtherInstanceTriesToStart, final boolean executeOnAWTEventDispatchThread) {

        if (hasBeenUsedAlready) {
            throw new IllegalStateException("This class/method can only be used once, which kinda makes sense if you think about it.");
        }
        hasBeenUsedAlready = true;

        final boolean ret = canLockFileBeCreatedAndLocked();

        if (codeToRunIfOtherInstanceTriesToStart != null) {
            if (ret) {
                // Only if this is the only instance, it makes sense to install a watcher for additional instances.
                installOtherInstanceLaunchAttemptWatcher(codeToRunIfOtherInstanceTriesToStart, executeOnAWTEventDispatchThread);
            } else {
                // Only if this is NOT the only instance, it makes sense to create&delete the trigger file that will effect notification of the other instance.
                //
                // Regarding "codeToRunIfOtherInstanceTriesToStart != null":
                // While creation/deletion of the file concerns THE OTHER instance of the program,
                // making it dependent on the call made in THIS instance makes sense
                // because the code executed is probably the same.
                createAndDeleteOtherInstanceWatcherTriggerFile();
            }
        }

        optionallyInstallShutdownHookThatCleansEverythingUp();

        return ret;
    }


    private void createAndDeleteOtherInstanceWatcherTriggerFile() {

        try {
            final RandomAccessFile randomAccessFileForDetection = new RandomAccessFile(DETECTFILE, "rw");
            randomAccessFileForDetection.close();
            Files.deleteIfExists(DETECTFILE.toPath()); // File is created and then instantly deleted. Not a problem for the WatchService :)
        } catch (Exception e) {
            e.printStackTrace();
        }
    }


    private boolean canLockFileBeCreatedAndLocked() {

        try {
            randomAccessFileForLock = new RandomAccessFile(LOCKFILE, "rw");
            fileLock = randomAccessFileForLock.getChannel().tryLock();
            return fileLock != null;
        } catch (Exception e) {
            return false;
        }
    }


    private void installOtherInstanceLaunchAttemptWatcher(final Runnable codeToRunIfOtherInstanceTriesToStart, final boolean executeOnAWTEventDispatchThread) {

        // PREPARE WATCHSERVICE AND STUFF
        try {
            watchService = FileSystems.getDefault().newWatchService();
        } catch (IOException e) {
            e.printStackTrace();
            return;
        }
        final File appFolder = new File("").getAbsoluteFile(); // points to current folder
        final Path appFolderWatchable = appFolder.toPath();


        // REGISTER CURRENT FOLDER FOR WATCHING FOR FILE DELETIONS
        try {
            appFolderWatchable.register(watchService, StandardWatchEventKinds.ENTRY_DELETE);
        } catch (IOException e) {
            e.printStackTrace();
            return;
        }


        // INSTALL WATCHER THAT LOOKS IF OUR detectFile SHOWS UP IN THE DIRECTORY CHANGES. IF THERE'S A CHANGE, ANOTHER INSTANCE TRIED TO START, SO NOTIFY THE CURRENT ONE OF THAT.
        final Thread t = new Thread(() -> watchForDirectoryChangesOnExtraThread(codeToRunIfOtherInstanceTriesToStart, executeOnAWTEventDispatchThread));
        t.setDaemon(true);
        t.setName("directory content change watcher");
        t.start();
    }


    private void optionallyInstallShutdownHookThatCleansEverythingUp() {

        if (fileLock == null && randomAccessFileForLock == null && watchService == null) {
            return;
        }

        final Thread shutdownHookThread = new Thread(() -> {
            try {
                if (fileLock != null) {
                    fileLock.release();
                }
                if (randomAccessFileForLock != null) {
                    randomAccessFileForLock.close();
                }
                Files.deleteIfExists(LOCKFILE.toPath());
            } catch (Exception ignore) {
            }
            if (watchService != null) {
                try {
                    watchService.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        });
        Runtime.getRuntime().addShutdownHook(shutdownHookThread);
    }


    private void watchForDirectoryChangesOnExtraThread(final Runnable codeToRunIfOtherInstanceTriesToStart, final boolean executeOnAWTEventDispatchThread) {

        while (true) { // To eternity and beyond! Until the universe shuts down. (Should be a volatile boolean, but this class only has absolutely required features.)

            try {
                Thread.sleep(POLLINTERVAL);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }


            final WatchKey wk;
            try {
                wk = watchService.poll();
            } catch (ClosedWatchServiceException e) {
                // This situation would be normal if the watcher has been closed, but our application never does that.
                e.printStackTrace();
                return;
            }

            if (wk == null || !wk.isValid()) {
                continue;
            }


            for (WatchEvent<?> we : wk.pollEvents()) {

                final WatchEvent.Kind<?> kind = we.kind();
                if (kind == StandardWatchEventKinds.OVERFLOW) {
                    System.err.println("OVERFLOW of directory change events!");
                    continue;
                }


                final WatchEvent<Path> watchEvent = (WatchEvent<Path>) we;
                final File file = watchEvent.context().toFile();


                if (file.equals(DETECTFILE)) {

                    if (!executeOnAWTEventDispatchThread || SwingUtilities.isEventDispatchThread()) {
                        codeToRunIfOtherInstanceTriesToStart.run();
                    } else {
                        SwingUtilities.invokeLater(codeToRunIfOtherInstanceTriesToStart);
                    }

                    break;

                } else {
                    System.err.println("THIS IS THE FILE THAT WAS DELETED: " + file);
                }

            }

            wk.reset();
        }
    }

}
Autorizzato sotto: CC-BY-SA insieme a attribuzione
Non affiliato a StackOverflow
scroll top