Bagaimana cara mengimplementasikan aplikasi Java instance tunggal?

89

Kadang-kadang saya melihat banyak aplikasi seperti msn, windows media player dll yang merupakan aplikasi instance tunggal (ketika pengguna mengeksekusi saat aplikasi sedang berjalan, instance aplikasi baru tidak akan dibuat).

Di C #, saya menggunakan Mutexkelas untuk ini tapi saya tidak tahu bagaimana melakukan ini di Java.

Fuangwith S.
sumber
Pendekatan yang sangat sederhana dengan java NIO lihat contoh lengkap stackoverflow.com/a/20015771/185022
AZ_

Jawaban:

62

Jika saya percaya artikel ini , oleh:

memiliki upaya contoh pertama untuk membuka soket pendengaran di antarmuka localhost. Jika dapat membuka soket, diasumsikan bahwa ini adalah contoh aplikasi pertama yang diluncurkan. Jika tidak, asumsinya adalah bahwa instance aplikasi ini sudah berjalan. Instance baru harus memberi tahu instance yang ada bahwa peluncuran telah dicoba, lalu keluar. Instance yang ada mengambil alih setelah menerima notifikasi dan mengaktifkan peristiwa ke pendengar yang menangani tindakan tersebut.

Catatan: Ahe menyebutkan dalam komentar yang menggunakan InetAddress.getLocalHost()bisa rumit:

  • itu tidak berfungsi seperti yang diharapkan di DHCP-environment karena alamat yang dikembalikan tergantung pada apakah komputer memiliki akses jaringan.
    Solusinya adalah membuka koneksi dengan InetAddress.getByAddress(new byte[] {127, 0, 0, 1});
    Mungkin terkait dengan bug 4435662 .
  • Saya juga menemukan bug 4665037 yang melaporkan hasil yang diharapkan dari getLocalHost: alamat IP pengembalian mesin, vs. Hasil aktual: kembali 127.0.0.1.

sangat mengejutkan untuk getLocalHostkembali 127.0.0.1ke Linux tetapi tidak di windows.


Atau Anda bisa menggunakan ManagementFactoryobjek. Seperti yang dijelaskan di sini :

The getMonitoredVMs(int processPid)Metode menerima sebagai parameter aplikasi PID saat ini, dan menangkap nama aplikasi yang disebut dari baris perintah, misalnya, aplikasi ini dimulai dari c:\java\app\test.jarjalan, maka nilai variabel adalah " c:\\java\\app\\test.jar". Dengan cara ini, kita hanya akan menangkap nama aplikasi pada baris 17 dari kode di bawah ini.
Setelah itu kita cari JVM untuk proses lain dengan nama yang sama, jika ditemukan dan PID aplikasinya berbeda, artinya itu adalah instance aplikasi kedua.

JNLP juga menawarkan a SingleInstanceListener

VonC
sumber
3
Ketahuilah bahwa solusi pertama memiliki bug. Kami baru-baru ini menemukan bahwa InetAddress.getLocalHost()tidak berfungsi seperti yang diharapkan di lingkungan DHCP karena alamat yang dikembalikan bergantung pada apakah komputer memiliki akses jaringan. Solusinya adalah membuka koneksi dengan InetAddress.getByAddress(new byte[] {127, 0, 0, 1});.
Ahe
2
@Ahe: poin yang sangat baik. Saya telah menyertakan komentar Anda serta referensi laporan bug Oracle-Sun dalam jawaban saya yang diedit.
VonC
3
Menurut JavaDoc InetAddress.getByName(null)mengembalikan alamat antarmuka loop kembali. Saya kira ini lebih baik daripada menetapkan 127.0.0.1 secara manual karena secara teori ini juga harus bekerja di lingkungan khusus IPv6.
kayahr
1
@ Puce Tentu, tidak masalah: Saya telah memulihkan tautan itu.
VonC
65

Saya menggunakan metode berikut dalam metode utama. Ini adalah metode paling sederhana, paling kuat, dan paling tidak mengganggu yang pernah saya lihat, jadi saya pikir saya akan membagikannya.

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;
}
Robert
sumber
apa parameter "lockFile" yang seharusnya untuk aplikasi desktop? nama file aplikasi jar? bagaimana kalau tidak ada file jar hanya beberapa file kelas?
5YrsLaterDBA
2
Apakah benar-benar perlu untuk melepaskan kunci file dan menutup file secara manual saat dimatikan? Bukankah ini terjadi secara otomatis saat proses mati?
Natix
5
Tetapi apa yang terjadi jika daya mati dan komputer mati tanpa menjalankan hook shutdown? File akan tetap ada dan aplikasi tidak akan dapat diluncurkan.
Petr Hudeček
6
@ PetrHudeček Tidak apa-apa. Tidak peduli bagaimana aplikasi berakhir, kunci file akan dilepaskan. Jika itu bukan shutdown yang tepat, maka ini bahkan memiliki manfaat untuk memungkinkan aplikasi menyadari hal ini pada proses berikutnya. Bagaimanapun: Kunci adalah yang terpenting, bukan keberadaan file itu sendiri. Jika file masih ada, aplikasi tetap akan diluncurkan.
Presiden Dreamspace
@Robert: Terima kasih atas solusinya, saya telah menggunakannya sejak saat itu. Dan sekarang, saya memperluasnya untuk juga berkomunikasi dengan instance yang sudah ada yang coba dijalankan oleh instance lain - menggunakan folder WatchService! stackoverflow.com/a/36772436/3500521
Presiden Dreamspace
10

Jika aplikasi. memiliki GUI, luncurkan dengan JWS dan gunakan SingleInstanceService.

Memperbarui

Java Plug-In (diperlukan untuk applet dan aplikasi JWS) tidak digunakan lagi oleh Oracle dan dihapus dari JDK. Produsen browser telah menghapusnya dari browser mereka.

Jadi jawaban ini sudah tidak berfungsi. Hanya meninggalkannya di sini untuk memperingatkan orang-orang yang melihat dokumentasi lama.

Andrew Thompson
sumber
2
Perhatikan juga bahwa tampaknya instance yang berjalan dapat diberi tahu tentang instance baru dan argumennya sehingga mudah untuk dikomunikasikan ke dalam program semacam itu.
Thorbjørn Ravn Andersen
6

Ya, ini adalah jawaban yang sangat layak untuk aplikasi contoh gerhana gerhana RCP tunggal di bawah ini adalah kode saya

di 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;
}
parvez Ahmad
sumber
5

Kami menggunakan penguncian file untuk ini (ambil kunci eksklusif pada file ajaib di direktori data aplikasi pengguna), tetapi kami terutama tertarik untuk mencegah beberapa contoh agar tidak pernah berjalan.

Jika Anda mencoba untuk memiliki contoh kedua pass command line args, dll ... ke contoh pertama, maka menggunakan koneksi soket di localhost akan membunuh dua burung dengan satu batu. Algoritma umum:

  • Saat peluncuran, coba buka listener di port XXXX di localhost
  • jika gagal, buka penulis ke port itu di localhost dan kirim argumen baris perintah, lalu matikan
  • jika tidak, dengarkan di port XXXXX di localhost. Saat menerima argumen baris perintah, proses seolah-olah aplikasi diluncurkan dengan baris perintah tersebut.
Kevin Day
sumber
5

Saya telah menemukan solusi, penjelasan yang agak kartun, tetapi masih berfungsi dalam banyak kasus. Ini menggunakan file kunci lama biasa yang membuat barang, tetapi dalam tampilan yang sangat berbeda:

http://javalandscape.blogspot.com/2008/07/single-instance-from-your-application.html

Saya pikir ini akan membantu mereka yang memiliki pengaturan firewall yang ketat.

Ikon
sumber
Ya, itu cara yang baik karena acara kunci akan dilepaskan jika aplikasi
rusak
5

Anda dapat menggunakan perpustakaan JUnique. Ini memberikan dukungan untuk menjalankan aplikasi java instance tunggal dan open-source.

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

Pustaka JUnique dapat digunakan untuk mencegah pengguna menjalankan lebih banyak instance dari aplikasi Java yang sama.

JUnique mengimplementasikan kunci dan saluran komunikasi yang dibagi antara semua instans JVM yang diluncurkan oleh pengguna yang sama.

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]));
        }
    }
}

Di balik terpal, ini membuat kunci file di folder% USER_DATA% /. Junique dan membuat soket server di port acak untuk setiap appId unik yang memungkinkan pengiriman / penerimaan pesan antara aplikasi java.

kolobok
sumber
Bisakah saya menggunakan ini untuk mencegah multi instance aplikasi java di jaringan? alias, hanya satu contoh aplikasi saya yang diizinkan dalam seluruh jaringan saya
Wuaner
4

Di Windows, Anda dapat menggunakan launch4j .

Jacek Szymański
sumber
2

Anda dapat mencoba menggunakan API Preferensi. Ini adalah platform independen.

Javamann
sumber
Saya suka ide ini karena API-nya sederhana tetapi mungkin beberapa pemindai virus tidak suka Anda mengubah registri sehingga Anda mendapatkan masalah yang sama seperti saat menggunakan RMI pada sistem dengan firewall perangkat lunak .... tidak yakin.
Kal
@Cal Tapi masalah yang sama adalah dengan mengubah file / mengunci / dll ... bukankah begitu?
Alex
2

Cara yang lebih umum untuk membatasi jumlah instance di satu mesin, atau bahkan seluruh jaringan, adalah dengan menggunakan soket multicast.

Menggunakan soket multicast, memungkinkan Anda untuk menyiarkan pesan ke sejumlah contoh aplikasi Anda, beberapa di antaranya dapat berada di mesin jarak jauh secara fisik di seluruh jaringan perusahaan.

Dengan cara ini Anda dapat mengaktifkan berbagai jenis konfigurasi, untuk mengontrol hal-hal seperti

  • Satu atau Banyak instance per mesin
  • Satu atau Banyak contoh per jaringan (mis. Mengontrol pemasangan di situs klien)

Dukungan multicast Java adalah melalui paket java.net dengan MulticastSocket & DatagramSocket sebagai alat utamanya.

Catatan : MulticastSocket tidak menjamin pengiriman paket data, jadi Anda harus menggunakan alat yang dibangun di atas soket multicast seperti JGroups . JGroups melakukan pengiriman jaminan semua data. Ini adalah satu file jar, dengan API yang sangat sederhana.

JGroups telah ada sejak lama, dan memiliki beberapa penggunaan yang mengesankan dalam industri, misalnya JGroup mendukung mekanisme clustering JBoss yang menyiarkan data ke semua instance cluster.

Untuk menggunakan JGroups, untuk membatasi jumlah instance aplikasi (di mesin atau jaringan, katakanlah: ke jumlah lisensi yang telah dibeli pelanggan) secara konseptual sangat sederhana:

  • Saat memulai aplikasi Anda, setiap instance mencoba untuk bergabung dengan grup bernama misalnya "Grup Aplikasi Hebat Saya". Anda akan mengonfigurasi grup ini untuk mengizinkan 0, 1 atau N anggota
  • Ketika jumlah anggota grup lebih besar dari yang telah Anda konfigurasikan untuknya .. aplikasi Anda harus menolak untuk memulai.
johnm
sumber
1

Anda dapat membuka File yang Dipetakan Memori dan kemudian melihat apakah file itu sudah TERBUKA. jika sudah terbuka, anda bisa kembali dari main.

Cara lain adalah dengan menggunakan file kunci (praktik unix standar). Satu cara lagi adalah dengan memasukkan sesuatu ke clipboard ketika main dimulai setelah memeriksa apakah ada sesuatu yang sudah ada di clipboard.

Lain, Anda dapat membuka soket dalam mode mendengarkan (ServerSocket). Pertama coba sambungkan ke soket hte; jika Anda tidak dapat terhubung, buka serverocket. jika Anda terhubung, Anda tahu bahwa instance lain sudah berjalan.

Jadi, hampir semua sumber daya sistem dapat digunakan untuk mengetahui bahwa suatu aplikasi sedang berjalan.

BR, ~ A

anjanb
sumber
apakah Anda memiliki kode untuk ide-ide itu? juga, bagaimana jika saya ingin jika pengguna memulai instance baru, itu akan menutup semua yang sebelumnya?
Pengembang android
1

Saya menggunakan soket untuk itu dan tergantung apakah aplikasinya ada di sisi klien atau sisi server, perilakunya sedikit berbeda:

  • sisi klien: jika sebuah instance sudah ada (saya tidak dapat mendengarkan pada port tertentu) saya akan meneruskan parameter aplikasi dan keluar (Anda mungkin ingin melakukan beberapa tindakan pada instance sebelumnya) jika tidak, saya akan memulai aplikasi.
  • sisi server: jika sebuah contoh sudah ada saya akan mencetak pesan dan keluar, jika tidak saya akan memulai aplikasi.
adrian.tarau
sumber
1
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";
    bingkai JFrame statis pribadi = null;

    public static void main (String [] args) {
        coba {
            FileChannel lockChannel = RandomAccessFile baru (LOCK, "rw"). GetChannel ();
            FileLock flk = null; 
            coba {
                flk = lockChannel.tryLock ();
            } catch (Throwable t) {
                t.printStackTrace ();
            }
            if (flk == null ||! flk.isValid ()) {
                System.out.println ("sudah berjalan, meninggalkan pesan ke pipa dan keluar ...");
                FileChannel pipeChannel = null;
                coba {
                    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 ();
                } akhirnya {
                    if (pipeChannel! = null) {
                        coba {
                            pipeChannel.close ();
                        } catch (Throwable t) {
                            t.printStackTrace ();
                        }
                    } 
                }
                System.exit (0);
            }
            // Kami tidak membuka kunci dan menutup saluran di sini, 
            // yang akan dilakukan setelah aplikasi crash atau menutup secara normal. 
            SwingUtilities.invokeLater (
                Runnable baru () {
                    public void run () {
                        createAndShowGUI ();
                    }
                }
            );

            FileChannel pipeChannel = null;
            coba {
                pipeChannel = new RandomAccessFile (PIPE, "rw"). getChannel ();
                MappedByteBuffer bb = pipeChannel.map (FileChannel.MapMode.READ_WRITE, 0, 1);
                sementara (benar) {
                    byte b = bb.get (0);
                    jika (b> 0) {
                        bb.put (0, (byte) 0);
                        bb.force ();
                        SwingUtilities.invokeLater (
                            Runnable baru () {
                                public void run () {
                                    frame.setExtendedState (JFrame.NORMAL);
                                    frame.setAlwaysOnTop (true);
                                    frame.toFront ();
                                    frame.setAlwaysOnTop (false);
                                }
                            }
                        );
                    }
                    Thread.sleep (1000);
                }
            } catch (Throwable t) {
                t.printStackTrace ();
            } akhirnya {
                if (pipeChannel! = null) {
                    coba {
                        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 (JLabel baru ("JENDELA UTAMA", 
                    SwingConstants.CENTER), BorderLayout.CENTER);
        frame.setLocationRelativeTo (null);
        frame.setVisible (true);
    }
}

George
sumber
1

EDIT : Alih-alih menggunakan pendekatan WatchService ini, utas pengatur waktu 1 detik sederhana dapat digunakan untuk memeriksa apakah indicatorFile.exists (). Hapus, lalu bawa aplikasi keFront ().

EDIT : Saya ingin tahu mengapa ini tidak disukai. Itu solusi terbaik yang pernah saya lihat sejauh ini. Misalnya pendekatan soket server gagal jika aplikasi lain kebetulan sudah mendengarkan port.

Cukup unduh Microsoft Windows Sysinternals TCPView (atau gunakan netstat), mulai, urutkan berdasarkan "Status", cari blok baris yang bertuliskan "MENDENGARKAN", pilih salah satu yang alamat jarak jauhnya menyebutkan nama komputer Anda, masukkan port tersebut ke Socket baru Anda ()-larutan. Dalam implementasinya, saya bisa menghasilkan kegagalan setiap saat. Dan itu logis , karena itu adalah dasar dari pendekatan tersebut. Atau apa yang tidak saya dapatkan tentang cara menerapkan ini?

Tolong beri tahu saya jika dan bagaimana saya salah tentang ini!

Pandangan saya - yang saya minta untuk Anda sangkal jika memungkinkan - adalah bahwa pengembang disarankan untuk menggunakan pendekatan dalam kode produksi yang akan gagal setidaknya dalam 1 dari sekitar 60.000 kasus. Dan jika pandangan ini kebetulan benar, maka sama sekali tidak mungkin solusi yang disajikan yang tidak memiliki masalah ini tidak disukai dan dikritik karena jumlah kodenya.

Kerugian dari pendekatan soket dibandingkan:

  • Gagal jika tiket lotere yang salah (nomor port) dipilih.
  • Gagal di lingkungan multi-pengguna: Hanya satu pengguna yang dapat menjalankan aplikasi pada waktu yang sama. (Pendekatan saya harus sedikit diubah untuk membuat file di pohon pengguna, tapi itu sepele.)
  • Gagal jika aturan firewall terlalu ketat.
  • Membuat pengguna yang mencurigakan (yang saya temui di alam liar) bertanya-tanya apa yang Anda lakukan ketika editor teks Anda mengklaim soket server.

Saya baru saja mendapat ide bagus tentang cara menyelesaikan masalah komunikasi Java instance-to-existing-instance baru dengan cara yang seharusnya berfungsi pada setiap sistem. Jadi, saya memulai kelas ini dalam waktu sekitar dua jam. Bekerja seperti pesona: D

Ini didasarkan pada pendekatan kunci file Robert (juga di halaman ini), yang telah saya gunakan sejak saat itu. Untuk memberi tahu instance yang sudah berjalan bahwa instance lain mencoba untuk memulai (tetapi tidak) ... file dibuat dan segera dihapus, dan instance pertama menggunakan WatchService untuk mendeteksi perubahan konten folder ini. Saya tidak percaya ternyata ini adalah ide baru, mengingat betapa fundamental masalahnya.

Ini dapat dengan mudah diubah menjadi hanya membuat dan tidak menghapus file, dan kemudian informasi dapat dimasukkan ke dalamnya yang dapat dievaluasi oleh instance yang tepat, misalnya argumen baris perintah - dan instance yang sesuai kemudian dapat melakukan penghapusan. Secara pribadi, saya hanya perlu tahu kapan harus memulihkan jendela aplikasi saya dan mengirimkannya ke depan.

Contoh penggunaan:

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.");
}

Inilah kelasnya:

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();
        }
    }

}
Presiden Dreamspace
sumber
Anda tidak memerlukan ratusan baris kode untuk menyelesaikan masalah ini. new ServerSocket()dengan blok tangkapan cukup memadai,
Marquis dari Lorne
@EJP Apakah Anda mengacu pada jawaban yang diterima, atau apa yang Anda bicarakan? Saya telah mencari sedikit solusi x-platform no-extra-library yang tidak gagal misalnya karena beberapa soket kebetulan sudah ditempati oleh aplikasi yang berbeda . Jika ada solusi untuk ini - terutama sesederhana yang Anda maksud - maka saya ingin mengetahuinya.
Presiden Dreamspace
@EJP: Saya ingin bertanya lagi 1) solusi sepele apa yang Anda maksud yang Anda telah menggantung seperti wortel di depan kepala saya, 2) jika itu solusi soket jawaban yang diterima dimulai dengan, jika satu atau lebih dari Poin-poin "Kekurangan pendekatan soket" saya berlaku, dan 3) jika demikian, mengapa terlepas dari kekurangan-kekurangan itu Anda masih akan merekomendasikan pendekatan itu daripada pendekatan saya.
Presiden Dreamspace
@EJP: Masalahnya adalah suara Anda memiliki bobot yang cukup, seperti yang Anda sadari, tetapi semua bukti yang saya miliki memaksa saya untuk yakin bahwa nasihat Anda di sini salah. Lihat, saya tidak bersikeras bahwa solusi saya benar dan sebagainya, tetapi saya adalah mesin berbasis bukti. Tidakkah Anda melihat bahwa posisi Anda memberi Anda tanggung jawab kepada komunitas untuk mengisi potongan teka-teki yang hilang dari komunikasi yang Anda mulai ini?
Presiden Dreamspace
@EJP: Karena sayangnya tidak ada reaksi dari Anda, inilah yang akan saya asumsikan sebagai fakta: Kebenaran tentang solusi soket server adalah bahwa itu benar-benar sangat cacat , dan alasan sebagian besar yang memilihnya mungkin adalah "Yang lain gunakan ini juga. ", atau mereka mungkin telah ditipu untuk menggunakannya oleh orang yang tidak bertanggung jawab. Saya rasa sebagian dari alasan mengapa Anda tidak menghargai kami dengan penjelasan yang diperlukan adalah karena Anda tidak dapat memahami / mengapa Anda tidak pernah mempertanyakan pendekatan ini, dan Anda tidak ingin membuat pernyataan publik yang mengungkapkan hal ini.
Presiden Dreamspace
1

Pustaka Unique4j dapat digunakan untuk menjalankan satu contoh aplikasi Java dan meneruskan pesan. Anda bisa melihatnya di https://github.com/prat-man/unique4j . Ini mendukung Java 1.6+.

Ini menggunakan kombinasi kunci file dan kunci port dinamis untuk mendeteksi dan berkomunikasi antara instans dengan tujuan utama hanya mengizinkan satu instans untuk berjalan.

Berikut ini adalah contoh sederhananya:

import tk.pratanumandal.unique4j.Unique4j;
import tk.pratanumandal.unique4j.exception.Unique4jException;

public class Unique4jDemo {

    // unique application ID
    public static String APP_ID = "tk.pratanumandal.unique4j-mlsdvo-20191511-#j.6";

    public static void main(String[] args) throws Unique4jException, InterruptedException {

        // create unique instance
        Unique4j unique = new Unique4j(APP_ID) {
            @Override
            public void receiveMessage(String message) {
                // display received message from subsequent instance
                System.out.println(message);
            }

            @Override
            public String sendMessage() {
                // send message to first instance
                return "Hello World!";
            }
        };

        // try to obtain lock
        boolean lockFlag = unique.acquireLock();

        // sleep the main thread for 30 seconds to simulate long running tasks
        Thread.sleep(30000);

        // try to free the lock before exiting program
        boolean lockFreeFlag = unique.freeLock();

    }

}

Penafian: Saya membuat dan memelihara perpustakaan Unique4j.

Pratanu Mandal
sumber
0

Saya menulis perpustakaan khusus untuk https://sanyarnd.github.io/applocker itu

Ini didasarkan pada penguncian saluran file, sehingga tidak akan memblokir nomor port, atau aplikasi kebuntuan jika listrik padam (saluran dilepaskan setelah proses dihentikan).

Library itu sendiri ringan dan memiliki API yang lancar.

Itu terinspirasi oleh http://www.sauronsoftware.it/projects/junique/ , tetapi ini didasarkan pada saluran file sebagai gantinya. Dan ada fitur ekstra baru lainnya.

Alexander Biryukov
sumber