вторник, 16 июля 2024 г.

NGINX и PHP-FPM. "Грабли" с doc_root

Вводные. 

Unix-like система. Стоит Nginx v1.25.3 и PHP-FPM v8.3.2
Теперь важно. В php.ini параметр doc_root = "/opt/share/www". 
Допустим наш сайт лежит в /opt/share/www/site1. Там есть index.php следующего содержания:
<?php echo __FILE__;
Ну, то есть, примитивизм - в случае обращение к этому файлу, он будет выдавать свой абсолютный путь.

Конфигурация nginx выглядит примерно так:

	location /api/ {
		root /opt/share/www/site1;
		default_type "application/json";
		autoindex off;

		fastcgi_pass unix:/opt/var/run/php8-fpm.sock;
		include fastcgi_params;
		
		fastcgi_split_path_info ^(/api)(/.+)$;
		fastcgi_param SCRIPT_FILENAME $document_root/index.php;
		fastcgi_param PATH_INFO $fastcgi_path_info;
		fastcgi_param REQUEST_URI $fastcgi_path_info;
	}

Так вот в такой ситуации при обращении по адресу http://site1/api/blablabla (подразумевается что site1 маппится на нужный для nginx IP и порт) вам в браузер будет выдаваться "No input file specified". При этом в error.log будет что-то типа "Unable to open primary script: /opt/share/www/site1/index.php (No such file or directory)".

Если заглянуть в nginx/fastcgi_params, то мы там кстати увидим, что никакой SCRIPT_FILENAME не задаётся, а задаётся SCRIPT_NAME. Может это его надо переопределить? 

Допишем его в конфиг nginx аналогично SCRIPT_FILENAME:

  
  include fastcgi_params;
  ...
  fastcgi_param SCRIPT_FILENAME $document_root/index.php;
  fastcgi_param SCRIPT_NAME $document_root/index.php;
  ...

Ну и, понятное дело, перезагрузим конфигурацию nginx через "nginx -s reload"

Это ничего не даст. Ошибка будет та же.

Если же убрать из конфига nginx передачу параметра SCRIPT_FILENAME, то ошибка будет другая: в браузере будет "File not found", а в error.log - "Primary script unknown".

Ну ОК. Вы скажете: "Чувак, у тебя задан doc_root, вот и надо указывать путь к index.php не абсолютный путь относительно корня, а от /opt/share/www !"

Ха, если бы все было так просто. Ну смотрите. Выкидываем $document_root из параметров и делаем так:

  
  include fastcgi_params;
  ...
  fastcgi_param SCRIPT_FILENAME /site1/index.php;
  fastcgi_param SCRIPT_NAME /site1/index.php;
  ...
Следуя логике, у нас /opt/share/www соединится с /site1/index.php, и мы получим абсолютный путь. 
Перечитываем конфигурацию nginx. 
Переходим по http://site1/api/blablabla. 
Получаем "Primary script unknown"... 

И как бы мы не извращались передавая только SCRIPT_FILENAME или только SCRIPT_NAME - ошибка та же. 

А теперь, собственно, в чём прикол? Ну и решение. 
SCRIPT_FILENAME - это параметр для php-fpm. И он НЕ зависит от doc_root в php.ini. 
SCRIPT_NAME - это параметр для php. И он зависит от doc_root в php.ini. 

Таким образом, правильной конфигурацией nginx будет:
  
  include fastcgi_params;
  ...
  fastcgi_param SCRIPT_FILENAME $document_root/index.php;
  fastcgi_param SCRIPT_NAME /site1/index.php;
  ...
В таком случае, в нашем примере, в конечном итоге оба параметра будут ссылаться на /opt/share/www/site1/index.php 

ЗЫ. Причем, что интересно, SCRIPT_FILENAME может указывать на другой php-файл (может быть даже не на php-файл - не проверял) - главное чтобы этот файл существовал. А выполнится при этом тот файл, который указан именно в SCRIPT_NAME. 

ЗЗЫ. Естественно, если просто закоментировать doc_root в php.ini, то первоначальная конфигурация будет работать.

понедельник, 17 сентября 2018 г.

PostgreSQL. Миграция базы на новую версию.

Для разработки локально я пользуюсь OpenServer. Периодически у него выходят новые версии, в которых добавляются новые версии модулей, в частности PostgreSQL. В принципе OpenServer тут не причём, и все ниженаписанное относится просто к переезду на новую версию PostgreSQL.

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

Собственно в чем проблема? Проблема в том, что разные версии PostgreSQL хранят данные в разных папках и тупо скопировать/переименовать старую папку в новую не получится - после такого новая версия просто не запускается.

Ок. Для этого есть стандартный приём - сделать дамп базы в старой версии и восстановить ее в новой.

По началу, чтобы не заморачиваться с консолькой и pg_dump, я решил заюзать что-то более дружелюбное.
Идущий в составе OpenServer последний официальный PhpPgAdmin не обновлялся много лет и с PHP7 не работает вообще. Есть работающий форк , но с экспортом через него тоже не вышло - данные-то он вроде выгружает, а вот структуру БД нет. Возможно в каком-то из 218 форков и это решено, но перебирать их все я не стал.

Далее я попробовал экспортировать БД с помощью HeidiSQL, но эта софтина мне вообще нифига не экспортировала, а только сыпала ошибками типа



/* ERROR:  syntax error at or near "CREATE" LINE 1: SHOW CREATE DATABASE "public"

Все таки поддержка PostgreSQL в ней заявлена как экспериментальная. Хотя сообщения о такой проблеме были много билдов назад, могли бы и пофиксить уже.

В итоге дамп я выгрузил с помощью pg_dump.exe, но пока я гуглил как же это все таки сделать правильно, я наткнулся на упоминание pg_upgrade  и решил попробовать ее.

Естественно с первого десятка раз нифига не получилось. Софтина неизменно писала в лог ошибку:


pg_ctl: unrecognized operation mode "50432"

В русской версии это звучит как "pg_ctl нераспознанный режим работы "50432"".

Собственно, софтина пытается запустить сервер PostgreSQL через pg_ctl на порту 50432 и у нее нифига не выходит. В логе pg_upgrade_server_start.log есть вся команда, которая пытается выполниться. Попытка выполнить запуск сервера этой же командой из консоли самостоятельно приводила к такому же результату. В команде в частности в параметре -o передаются параметры, которые будут переданы непосредственно исполнимому файлу postgres. Выглядел он так -o "-p 50432 -b ". То есть, теоретически, в postgres должен передаваться порт 50432.  Какой нафиг режим работы?!

Я пошел проанализировал исходники pg_ctl и у меня закралось подозрение, что pg_ctl как-то не так парсит переданные ей параметры. В итоге до меня дошло. Причина оказалась в следующем.

Вызов pg_upgrade производился такой командой:

pg_upgrade.exe
  -b e:\Soft\OpenServer\modules\database\PostgreSQL-9.6-x64\bin\ 
  -B e:\Soft\OpenServer\modules\database\PostgreSQL-10.5-x64\bin\
  -d e:\Soft\OpenServer\userdata\PostgreSQL-9.6-x64\ 
  -D e:\Soft\OpenServer\userdata\PostgreSQL-10.5-x64\

Тут -d  - это путь к базе старой версии. В pg_ctl это попадало так (лишнее опущено:)

pg_ctl -D "e:\Soft\OpenServer\userdata\PostgreSQL-9.6-x64\" -o "-p 50432 -b " start

Обратите внимание на последний слэш в пути, выделенный красным. Судя по всему, pg_ctl считает что \" это не окончание параметра пути, а экранированный символ кавычек, и строка пути продолжается дальше. В итоге она не находит знакомой команды (в данном случае start, так как start становится значением параметра -b) и возникает ошибка.

В итоге если убрать все оконечные слэши в путях все запускается и отрабатывает на ура. Единственное, еще нужно добавить параметр -U postgres для pg_upgrade (и возможно запускать от имени администратора). Таким образом, итоговая работающая команда выглядит так:

pg_upgrade.exe
  -b e:\Soft\OpenServer\modules\database\PostgreSQL-9.6-x64\bin 
  -B e:\Soft\OpenServer\modules\database\PostgreSQL-10.5-x64\bin
  -d e:\Soft\OpenServer\userdata\PostgreSQL-9.6-x64 
  -D e:\Soft\OpenServer\userdata\PostgreSQL-10.5-x64
  -U postgres  

вторник, 4 апреля 2017 г.

Angular 2. SystemJS для разработки, WebPack для продакшена. Проблемы с LazyLoading и относительными путями вообще


Введение 

Начинал я делать проект на каком-то там ReleaseCandidate Angular2 или даже на beta. И, соответственно, во всю там применялся SytemJS для подгрузки частей приложения. Сейчас команда Angular постепенно мигрирует на Webpack, но SystemJS еще пока есть в гайдах, полно примеров как делать именно с его помощью, короче - помирать пока не собирается. И, откровенно говоря, с ним удобнее разрабатывать.
Как выглядит разработка с использованием SystemJS? Вы правите свой ts-файлик, сохраняете, он компилируется в js-файлик. Ручками или, что удобнее, с помощью запущенного через npm typescript компилятора в режиме ожидания изменений, или, что еще удобнее, IDE вам сама как-то там запускает typescript-компилятор. Все это не столь важно. Важно что через секунду после сохранения ts-файла у вас новый js-файл. Можно в браузере нажать обновить и все - новый код в работе.
Кроме того, SystemJS по умолчанию грузит каждый файл отдельным запросом. Да - для продакшена это плохо, но для разработки это удобно.

Что происходит, если перейти на WebPack? После того как вы поменяли и сохранили свой ts-файлик, вам надо перебилдить bundle с помощью webpack. На большом проекте это время. Да, для webpack есть некий webpack-dev-server, который можно запустить в режиме отслеживания изменений, он будет на лету как-то там посылать измененные js прямо в браузер. Но у меня эта схема не заработала, подозреваю из-за того что у меня тут же крутится и backend на PHP. А вот для продакшена webpack классный - твои сотни js- и html- файлов собираются в несколько bundle.js (как настроишь, хоть все в один) и запросов на сервак уходит на порядки меньше. Причем там еще все это можно сжать, минифицировать и так далее. Таким образом работает все намного быстрее.

Возможно для SystemJS тоже есть некое подобие сборки всего в один-два файла. Я не разбирался и смысла не вижу на данном этапе.

Проблема

Я попробовал перевести проект на рельсы Webpack. После некоторых танцев с бубном, все получилось. Но выявилась проблема с лениво загружаемыми модулями. В Angular для таких в роутере указывается параметр loadChildren и его значение либо строка, либо функция типа LoadChildrenCallback:
export declare type LoadChildrenCallback = () => Type<any> | NgModuleFactory<any> | Promise<Type<any>> | Observable<Type<any>>;
Для Webpack c этим ничего сложного нет - подключаете там нужный loader (e.g., angular-router-loader), и он делает так, что ваши LazyLoaded модули превращаются в отдельные xx.chunk.js и грузятся когда надо. Статей про это полно в инете полно. В конфиге webpack у меня например написано так:
...
module: {
        rules: [
            {
                test: /\.ts$/,
                loaders: ['awesome-typescript-loader', 'angular2-template-loader','angular-router-loader']
            },
  ...
}

Более того, в webpack пути в loadChildren указываются относительные, что удобнее и логичнее. А в том же пресловутом SystemJS, пути от корня приложения. И приходилось либо реально писать фактически абсолютные пути, либо изобретать велосипеды в виде собственного загрузчика ну или генератора абсолютного пути по относительному. Ну, например, типа таких: Первым параметром там передается moduleId. Вопрос откуда он берется. А это ключевая штука.Дело в том что в момент загрузки скрипта и его интерпретации поле module.id равно абсолютному пути этого скрипта. То есть если, скажем, ваши корневые маршруты лежат в app/app.route.ts, то в момент интерпретации app.route.js module.id будет равен "http://blabla.bla/app/app.route.js". Таким образом в app.route.ts можно написать что-то типа
loadChildren: Helpers.getAbsolutePath(module.id, './dash/dash.module#DashboardModule')
и в loadChildren будет абсолютный путь. Но все это не прокатывает для WebPack. Проблем а в том, что его loader такую конструкцию не поймет и наш lazy-loaded модуль просто не попадет в сборку. Это происходит поскольку загрузчик просто ищет строку в ts-файле по регулярному выражению и наши Helpers.blablabla ему ни о чем не говорят.

Решение

 Вообще можно, наверно, было форкнуть загрузчик webpack и заставлять его понимать такие извращения, но, во-первых, мне это показалось еще пока сложноватым, а во-вторых, больше хотелось заставить SystemJS понимать относительные пути.
И я начал ползать по дебрям Angular, Observable и Promise пытаясь понять где, как и в какой момент это все грузится и можно ли туда внедриться.
Грузится это все, кстати, классом RouterConfigLoader. На вход ему подается абстрактный NgModuleFactoryLoader, а реально там SystemJsNgModuleLoader.
RouterConfigLoader подменить не вышло (Angular его не экспортирует, проинжектить вместо него что-то другое тоже), подменять SystemJsNgModuleLoader смысла нет, потому что его методы на вход приходит уже просто строка пути. А идея была строку не трогать никак, чтобы была совместимость с webpack.
И я заметил что RouterConfigLoader перед загрузкой дергает callback onLoadStartListener и пошел искать откуда это берется и куда ведет. Оказалось, что это метод onLoadStart класса Router и передается он в RouterConfigLoader при его создании. Метод в свою очередь вызывает метод triggerEvent с новым экземпляром RouteConfigLoadStart, внутри которого маршрут route, который мы будем грузить. А triggerEvent просто вызывает next на routerEvents, которые есть Observable и на них можно подписаться через router.events.

И возникла следующая схема. В маршруты в поле data мы добавляем значение module.id. Таким образом, в момент интерпретации файла с маршрутами у нас в нем будет сохранен статический относительный путь от него до требуемого lazy-loading модуля и динамический абсолютный путь до самого фала маршрута. Далее в корневом (чтобы как можно раньше) компоненте app.component (который мы bootstrap'им) мы получаем экземпляр класса Router с помощью DI, подписываемся на его события и, если приходит RouteConfigLoadStart, то вычисляем абсолютный путь до модуля и подменяем его в маршруте.

Получился вот такой код:
export class AppComponent {
    constructor(private router: Router) {
        router.events.filter((event) => event instanceof RouteConfigLoadStart).subscribe((event: RouteConfigLoadStart) => {
            if (event.route && event.route.loadChildren && event.route.data && event.route.data.moduleId && !event.route.data.absolute) {
                if (typeof event.route.loadChildren === "string") {
                    let relPath = event.route.loadChildren;
                    if (relPath.substr(0, 1) == ".") {
                        event.route.loadChildren = Helpers.getAbsolutePath(event.route.data.moduleId, relPath);
                        event.route.data.absolute = true;
                    }
                }
            }
        })
    }
}

А в маршрутах пишем примерно так:
export const routes: Routes = [
    {
        path: '',
        loadChildren: './main/main.module#MainModule',
        data: {
            moduleId: module.id,
        }
    }

Тут я в route.data.absolute прописываю true, чтобы второй раз не срабатывало, но вообще загрузка происходит 1 раз и второго раза не будет для маршрута. Просто перестраховка.
Также я собирался проверять в runtime используем ли мы systemjs, но оказалось это не надо и под webpack тоже все работает чудесным образом.

понедельник, 3 апреля 2017 г.

О чем это...

Описания последствий от наступления на грабли при разработке сайтов.
Изобретения велосипедов и внедрение костылей.
Борьба с фреймворками.
Бэкэнд и фронтенд.

Этот блог - записки практически начинающего web-разработчика, который когда-то "видел", но последнее время занимался совсем другими проектами, а теперь пытается наверстать упущенное и стать в тренде. Но со своими тараканами. Особых откровений тут не будет, но и банальщины, надеюсь, тоже.