Как определить, запущено ли приложение OS X

Обычно пакет приложений в OS X можно запустить только один раз, однако, просто скопировав пакет, одно и то же приложение можно запустить дважды. Какова наилучшая стратегия для обнаружения и предотвращения такой возможности?

В Windows этот эффект может быть просто достигнут путем создания приложением именованного ресурса при запуске, а затем выхода, если именованный ресурс не может быть создан, указывая на то, что запущен другой процесс, который уже создал тот же ресурс. Эти ресурсы надежно высвобождаются в Windows при завершении работы приложения.

Проблема, с которой я столкнулся при исследовании, заключается в том, что API-интерфейсы в OS X сохраняют состояние в файловой системе и, таким образом, делают стратегию, используемую в Windows, ненадежной, т. е. устаревшие файлы после неправильного выхода могут ложно указывать на то, что приложение уже запущено.

API, которые я могу использовать для достижения того же эффекта в OS X: posix, carbon и boost.

Идеи?


person Per Ersson    schedule 26.03.2009    source источник
comment
Почему ты вообще хочешь это сделать? В отличие от Windows, операционная система в общем случае заботится о предотвращении запуска нескольких экземпляров приложения. В редком случае зачем его предотвращать?   -  person Chris Hanson    schedule 05.04.2009
comment
Рассматриваемое приложение является игрой. Запустив несколько копий игры на одном компьютере, игрок в некоторых ситуациях получит несправедливое преимущество перед другими игроками.   -  person Per Ersson    schedule 08.04.2009


Ответы (9)


Низкоуровневое решение — использовать flock().

Каждый экземпляр будет пытаться заблокировать файл при запуске, и если блокировка не удастся, то другой экземпляр уже запущен. Флоки автоматически освобождаются при выходе из вашей программы, так что не беспокойтесь об устаревших блокировках.

Обратите внимание, что какое бы решение вы ни выбрали, вам необходимо принять осознанное решение о том, что значит иметь «несколько экземпляров». В частности, если несколько пользователей запускают ваше приложение одновременно, это нормально?

person vasi    schedule 26.03.2009
comment
Спасибо, это решение подойдет. Файлы блокировки будут для каждого пользователя, чтобы не блокировать несколько пользователей на одном компьютере для одновременного запуска приложения. - person Per Ersson; 26.03.2009

В Snow Leopard это очень просто:

- (void)deduplicateRunningInstances {
    if ([[NSRunningApplication runningApplicationsWithBundleIdentifier:[[NSBundle mainBundle] bundleIdentifier]] count] > 1) {
        [[NSAlert alertWithMessageText:[NSString stringWithFormat:@"Another copy of %@ is already running.", [[NSBundle mainBundle] objectForInfoDictionaryKey:(NSString *)kCFBundleNameKey]] 
                         defaultButton:nil alternateButton:nil otherButton:nil informativeTextWithFormat:@"This copy will now quit."] runModal];

        [NSApp terminate:nil];
    }
}

См. http://blog.jseibert.com/post/1167439217/deduplicating-running-instances-or-how-to-detect-if для получения дополнительной информации.

person Jeff Seibert    schedule 22.09.2010
comment
В OS 10.8 выполнение этой проверки сразу при запуске в main.m не работает, потому что на этом этапе работающее приложение еще не находится в массиве (вероятно, регистрируется только в следующем цикле выполнения или около того), поэтому вместо › 1 вам нужно будет проверить на › 0. Чтобы не рисковать будущих версиях лучше явно проверять массив для текущего приложения: for (NSRunningApplication* runningApp in runningApplications) { if (![runningApp isEqual:[NSRunningApplication currentApplication]]) { // Alert and exit }} - person Andreas Zollmann; 18.09.2013
comment
Еще одна важная проблема: runningApplicationsWithBundleIdentifier возвращает запущенные приложения, соответствующие идентификатору пакета, но, что особенно важно, только те, которые принадлежат текущему пользователю (таким образом, эти решения не будут препятствовать одновременному запуску вашего приложения разными пользователями на этом компьютере). - person Andreas Zollmann; 18.09.2013

Есть загадочный ключ Info.plist под названием «Приложение запрещает несколько экземпляров», но, похоже, он у меня не работает. Я пишу приложение CLI и запускаю его из пакета. Возможно, это сработает в приложении с графическим интерфейсом, но я не пробовал.

person Raffi Khatchadourian    schedule 08.03.2012
comment
Этот ключ (LSMultipleInstancesProhibited) хорошо работает, когда приложение запускается из Launchpad или Finder. В качестве бонуса уже работающее приложение вынесено на передний план. Для меня это лучше, чем отображать диалоговое окно с ошибкой. Ключ не работает, когда приложение запускается из командной строки. - person Maf; 01.12.2015

Как уже упоминалось, приложения Cocoa обычно не позволяют запускать более одного экземпляра одновременно.

В общем, какао способ решить эту проблему посмотрите на запускаемые приложения в NSWorkspace. Это возвращает NSArray, содержащий словарь для каждого запущенного приложения. Вы можете прокрутить массив, чтобы увидеть, запущено ли уже искомое приложение. Я бы посоветовал вам использовать значение с ключом NSApplicationBundleIdentifier, который будет иметь значение, подобное «com.mycompany.myapp», а не искать имя. Если вам нужно найти идентификатор пакета для приложения, вы можете посмотреть его файл info.plist в пакете приложения.

person Jon Steinmetz    schedule 04.07.2009

Во-первых, это «Mac OS X» или «OS X». Нет такой вещи, как «OS/X».

Во-вторых, Mac OS X не поставляется с Boost; вам нужно будет связать его с вашим приложением.

В-третьих, большая часть Carbon недоступна в 64-битной версии. Это четкий сигнал о том, что эти части Carbon когда-нибудь исчезнут (когда Apple откажется от 32-битного оборудования). Рано или поздно вам придется либо переписать свое приложение с помощью Cocoa, либо отказаться от Mac.

Обычно пакет приложений в OS/X можно запустить только один раз, однако, просто переименовав пакет, одно и то же приложение можно запустить дважды.

Нет, не может. Запуск переименованного или перемещенного приложения просто активирует (выведет на передний план) уже запущенный процесс; он не запустит новый второй процесс рядом с первым.


Есть несколько способов определить, запущено ли приложение. В каждом случае вы делаете это при запуске:

  1. Используйте NSConnection Cocoa для регистрации соединения с одним постоянным именем. Это не удастся, если имя уже зарегистрировано. (Вы можете использовать Foundation из приложения Carbon; это набор приложений, с которым вы должны быть осторожны.)
  2. Используйте диспетчер процессов, чтобы просмотреть список процессов на наличие процессов, идентификатор пакета которых совпадает с тем, который вы ищете. Идентификатор пакета не является неизменяемым, но его сложнее изменить, чем имя файла или местоположение.
  3. Если вы хотите увидеть, когда кто-то запускает вторую копию вас, вы можете использовать CFNotificationCenter:

    1. Add yourself as an observer for “com.yourdomain.yourappname.LaunchResponse”.
    2. Разместите уведомление под именем «com.yourdomain.yourappname.LaunchCall».
    3. Добавьте себя в качестве наблюдателя для «com.yourdomain.yourappname.LaunchCall».

    В обратном вызове наблюдения для уведомления о вызове разместите уведомление об ответе.
    В обратном вызове наблюдения для уведомления об ответе выйдите.

    Таким образом, когда первый процесс запустится, он вызовет вызов и не получит ответа; когда запускается второй процесс, он вызывает, получает ответ от первого процесса и завершает работу в знак уважения к первому.

person Peter Hosey    schedule 26.03.2009
comment
Я думаю, что он имел в виду копирование вместо переименования. В любом случае, вы можете открыть второй экземпляр, используя open -n TextEdit.app - person Georg Schölly; 26.03.2009
comment
Или запустите -m, если у вас установлен запуск Николаса Райли. - person Peter Hosey; 26.03.2009

Это комбинация ответов Романа и Джеффа для Swift 2.0: если другой экземпляр приложения с тем же идентификатором пакета уже запущен, покажите предупреждение, активируйте другой экземпляр и закройте повторяющийся экземпляр.

func applicationDidFinishLaunching(aNotification: NSNotification) {
    /* Check if another instance of this app is running. */
    let bundleID = NSBundle.mainBundle().bundleIdentifier!
    if NSRunningApplication.runningApplicationsWithBundleIdentifier(bundleID).count > 1 {
        /* Show alert. */
        let alert = NSAlert()
        alert.addButtonWithTitle("OK")
        let appName = NSBundle.mainBundle().objectForInfoDictionaryKey(kCFBundleNameKey as String) as! String
        alert.messageText = "Another copy of \(appName) is already running."
        alert.informativeText = "This copy will now quit."
        alert.alertStyle = NSAlertStyle.CriticalAlertStyle
        alert.runModal()

        /* Activate the other instance and terminate this instance. */
        let apps = NSRunningApplication.runningApplicationsWithBundleIdentifier(bundleID)
        for app in apps {
            if app != NSRunningApplication.currentApplication() {
                app.activateWithOptions([.ActivateAllWindows, .ActivateIgnoringOtherApps])
                break
            }
        }
        NSApp.terminate(nil)
    }

    /* ... */
}
person seb    schedule 03.10.2015

Как насчет IPC? Вы можете открыть сокет и договориться с другим запущенным экземпляром. Однако вам нужно быть осторожным, чтобы это работало, если оба приложения запускаются одновременно.

Я не могу предоставить вам пример кода, так как я (пока что, но скоро буду) его не использовал.

person Georg Schölly    schedule 26.03.2009
comment
Будьте осторожны, чтобы не нарушить способность вашего приложения работать под несколькими пользователями одновременно. Приложение, которое закрывается, когда его уже использует другой пользователь, не работает. - person Peter Hosey; 26.03.2009

Это версия seb для Swift 3.0: если другой экземпляр приложения с таким же идентификатором пакета уже запущен, показать предупреждение, активировать другой экземпляр и закрыть повторяющийся экземпляр.

func applicationDidFinishLaunching(aNotification: NSNotification) {
    /* Check if another instance of this app is running. */
    let bundleID = Bundle.main.bundleIdentifier!
    if NSRunningApplication.runningApplications(withBundleIdentifier: bundleID).count > 1 {
         /* Show alert. */
         let alert = NSAlert()
         alert.addButton(withTitle: "OK")
         let appName = Bundle.main.object(forInfoDictionaryKey: kCFBundleNameKey as String) as! String
         alert.messageText = "Another copy of \(appName) is already running."
         alert.informativeText = "This copy will now quit."
         alert.alertStyle = NSAlert.Style.critical
         alert.runModal()

         /* Activate the other instance and terminate this instance. */
         let apps = NSRunningApplication.runningApplications(withBundleIdentifier: bundleID)
             for app in apps {
                  if app != NSRunningApplication.current {
                      app.activate(options: [.activateAllWindows, .activateIgnoringOtherApps])
                      break
                  }
             }
                NSApp.terminate(nil)
         }   
       /* ... */
}
person Mavro    schedule 10.10.2017
comment
Это не работает, когда приложение запускается разными пользователями. В этом случае NSRunningApplication.runningApplications не возвращает экземпляры другого пользователя. - person akim; 29.05.2018

определить, запущено ли приложение с таким же идентификатором пакета, активировать его и закрыть то, что запускается.

- (id)init method of < NSApplicationDelegate >

    NSArray *apps = [NSRunningApplication runningApplicationsWithBundleIdentifier:[[NSBundle mainBundle] bundleIdentifier]];
    if ([apps count] > 1)
    {
        NSRunningApplication *curApp = [NSRunningApplication currentApplication];
        for (NSRunningApplication *app in apps)
        {
            if(app != curApp)
            {
                [app activateWithOptions:NSApplicationActivateAllWindows|NSApplicationActivateIgnoringOtherApps];
                break;
            }
        }
        [NSApp terminate:nil];
        return nil;
    }
person Roman Solodyashkin    schedule 21.05.2014