dotnet core: второй подход к снаряду

Written by elwood

Полгода назад я впервые попробовал .NET Core. В тот раз даже запустить helloworld не получилось, но это потому что я запускал его на Ubuntu 15.10, а формально поддерживалась только 14.04. С тех пор прошло немало времени, вышел официальный релиз 1.0 (в котором разработчики отпилили поддержку --native). Добавилась документация на официальном сайте. И я подумал – а почему бы и не попробовать ещё разок ? Пробовать решил на Console Framework. В принципе, это несложный проект, состоящий из нескольких модулей. Внешних зависимостей у него нет.

Установка .NET Core

Не вызвала проблем. Ставил на Ubuntu 14.04 по официальному гайду.

Отдельные модули

Для начала я взялся за те модули, которые не зависят от других. Для того, чтобы .NET Core собрал модуль, нужно, чтобы рядом с кодом модуля лежал файл project.json. Надо где-то получить шаблон этого файла. Я сделал 2 пустых проекта: один командой dotnet new, другой командой dotnet new -t Lib. Почему-то в них оказались разными ссылки на дефолтные библиотеки. Для исполняемого модуля было указано “netcoreapp1.0”:

{
  "version": "1.0.0-*",
  "buildOptions": {
    "debugType": "portable",
    "emitEntryPoint": true
  },
  "dependencies": {},
  "frameworks": {
    "netcoreapp1.0": {
      "dependencies": {
        "Microsoft.NETCore.App": {
          "type": "platform",
          "version": "1.0.0"
        }
      },
      "imports": "dnxcore50"
    }
  }
}

А для библиотеки “netstandard1.6”

{
  "version": "1.0.0-*",
  "buildOptions": {
    "debugType": "portable"
  },
  "dependencies": {},
  "frameworks": {
    "netstandard1.6": {
      "dependencies": {
        "NETStandard.Library": "1.6.0"
      }
    }
  }
}

Чтобы разобраться, что это за наборы библиотек и чем они отличаются, я сходил и почитал документацию, и ещё чуть-чуть. К сожалению, понимания это не добавило. Я просто попробовал оба набора, и в netstandard1.6 вообще ничего не завелось, а netcoreapp всё-таки получилось заставить работать. В общем, взял тот кусок “frameworks”, который про netcoreapp1.0.

Несовместимость

При сборке были ошибки. Например, почему-то отсутствовал метод Type.GetCustomAttributes(). Как выяснилось, в .NET 4.5 добавили промежуточный класс TypeInfo, и теперь надо дёргать Type.GetTypeInfo().GetCustomAttributes() – это новый extension-метод в System.Reflection. Аналогично пришлось поменять вызовы IsGenericType, IsEnum, GetEnumNames и GetEnumValues.

Assembly.Load(string assemblyName) пришлось заменить на AssemblyLoad(new AssemblyName(string assemblyName)).

Пропал ArrayList (который не-generic). Пришлось везде поменять на List<object>.

Пропал CharSet.Auto, заменил на CharSet.Unicode (это в интеропе).

А ещё пропал метод Assembly.GetExecutingAssembly(). Теперь его следует вызывать через typeof(SomeTypeInAssembly).GetTypeInfo().Assembly.

Environment.PlatformID пропал совсем (хз что делать, закостылил пока через условную компиляцию).

Не было обнаружено также свойство EventWaitHandle.SafeWaitHandle (тоже удалил через условную компиляцию, т.к. это был windows-specific код).

А ещё пропал класс ApplicationException. Не то, чтобы я сильно по нему скучал (по факту наличие таких исключений само по себе является проблемой в коде), но как-то все эти траублы не добавляют радости. Код действительно приходится портировать, несовместимостей в API очень много.

Многомодульный проект

Когда получилось добиться успешной сборки двух не зависящих ни от чего модулей, я приступил к настройке третьего модуля, который использует первые два. Почему-то в официальном гайде указано, что в родительском каталоге нужно завести файл global.json, однако это совершенно не нужно. Всё работает и без него. Нужно лишь явно указать название и версию каждому модулю в их project.json (“name” и “version”), а в project.json зависящего от них модуля прописать их в dependencies:

  "dependencies": {
    "Binding": {
	"version": "1.0.0",
	"target": "project"
    },
    "Xaml": {
	"version": "1.0.0",
	"target": "project"
    }
  }

После этого при попытке собрать этот проект dotnet автоматически будет собирать и зависимости.

entry points

Осталось настроить сборку ещё одного модуля – собственно исполняемое приложение. Тут поджидала ещё одна проблема. У меня этот модуль содержал несколько классов Program, в каждой из которых был свой метод Main(). В mono и обычном .net можно было спокойно указать класс, который использовался бы как EntryPoint. В .net core почему-то эта штука вообще не работает. По документации, можно определить название метода, однако судя по всему, эта директива вообще игнорируется. Пришлось просто исключить все “лишние” файлы с точками входа:

"exclude": [
  "RadioButtons/**",
  "TreeView/**",
  "Commands/**",
  "AsyncUIUpdate/**",
  "TabControl/**",
  "MainMenu/**",
  "CheckBoxes/**"
],

Заодно узнал, как прописывать embedded ресурсы:

"resource": [
  "**/*.xml"
],

После всех этих манипуляций проект наконец собрался и даже запустился (когда я подложил libtermkey.so), однако пока рано радоваться:

dotnetcore-consoleframework

Ничего не работает, похоже, что с interop’ом в .NET Core большие проблемы.

Кстати, забавно, что dotnet build не создает exe-файлы для исполняемых модулей, делает только dll.

Документация

Документация очень плоха. Официальные гайды слабы. Взять например project.json reference. Дефолтные значения атрибутов не указаны (а в исходной версии этой доки они были!), часть информации устарела. В сети тоже много всего разного. Часто найденные рецепты противоречат друг другу. И не очень понятно, где искать инфу про то, что есть в библиотеке, а чего нет. Был бы нормальный декомпилятор .NET под линукс, было бы проще, но его, похоже, нет. Встроенный в MonoDevelop декомпилятор не справляется (просто не может открыть сборку). Вкупе с другими многочисленными проблемами, всё это делает меня грустить. Я уже не говорю о тулинге (чтобы не пришлось руками редактировать json-файлики). В общем, пока использовать .NET Core рановато. Буду ждать очередных релизов и выхода Rider.

В завершение прикладываю diff изменений (в отдельной ветке), чтобы можно было посмотреть примеры файлов project.json.

Редиректы из-под nginx

Written by elwood

Предположим у нас есть nginx и проксируемая приложунька.

Nginx отвечает на запросы http://frontend.ru:8080/foo и проксирует их локально на http://localhost:8090/foo

Если приложение хочет ответить редиректом на /somewhere, то пользователь должен увидеть Location: http://frontend.ru:8080/somewhere

Тут есть нюанс. Поведение зависит от того, как мы передаём заголовок Host в бекенд. Можно написать так:

location / {
    proxy_pass http://localhost:8080;

    proxy_set_header HOST $host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-NginX-Proxy true;

    proxy_redirect off;
}

И тогда бекенду будет приходить заголовок Host: frontend.ru. Если приложение не экзотическое, оно на такой заголовок сформирует редирект без указания порта (т.к. про порт ему и узнать неоткуда). В результате ответ будет такой: Location: http://frontend.ru/, что совсем не круто. Нам-то нужен нормальный Location ! С портом !

В общем, есть два пути решения этой проблемы.

Первый способ: настроить rewrite для заголовков ответа

proxy_redirect http://$host/ http://$host:$server_port/;

Эта штука будет перехватывать ответ приложения и заменять в заголовках Location, дописывая порт.

И второй: использовать в директиве proxy_set_header HOST $host; переменную $http_host вместо переменной $host:

proxy_set_header HOST $http_host;

$http_host выгодно отличается от просто $host тем, что содержит порт (хехе), и всё сразу начинает работать. Приложение формирует правильные редиректы, их даже и не приходится перезаписывать, пользователи довольны, донатят тысячи и тысячи в валюте.

strace и setuid

Written by elwood

Игрался сегодня с strace. Запускал в нём ping. Но пинг, запущенный таким образом, почему-то отказывался работать, возвращая ошибку:

elwood@elwood:~$ strace -o out ping ya.ru
ping: icmp open socket: Operation not permitted

При этом просто пинг (без strace) вполне себе работал.

Почему так ?..

Выяснилось, что дело в особых правах на ping. Прикол в том, что для пинга установлен бит setuid:

elwood@elwood:~$ ls -l `which ping`
-rwsr-xr-x 1 root root 44168 May  7  2014 /bin/ping

И при запуске он по сути работает от рута. А strace такими правами не обладает, и запуская ping через него, я получал ошибку. Если убрать setuid бит с /bin/ping, то при запуске ping я тоже буду получать ошибку:

elwood@elwood:~$ sudo chmod u-s `which ping`
elwood@elwood:~$ ls -l `which ping`
-rwxr-xr-x 1 root root 44168 May  7  2014 /bin/ping
elwood@elwood:~$ ping ya.ru
ping: icmp open socket: Operation not permitted

Решить проблему можно либо добавив setuid бит для strace (chmod u+s `which strace`), либо запуская strace через sudo.