Спящий режим и использование InternetSharing на Macbook

в 20:01, , рубрики: mac os x, php

В статье описан способ автоматического отключения/включения InternetSharing, который препятствует переходу Macbook в спящий режим или вновь пробуждает ноутбук, если вручную всё же его усыпить.

Предистория

Работаю на Macbook, в помещении 3G отсутствует. При подключении ноутбука к сети через RJ-45, удобно использовать InternetSharing (далее IS), так как некоторая работа связанна с мобильными устройствами. Однажды настроив IS забыл о проблеме с интернетом на телефоне, однако стал замечать, когда прихожу на работу, ноутбук включен. Первый раз не придал значения. На второй раз решил посмотреть в логах, когда он же он включается. Оказалось, что включается он спустя пару минут, как я его перевожу в спящий режим.

Как оказалось службой IS блокирует сон в то время как он активен.
Команда

pmset -g assertions

укажет, что виновен именно IS:
pid 70000: [0x000…000] PreventSystemSleep named: «com.apple.InternetSharing».

Я стал искать как люди решают подобную проблему, но натыкался только на «отключите службу».
Пришлось решать проблему самому.

Что же делать?

Как оказалось – настроенная служба IS запускается и останавливается, используя файл:
/System/Library/LaunchDaemons/com.apple.InternetSharing.plist

Решение оказалось таким: включать службу при пробуждении и выключать при переходе в режим сна, то есть использовать такие команды:

  • для включения
    sudo launchctl load -F /System/Library/LaunchDaemons/com.apple.InternetSharing.plist

    Флаг -F обозначает форсированный старт, так как служба имеет флаг <key>Disabled</key> <true/>;

  • для выключения
    sudo launchctl unload /System/Library/LaunchDaemons/com.apple.InternetSharing.plist

    .

Но для этих операций требуется ввод пароля пользователя с правами администратора.

Осталось узнать «Как запускать программы при пробуждении и при переходе в сон» и при этом не требовали бы ввод пароля.

Решение

Первое – сделаем свою службу, которая будет запускаться с правами администратора – то есть не требовать ввод пароля для launchctl.
Второе – будет отправлять нашей службе команды при пробуждении и переходе в сон, соответственно start и stop.

Итак, служба будет прослушивать порт, например, 10001 и ждать команды start и stop.
Напишем службу на PHP (полный код ниже), соответственно для запуска IS и его остановки будут производиться следующие действия:
для запуска:

shell_exec('launchctl load -F /System/Library/LaunchDaemons/com.apple.InternetSharing.plist');

для остановки:

shell_exec('launchctl unload /System/Library/LaunchDaemons/com.apple.InternetSharing.plist');

Для запуска нашей службы надо поместить файл com.username.InternetSharing.plist в папку /Library/LaunchDaemons/.
Запустить командой:

sudo launchctl load -F /Library/LaunchDaemons/com.username.InternetSharing.plist

Для того чтобы отловить события пробуждения и переход в сон воспользуемся программой Scenario. Напишем для неё на AppleScripts два скрипа, которые будут вызывать клиентскую программу(полный код ниже) с параметром start или stop.

Скрипт запуска
do shell script "/usr/bin/php /Users/username/CheckMac/client.php start"
Скрипт остановки
do shell script "/usr/bin/php /Users/username/CheckMac/client.php stop"

Поместим их в специальные директории программы Scenario:
"~/Library/Scenario/Wake Scripts" и "~/Library/Scenario/Sleep Scripts".

Результат

При включении программа Scenario запускает скрипт из папки "Wake Scripts", который запускает Клиентскую программу с параметром start. Клиентская программа связывается с нашей службой, которая может запустить службу IS, так как обладает достаточными правами.
При переходе в режим сна выполняется аналогичная операция, только с папкой "Sleep Scripts" и с параметром stop.

Код

Служба

/Users/username/CheckMac/listen.php


<?php // PHP 5.3.10
// Исходные настройки
error_reporting(E_ALL ^ E_WARNING);
set_time_limit(0);
ob_implicit_flush();
$port = 10001;
$sock = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
socket_bind($sock, "127.0.0.1", $port) or die('Port listened'."n");
socket_set_option($sock, SOL_SOCKET, SO_KEEPALIVE, 1);
socket_set_nonblock($sock);
socket_listen($sock, 1000);
$clients = array($sock);
$tarr = array();
$iparr = array();

$current = 0;

while (true) {
  $read = $clients;
  if (count($tarr) > 0) foreach ($tarr as $ind => $tim) {
    // Если подключение не проявляет активность 10 секунд - отключать
    if ((time() - $tim) > 10) {
      socket_close($read[$ind + 1]);
      unset($clients[$ind+1]);
      unset($iparr[$ind]);
      unset($tarr[$ind]);
      echo "Disconnect client.n";
      continue;
    }
  }
  // Если нет клиентов - ждать
  if (socket_select($read, $write = NULL, $except = NULL, 1) < 1)
    continue;
  // Новое подключение
  if (in_array($sock, $read)) {
    $current ++;
    $clients[$current] = $newsock = socket_accept($sock);
    socket_write($newsock, "<OK>n");
    socket_getpeername($newsock, $ip);
    echo "New connection from ip: {$ip}n";
    $key = array_search($sock, $read);
    $iparr[$current-1] = $ip;
    $tarr[$current-1] = time();
    unset($read[$key]);
    $read[$current] = $newsock;
    continue;
  }
  // Проверить подключения
  foreach ($read as $index => $read_sock) {
    $data = socket_read($read_sock, 1024);
    if ($data === false) {
      $key = array_search($read_sock, $clients);
      unset($clients[$key]);
      unset($iparr[$key - 1]);
      echo "Disconnect client.n";
      unset($tarr[$key - 1]);
      continue;
    }
    $data = trim($data);
    // Команды для обработки
    if (!empty($data)) {
      echo $iparr[$index - 1] . "[$index] - $datan";
      $tarr[$index - 1] = time();
      switch ($data) {
        case "quit":
          socket_close($read_sock);
          $key = array_search($read_sock, $clients);
          unset($clients[$key]);
          unset($iparr[$key - 1]);
          echo "Disconnect client.n";
          unset($tarr[$key - 1]);
          break;
        case "ping":
          socket_write($read_sock, "<PONG> " . time() . "n");
          break;
        // Получили команду на запуск IS
        case 'start':
          $s = shell_exec('launchctl list');
          if(strpos($s, 'com.apple.InternetSharing') === false){
            shell_exec('launchctl load -F /System/Library/LaunchDaemons/com.apple.InternetSharing.plist');
          }
          break;
        // Получили команду на останов IS
        case 'stop':
          $s = shell_exec('launchctl list');
          if(strpos($s, 'com.apple.InternetSharing') !== false){
            shell_exec('launchctl unload /System/Library/LaunchDaemons/com.apple.InternetSharing.plist');
          }
          break;
      }
      if ($data === "close") {
        socket_close($sock);
        break(2);
      }
    }
  }
}
socket_close($sock);

com.username.InternetSharing.plist

/Library/LaunchDaemons/com.username.InternetSharing.plist

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
  <key>Label</key>
  <string>com.username.InternetSharing</string>
  <key>ProgramArguments</key>
  <array>
    <string>/usr/bin/php</string>
    <string>/Users/username/CheckMac/listen.php</string>
  </array>
  <key>RunAtLoad</key>
  <true/>
</dict>
</plist>

Клиентская программа

/Users/username/CheckMac/client.php


<?php
$fp = fsockopen("127.0.0.1", 10001, $errno, $errstr, 30);
if (!$fp) {
  echo "$errstr ($errno)<br />n";
} else {
  $out = @$argv[1]."rn";
  fwrite($fp, $out);
  fgets($fp, 128);

  sleep(1); // Иначе команды воспринимаются как единый текст

  $out = "quitrn";
  fwrite($fp, $out);
  fgets($fp, 128);

  fclose($fp);
}

Скрипт запуска IS (Не текстовой файл – создаётся программой AppleScripts)

~/Library/Scenario/Wake Scripts/wake.scpt

do shell script "/usr/bin/php /Users/username/CheckMac/client.php start"

Скрипт оастнова IS

~/Library/Scenario/Sleep Scripts/sleep.scpt

do shell script "/usr/bin/php /Users/username/CheckMac/client.php stop"

Автор: truekenny

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js