Меня давно занимала мысль как в Linux-е запускать программы на Java без вспомогательных Bash скриптов. Я не видел приемлемого решения, если не считать способ «bash script payload», когда в конец скрипта помещается бинарный файл.

Но на прошлой неделе случайно наткнулся на модуль ядра binfmt_misc, с помощью которого можно перехватить исполнение файла по его magic number. Для этого через update-binfmts добавляется собственный обработчик для получения имени исполняемого файла и аргументов пользователя.

Первое открытие


Как оказалось в моей Ubuntu 16.04 уже зарегистрирован обработчик для JAR файлов:

update-binfmts --display
...
jar (enabled):
     package = openjdk-8
        type = magic
      offset = 0
       magic = PK\x03\x04
        mask = 
 interpreter = /usr/bin/jexec
    detector = 

Отдав команду chmod +x foo.bar я радостно потирал руки, но реальность оказалось сурова — запуск ./foo.jar выдал следующее:

invalid file (bad magic number): Exec format error

Погуглив, я нашел обросший мхом баг bugs.java.com/bugdatabase/view_bug.do?bug_id=6401361 Как оказывается сборка через Maven не добавляет «0xcafe» в начало JAR файла. Не менее безответственно ведет себя и плагин maven-assembly-plugin. Что не нравится /usr/bin/jexec, зарегистрированному обработчику по умолчанию.

Погуглив еще, я нашел решение проблемы через установку пакета jarwrapper. После установки добавляется новый обработчик /usr/bin/jarwrapper и страховка /usr/bin/jardetector (проверяет по META-INF что это действительно JAR). Но изучив код обработчика мне не понравилась куча лишней работы, которую делает скрипт запуская множество вспомогательных программ.

Поэтому решением стал собственный обработчик:

#!/bin/sh
#/usr/bin/jarinvoke

JAR=$1
shift

exec java -jar $JAR $@

Дальше открываем файл sudo gedit /var/lib/binfmts/jar и регистрируем обработчик заменив строчку с /usr/bin/jexec на /usr/bin/jarinvoke. На самом деле это плохое решение и лучше создать собственную группу (об этом ниже), но для первичного понимания сойдет.

Для вступления изменений в силу может потребоваться выполнить:

sudo update-binfmts --disable jar && sudo update-binfmts --enable jar

После чего можете запускать JAR файлы как любые другие исполняемые файлы.

Исполняемые классы


Теперь можно идти дальше и сделать из Java классов исполняемые файлы, где jarwrapper не сможет помочь. Обработчик будет работать только для классов с пакетом по умолчанию (т.е. классы с отсутствующим package заголовком). Может можно сделать и лучше, но мне хватило такой функциональности для «скриптования» на Java:

#!/bin/sh
# /usr/bin/clsinvoke

CLASS_FILE=$1
shift

ABSOLUTE_PATH=`readlink -f $CLASS_FILE`

CLASS=`basename $ABSOLUTE_PATH`
CLASS=${CLASS%.*}
CLASSPATH=`dirname $ABSOLUTE_PATH`

exec java -cp $CLASSPATH $CLASS $@

После чего регистрируем собственный обработчик (этим же способом можно создать новый обработчик для JAR-ов не редактируя /usr/bin/jexec):

sudo update-binfmts --package clsinvoke --install clsinvoke /usr/bin/clsinvoke --magic '\xca\xfe\xba\xbe'

Тестируем:

public class HelloWorld {
    public static void main(String[] args) {
        System.out.println("Hello, World");
    }
}

javac HellWorld.java
chmod +x HelloWorld.class
./HelloWorld.class
Hello, World

Можно пойти и дальше, сделав более сложный обработчик, который по импорту классов будет определять какие библиотеки добавить в CLASSPATH из ~/.m2, но это отдельная история. Сейчас интересен взгляд со стороны, замечания, дополнения, если таковые есть. После чего думаю оформить это в deb пакет и выложить всё на гитхабе.
Поделиться с друзьями
-->

Комментарии (20)


  1. aol-nnov
    11.07.2017 19:22

    а практическая ценность-то? because we can? :)

    есть еще, например, такой подход: https://docs.spring.io/spring-boot/docs/current/reference/html/executable-jar.html но даже он мне сомнителен. зачем, если можно просто java -jar или вообще задеплоить куда-то?!


    1. Foror
      11.07.2017 19:57
      +1

      >а практическая ценность-то? because we can? :)
      Делать консольные приложения/утилиты на джаве. Я именно из-за нового проекта углубился в эту тему, чтобы избежать скрипта на баше рядом с JAR-ом.


      1. sshikov
        11.07.2017 20:09
        +1

        Из своего опыта — внутри кода на groovy можно спокойно написать обработчик — потому что это текст. И он будет запускаться ровно как питон или баш. Для именно консольных утилит — это самое то.

        #!/какой-то путь/.../groovy — я вот про это, если что

        Все остальные продукты как правило все-таки требуют несколько больше настроек, или работают скорее в режиме сервиса, т.е. требуют опять же настройки, чтобы запускаться/останавливаться при перезагрузке ОС в том числе.


        1. Foror
          11.07.2017 20:17

          Я пишу консольного клиента для УТМ и пишу на джаве, а не на груви. Все настройки хранятся в отдельном файле, которые клиент считывает по заранее известному пути ~/.foo-client/… (как это обычно принято в современном ПО под линукс) Как итог можно просто запускать клиента, как любой другой бинарник, добавив его в PATH.

          Так что пусть это и маленькая, но «удобность», особенно для меня как перфекциониста )


          1. sshikov
            11.07.2017 20:22
            +1

            Я вам ровно про такой случай и рассказываю. Вы можете не писать на груви (подставьте сюда что угодно другое, кстати, jython, кложа и т.п. вполне для такого же сценария годятся) основную часть приложения — вы просто заменяете баш на один из скриптовых jvm языков. И это получается очень удобно. У меня был случай, когда примерно 1000 строк шелла были переписаны на груви, сократившись при этом раза в три.


            1. Foror
              11.07.2017 20:33
              +3

              Это всё понятно, но я же пишу на джаве, а не на «подставьте сюда что угодно другое». К тому же скриптовые языки я недолюбливаю.


              1. Urgen
                11.07.2017 22:43

                Напиши запускатор на джаве, потом убери из него все точки с запятой и смени расширение на .groovy)


                1. Foror
                  12.07.2017 07:22

                  А когда нужно дописать код, сменить расширение на .java, дописать код, убрать все точки с запятой и снова заменить расширение на .groovy? )

                  Оу, щит, я же забыл, перед этим нужно добавить точки с запятой, перед переименованием на .java…

                  Наверное это удобно когда платят за строки кода час, а не когда у вас собственный бизнес.


                  1. Urgen
                    12.07.2017 09:52

                    1. Не думаю, что в запускаторе должен быть ещё какой-то функционал.
                    2. Зачем менять разрешение? Пиши в .groovy-файле в java-стиле, просто точки с запятой не ставь


                    1. fokan
                      12.07.2017 11:10

                      Никто в принципе не мешает их таки ставить.


  1. sshikov
    11.07.2017 20:06

    Аналогичный вопрос — а зачем? Ну вот зачем нужен dropship, я допустим понимаю. А такой подход — как-то сомнительно. Чего вы хотели добиться по сравнению скажем с произвольно взятым wrapper-ом?


    1. Foror
      11.07.2017 20:29

      >Чего вы хотели добиться
      Хочу писать скрипты на джаве в Eclipse'e, когда баша не хватает, а питон не нравится ) У меня в загашнике проект по типу Yeoman.

      >с произвольно взятым wrapper-ом?
      Не люблю лишние сущности, если от чего-то можно избавиться и забыть, то я избавляюсь и забываю.


  1. Pasha13666
    11.07.2017 20:52
    +2

    Еще jar-файл можно запускать как скрипт:

    $ echo '#!/usr/bin/java -jar' >test.jar
    $ cat /path/to/file.jar >>test.jar 
    $ chmod +x test.jar 
    $ ./test.jar
    


    1. Foror
      11.07.2017 20:57
      +2

      Тут могут быть лишние хлопоты, если JAR подписывается во время сборки… Да и опять же для каждого JAR-а нужно кастомную сборку делать.


  1. Borz
    12.07.2017 03:08

    эм, а чем вам "jar cvfm ..." не угодил (это если не перечислять плагины для Maven/Gradle)?


  1. jamakasi666
    12.07.2017 12:27

    Когда учился еще в институте препод подкинул идею на курсач какраз такого характера. После небольшого мозгового штурма решение получилось крайне простым и удобным, составили и реализовали примерно следующее ТЗ сами себе.
    1) Юниксы ищут программу в /bin /sbin /usr/bin и /usr/sbin.
    2) Необходимо учесть что программа при запуске может потребовать определенную версию библиотек или jre.
    3) Целевая программа должна легко обновляться.
    4) Решение которое будет запускать должно быть простым и легким.

    Сделали так:
    1) определили что конфиги будут лежать в /etc/jarlauncher/
    2) сами jar'ники в /usr/jarlauncherbin/
    3) Написали на с++ маленькую програмку которая:
    -при запуске смотрит какое у нее имя
    -по полученному имени пытается найти в /etc/jarlauncher/имя_программы.conf
    -если найти не удалось то происходит попытка запуска «java -jar /usr/jarlauncherbin/имя_программы.jar args[]»
    -иначе если удалось найти /etc/jarlauncher/имя_программы.conf то в нем следующее:
    *jar=«путь до jar» -обязательное
    *jre=«путь до java» — опциональное
    *classpath=«перечислены библиотеки необходимые для работы» -опциональное
    *jreenv=«особые параметры запуска jvm» — опциональное
    *logfile=«путь до файла в который проксируется вывод программы» -опционально
    -запускает согласно параметров из конфига.

    Соответсвенно все что было необходимо это взять эту прогу и скопировать в /usr/bin/, назвать как надо, с таким же названием сделать конфиг с параметрами или положить jar файлик в /usr/jarlauncherbin/. Все предельно тупо и просто но работало отлично и удобно. Позже слышал что кто то из студентов другого курса написал плагин к мавену который автоматизировал все действия.


  1. saboteur_kiev
    12.07.2017 19:31

    А если пожаловаться в мавен, или если хватает сил, даже сразу пулл реквест?


  1. salas
    12.07.2017 20:29
    +1

    chmod +x foo.bar

    Красивый случай победы автокоррекции: не так-то просто убедить человеческие руки, что foo.jar — не ошибка.


  1. paimei
    18.07.2017 10:07

    А Вы когда-нибудь ядро Linux перекомпилировали самостоятельно?
    Я помню, еще в дремучие 2000-е годы (а то и в конце 90-х) при конфигурировании ядра Linux была такая опция: Support for Java binaries.
    Если ее включить — то достаточно было потом сделать только «chmod +x MyJavaClass.class» — и он запускался непосредственно, безо всяких скриптов.
    С тех пор уже много воды утекло, конечно… но все же, это, часом, не то, что Вы искали?


    1. paimei
      18.07.2017 10:22

      Вот краткий HOWTO про то, о чем что я писал выше:
      http://www.nirendra.net/cms/java/linux

      Но если серьезно, то это все игрушки.
      Как, например, Вы будете устанавливать максимальный размер кучи, разные системные переменные, настройки GC, режим дебага и т.п.?