Instalacja Debiana: nie wykryto napędu CD-ROM

Od dawna instaluję Debiana z pendrive’a. Zawsze używałem programu UNetbootin do nagrywania obrazu płyty instalacyjnej na pendrive’ie. Zazwyczaj instalacja zajmuje mi jakieś 30 minut. Ale nie tym razem. Tym razem męczyłem się z instalacją jakieś trzy godziny.

Zaczęło się dobrze: laptop uruchomił się z pendrive’a, pokazał menu instalatora, wybrałem instalację graficzną. W kolejnych krokach wybrałem język polski i polski układ klawiatury, a potem… instalator oświadczył, że nie wykrył CD-ROMu i zapytał czy chcę załadować sterowniki z innego nośnika.

Pominę pełen opis prób i błędów, które popełniłem, wymienię je tylko w telegraficznym skrócie:

  • To błąd w instalatorze, należy spróbować innego obrazu płyty.
  • Ponoć pendrive sformatowany na FAT32 może sprawiać problemy. Lepiej użyć FAT16.
  • Pendrive należy włożyć w port USB 2, porty USB 3 mogą sprawiać problemy.
  • Po wyświetleniu komunikatu należy wyjąć i ponownie włożyć pendrive.
  • W menu BIOS/UEFI należy zmienić opcję obsługi dysków na AHCI.

Tymczasem rozwiązaniem okazało się nagranie obrazu płyty na pendrive’a poleceniem dd:

dd bs=4M if=/home/michal/Pobrane/debian-testing-amd64-netinst.iso of=/dev/sdb && sync

PS. Laptop to był Dell Inspiron z serii 3000, nic fikuśnego. Niestety standardowo miał zainstalowanego Windowsa.

Tryb recovery w Motoroli Moto G z Androidem 5

Aktualizacja mojej Motorolki Moto G (pierwszej generacji) do Androida 5.0.2 była słabym pomysłem. Telefon z czasem zaczyna działać coraz wolniej, czasami zamyśla się na kilkanaście sekund przy prostych operacjach, odczuwalna jakość interakcji z urządzeniem jest wyraźnie gorsza. Kit Kat był w tym modelu o wiele lepiej dopracowany.

Jednym z pomysłów na poprawę sytuacji było wyczyszczenie partycji /cache urządzenia, co wymaga uruchomienia telefonu w trybie recovery. Wyszukiwarka usłużnie podaje liczne instrukcje, z których większość jest… błędna. Okazuje się, że przy Androidzie 5 wywołanie trybu recovery jest inne niż w telefonie z Kit Katem. Zanim do tego doszedłem przez blisko 10 minut bezowocnie międliłem klawisze. Poniżej działająca procedura.

  1. Wyłączyć telefon.
  2. Przytrzymać przez kilka sekund jednocześnie przyciski zwiększania głośności, zmniejszania głośności i włącznik.
  3. Używając przycisku zmniejszania głośności w menu należy podświetlić opcję Recovery, a następnie zatwierdzić wybór przyciskiem zwiększania głośności.
  4. Gdy na ekranie pojawi się leżący na plecach android oraz napis „no command” należy przytrzymać włącznik i nacisnąć przycisk zwiększania głośności.
  5. Na ekranie objawi się menu trybu Recovery, z którego można wybrać między innymi czyszczenie partycji cache. Wybór zatwierdza się przyciskiem włączania telefonu.

Skuteczność tej operacji ociera się o efekt placebo; wydaje mi się, że jest ciut lepiej, ale na pewno nie jest dobrze. Z utęsknieniem czekam na Androida 5.1.

Aktualizacja nginx do 1.8.0 i problem z PHP

Dzisiaj zespół dotdeb.org wypuścił nginxa 1.8.0 na Debiana, a ja niezwłocznie dokonałem aktualizacji z wersji 1.6.3. Początkową radość szybko zastąpiła panika, kiedy okazało się, że wszystkie strony oparte o PHP przestały działać, a konkretnie zaczęły zwracać pustą treść. W logach nginxa, PHP-FPM ani nigdzie indziej nie znalazłem błędów, w konfiguracji nginxa zmiany były tylko kosmetyczne, konfiguracja PHP w ogóle nie była ruszana, po prostu czeski film.

Rozwiązanie okazało się trywialne: w pliku /etc/nginx/fastcgi_params należy dodać linijkę:

fastcgi_param  SCRIPT_FILENAME    $document_root$fastcgi_script_name;

Potem już tylko przeładowanie konfiguracji nginxa i wszystko powinno działać.

Tytułem uzupełnienia dodam, że na właściwy trop trafiłem, kiedy spróbowałem połaczyć się z PHP-FPM bezpośrednio przez FastCGI. Wedle przepisu z tej strony w konsoli zapodałem:

SCRIPT_NAME=/index.php \
SCRIPT_FILENAME=/index.php \
REQUEST_METHOD=GET \
DOCUMENT_ROOT=/var/www/anime.com.pl/ \
cgi-fcgi -bind -connect /var/run/php5-fpm.sock

W odpowiedzi otrzymałem:

Primary script unknown
Status: 404 Not Found
Content-type: text/html

File not found.

Dalej już było z górki. Ciekawe tylko dlaczego to się nigdzie nie zalogowało?

Jako lekturę uzupełniającą polecam wpis z bloga Martina Fjordvalda, który tłumaczy czym różni się plik fastcgi_params od fastcgi.conf i skąd się ta różnica wzięła.

Wyłączanie filtrów w Doctrine2

Filtry w Doctrine to poniekąd pożyteczna funkcja, na filtrach jest oparte m.in miękkie kasowanie (ang. soft delete), które można podpiąć z paczki gedmo/doctrine-extensions. Filtry przeważnie są włączane na stałe w konfiguracji ORM-a w pliku config.yml, ale w razie potrzeby można je włączać i wyłączać w kodzie. Obiekt FilterCollection pobieramy z entity managera, a na nim można wykonać m.in. poniższe metody:

// sprawdzenie czy filtr jest zainstalowany
$em->getFilters()->has("soft-deleteable");

// sprawdzenie czy filtr jest włączony
$em->getFilters()->isEnabled("soft-deleteable");

// włączenie filtra
$em->getFilters()->enable("soft-deleteable");

// wyłączenie filtra
$em->getFilters()->disable("soft-deleteable");

Cała ta notka wzięła się stąd, że dłużej niż powinienem szukałem błędu w kodzie, a jego źródłem było właśnie miękkie kasowanie. Zapytanie, które powinno zwracać encję zwracało NULL, a taki przypadek nie był obsłużony w kodzie.

Roundcube przez HTTPS czyli nginx i self-signed certificate

Powinienem to zrobić dawno temu, ale dopiero teraz zabrałem się za zabezpieczenie SSL-em mojego webmaila opartego na Roundcube. Ponieważ poza mną nikt z niego nie korzysta, a sam sobie zazwyczaj ufam, to zdecydowałem się nie korzystać z żadnego centrum certyfikacji i wygenerować certyfikat własnoręcznie.

W sieci nie brakuje instrukcji generowania certyfikatów, ja staram się robić wszystko po debianowemu, więc kierowałem się opisem z debianowej wiki. Dlatego certyfikat wylądował w katalogu /etc/ssl/localcerts, który musiałem stworzyć:

mkdir -p /etc/ssl/localcerts

Polecenie generowania klucza i certyfikatu:

openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout /etc/ssl/localcerts/mail.komitywa.net.key -out /etc/ssl/localcerts/mail.komitywa.net.crt

Przed wygenerowaniem certyfikatu zostaniemy zapytani o dane certyfikowanego podmiotu:

You are about to be asked to enter information that will be incorporated
into your certificate request.
What you are about to enter is what is called a Distinguished Name or a DN.
There are quite a few fields but you can leave some blank
For some fields there will be a default value,
If you enter '.', the field will be left blank.
-----
Country Name (2 letter code) [AU]:PL
State or Province Name (full name) [Some-State]:Mazowieckie
Locality Name (eg, city) []:Warsaw
Organization Name (eg, company) [Internet Widgits Pty Ltd]:komitywa.net
Organizational Unit Name (eg, section) []:.
Common Name (eg, YOUR name) []:mail.komitywa.net
Email Address []:moj-adres-email@komitywa.net

Na koniec warto ograniczyć dostęp do wygenerowanych plików:

chmod 600 /etc/ssl/localcerts/mail.komitywa.net.*

Teraz można już SSL do konfiguracji nginx’a. Do działającego wirtualnego hosta dodałem:

listen 443 ssl;
ssl_certificate /etc/ssl/localcerts/mail.komitywa.net.crt;
ssl_certificate_key /etc/ssl/localcerts/mail.komitywa.net.key;

Po przeładowaniu nginx’a i przetestowaniu, że połaczenie po HTTPS działa postanowiłem jeszcze dodać przekierowanie z HTTP na HTTPS:

server {
       listen 80;
       server_name mail.komitywa.net;
       return 301 https://mail.komitywa.net$request_uri;
}

Wygenerowany certyfikat jest ważny przez rok, więc w kwietniu 2016 będę musiał wygenerować nowy certyfikat.

Weekend w Brukseli

Miniony weekend spędziłem z Kariną w Brukseli. Wycieczkę uważam za więcej niż udaną, a bez fałszywej skromności dodam, że duża w tym zasługa dobrego planowania. Poniżej garść luźnych uwag i sugestii, które mogą się przydać komuś, kto chciałby sobie urządzić podobną wycieczkę.

Tanie latanie

Mój pierwszy lot Ryanairem okazał się niespodziewanie… zwyczajny. Po lekturze opinii w internecie obawiałem się, że ludzie będą wskakiwać do samolotu oknami, wpychać się na nie swoje miejsca, grillować między fotelami, słowem Sodomy i Gomory ze skrzydłami albo latającego odpowiednika „Słonecznego” Koleii Mazowieckich. Tymczasem okazało się, że choć samolot był wypełniony chyba do ostatniego miejsca, a pasażerowie uformowali pokaźną kolejkę na długo przed ogłoszeniem boardingu to wejście na pokład odbyło się bez ekscesów. Wprawdzie na moim miejscu usiadła jakaś miła starsza pani, ale tylko dlatego, że nie wiedziała, że na karcie pokładowej ma wydrukowane miejsce. Grzecznie jej to wytłumaczyłem i problem był rozwiązany. Fotele, a i owszem, nie odchylają się, miejsca jest co najwyżej akuratnie, za bułę i napój trzeba zapłacić, a film sobie zorganizować we własnym zakresie, ale przez dwie godziny można znieść takie niedogodności, tym bardziej, że bilet był dwukrotnie tańszy niż na pendziolino do Krakowa. Po stronie plusów należy zapisać też dogodne godziny wylotów na weekendowy wyjazd: wylot w piątek o 9:20, powrót w niedzielę o 18:25.

Dojazd

Ryainair ląduje lotnisku Charleroi, takim belgiskim Modlinie (choć wielkością bliżej mu do Okęcia). Wygodne połączenie z Brukselą zapewnia linia autokarowa. Bilety najlepiej kupić przez internet, będą wtedy kosztowały przynajmniej o 3 € od łebka taniej, co przy dwóch osobach w obie strony da ok 50 zł oszczędności. Oszczędza się też czas, bo wprost z samolotu można wsiadać do autokaru.

Nocleg

Poszukiwania noclegu rozpocząłem od Airbnb, ale okazało się, że w obrębie starego miasta taniej można spać w hotelu. Wybrałem hotel Saint Nicolas, dwie minuty spacerkiem od Wielkiego Placu i rzut beretem od stacji metra Bourse. Miejscówka jest świetna jako baza wypadowa do korzystania z brukselskich atrakcji, zarówno za dnia jak i w nocy. Trzeba mieć jednak świadomość, że położenie może być też wadą, hotel stoi przy ruchliwym deptaku, a centrum Brukseli żyje do późnych godzin nocnych.

Zwiedzanie

Na zwiedzanie Brukseli warto uzbroić się w Brussels Card czyli kartkę (bo dostajemy ją jako wydruk A4) dającą wstęp do 30 muzeów (w tym tych MSZ najciekawszych) i zniżki w innych miejscach (np. Atomium). Biorąc pod uwagę, że wstęp do muzeum kosztuje przeważnie od 6 do 10 € to zakup karty się opłaca. Ja kupiłem takową na 48 godzin, aktywowałem w piątek po południu o korzystałem aktywnie aż do wyjazdu w niedzielę.

Brussels Card ma też opcję korzystania z dwóch autobusowych linii turystycznych, ale ja wybrałem komunikację miejską. Bruksela ma znakomity zbiorkom, do większości interesujących mnie miejsc dojechałem prawie pod drzwi. Opłaca się kupić bilety na 5 lub 10 przejazdów ew. na 48 godzin. Niestety trzeba się przyzwyczaić do aromatu uryny na stacjach metra.

Google Maps dobrze sprawdza się w nawigowaniu po Brukselii z buta, ale sugestie dotyczące używania zbiorkomu bywają nietrafione. Mapę Brukselii zapisałem w telefonie, ale z myślą o Google Maps wykupiłem pakiet 100 MB transferu w roamingu. Pod koniec weekendu okazało się, że wykorzystałem go nieco ponad 30 MB, z czego tylko mała część przypadła na Mapy.

Manneken pis to oczywiście punkt obowiązkowy, choć tak po prawdzie nie ma co oglądać. Ciekawsze jest to, że w ramach równouprawnienia od 1987 Bruksela ma też siusiającą dziewczynkę czyli Jeanneke pis. Jest jeszcze trzecia siusiająca rzeźba – Zinneke pis czyli pies. Do kompletu brakuje jeszcze kota.

Zinneke pis

Z kolei Atomium na żywo robi większe wrażenie niż się spodziewałem. Niestety swoje trzeba odstać w kolejkach, w deszczowy niedzielny poranek czekaliśmy blisko godzinę.

Atomium

Belgia stoi komiksem, nic więc dziwnego, że muzeum komiksu jest na wypasie. Stała ekspozycja pokazuje historię komiksu, proces jego powstawania od pomysłu do gotowego albumu, różne style i gatunki i całą masę ciekawych eksponatów. W czasie mojej wizyty ekspozycję czasową stanowiła wystawa poświęcona Thorgalowi oraz osobna poświęcona Grzegorzowi Rosińskiemu. Moim zdaniem to najciekawsze muzeum, które odwiedziłem w Brukseli.

Podobne wrażenia oferuje MOOF czyli muzeum figurek kolekcjonerskich. Figurki to nie mój konik, ale miło się je oglądało. Oprócz figurek sporo miejsca poświęcono komiksom i filmom animowanym powstałym w Belgii. Bardzo fajnie bawiłem się przy starych konsolach z grami na podstawie komiksów o Asteriksie i Obeliksie.

MOOF

MOOF

Muzeum piwa brzmi ciekawie, ale jego oferta ogranicza się do projekcji umiarkowanie interesującego filmu i degustacji piwa.

Ciekawsze jest muzeum czekolady, a zwłaszcza pokaz robienia pralinek belgijskich. Oczywiście jest też degustacja.

Muzeum zabawek wygląda i pachnie jak mieszkanie patologicznego zbieracza. W sumie nic ciekawego, ale fajne jest to, że dużą częścią eksponatów można się bawić, a dzieciaki, które widziałem w muzeum miały z tego wielką frajdę.

Bardzo podobało mi się w Auto World. Mają tam dużą kolekcję zabytkowych samochodów, choć najdłużej przyglądałem się youngtimerom takim jak Honda NSX i specjalistycznie przygotowanym samochodom wyścigowym. Karinie, która jest umiarkowaną fanką motoryzacji, też się podobało, więc warto tam się wybrać.

Auto World - japońskie supersamochody

Auto World

Jedzenie

W Brukselii należy zjeść frytki i gofry. W pobliżu hotelu jest kanciapa z frytkami, która, jeśli wierzyć nalepce przy drzwiach, jest przez kogoś tam polecana. Potwierdzam, że fryty z sosem andaluzyjskim mają tam dobre. Po sąsiedzku jest kanciapa z goframi i churrosami, takoż smacznymi. W okolicy jest jednak pierdyliard podobnych kanciap, możliwe, że lepszych lub bardziej znanych. Z drugiej strony nie polecam belgiskiego McDonald’sa czyli Quicka. Co by nie mówić o Maku to paszę mają tam smaczną, a lokale czyste. W Quicku dostałem kanapkę, która wyglądała jakby w celu podgrzania ktoś na niej usiadł, do podłogi prawie się przykleiłem, a na koniec musiałem szukać kosza na śmieci, który nie był przepełniony.

WHERE warunek vs LEFT JOIN warunek

Być może nie powinienem publicznie przyznawać się, że popełniłem tak podstawowy błąd w konstruowaniu zapytania SQL, ale po jego odkryciu szczerze się uśmiałem, a skoro śmiech to zdrowie to się nim podzielę.

W uproszczeniu: mam tabelkę z danymi klientów (client) i tabelkę produktów (product), pomiędzy którymi istnieje relacja jeden do wielu. Wyciąganie danych do pokazania jest zrealizowane w Doctrine w następujący sposób:

$qb
    ->select('customer, product')
    ->from($this->getEntityName(), 'customer')
    ->leftJoin('customer.products', 'product')
    ->where('customer.identifier = :identifier')
    ->setParameter('identifier', $identifier);

Dostałem za zadanie zmienić wyświetlanie danych w ten sposób by pokazywać jedynie aktywne produkty. Zrobiłem to tak:

$qb
    ->select('customer, product')
    ->from($this->getEntityName(), 'customer')
    ->leftJoin('customer.products', 'product')
    ->where('customer.identifier = :identifier')
    ->andWhere('product.isActive = 1')
    ->setParameter('identifier', $identifier);

Ku memu zdziwieniu w wypadku klienta, który ma tylko jeden nieaktywny produkt wynik zapytania był pusty. No jakże to?! Przecież klient jak najbardziej istnieje, a ponieważ nie ma produktów spełniających kryteria wyszukiwania (isActive = 1) to w miejscu danych produktu powinny być NULLe.

Swój błąd zrozumiałem dopiero, kiedy spojrzałem na generowane przez Doctrine zapytanie SQL. W uproszczeniu wygląda ono tak:

SELECT 
  c.name,
  p.product_type
FROM 
  customer c 
  LEFT JOIN product p ON c.id = p.customer_id 
WHERE 
  c.identifier = '65092900214'
  AND p.is_active = 1

Ponieważ warunek p.is_active = 1 sprawdzamy w części WHERE zapytania to eliminujemy z zapytania wszystkie wiersze, w których on nie jest spełniony. Zatem jeśli klient ma tylko jeden nieaktywny produkt to wynik zapytania będzie pusty.

Oczekiwany efekt (wiersz z NULLami w miejscu danych produktu) uzyskamy przenosząc warunek do części LEFT JOIN zapytania:

SELECT 
  c.name,
  p.product_type
FROM customer c
  LEFT JOIN product p ON c.id = p.customer_id 
  AND p.is_active = 1
WHERE 
  c.identifier = '65092900214'

Ponieważ warunek dotyczy złączenia to jeśli nie zostanie on spełniony to złączenie nie zostanie wykonane, ale dane klienta zostaną zwrócone. Dokładnie tak jak chciałem.

W Doctrine poprawne zapytanie należy zbudować tak:

$qb
    ->select('customer, product')
    ->from($this->getEntityName(), 'customer')
    ->leftJoin('customer.products', 'product', 'WITH', 'product.isActive = 1')
    ->where('customer.identifier = :identifier')
    ->setParameter('identifier', $identifier);

Twig Extensions w aplikacji Symfony2

Twig sam z siebie ma sporo wygodnych funkcji, ale kilka bardzo przydatnych udogodnień nie trafiło do głównej biblioteki lecz do osobnej paczki Twig Extensions. W wypadku standardowej aplikacji Symfony2 te rozszerzenia już są uwzględnione w composer.json, ale jeśli nie to można jest zainstalować poleceniem:

composer require twig/extensions ~1.1.0

Trzeba je także włączyć w konfiguracji aplikacji, tak jak każde inne rozszerzenie Twiga:

services:
    twig.extension.intl:
        class: Twig_Extensions_Extension_Intl
        tags:
            - { name: twig.extension }

Powyższy przykład dodaje rozszerzenie Intl, które podobnie jak biblioteka Intl ułatwia formatowanie danych zgodnie z ustawieniami lokalizacji. Obecnie w Twig Extensions dostępne są następujące klasy:

  • Twig_Extensions_Extension_Array
  • Twig_Extensions_Extension_Date
  • Twig_Extensions_Extension_I18n
  • Twig_Extensions_Extension_Intl
  • Twig_Extensions_Extension_Text

Przy założeniu, że aplikacja ma ustawione polską lokalizację to wyświetlenie daty w postaci „13 listopada 2014” wygląda tak:

{{ meeting.date|localizeddate('long', 'none') }}

Wyświetlenie liczby z częścią dziesiętną:

{{ player.power|round(2)|localizednumber }}

Przy korzystaniu z filtra localizednumber tylko liczby z częścią ułamkową będą wyświetlane z przecinkiem, a liczby całkowite są wyświetlane bez przecinka.

Klucz kluczem do sukcesu

Mechanizm planowania zapytań w MySQL jest mniej sprytny niż sądziłem. Chciałem wykonać z pozoru łatwe zapytanie: z bazy użytkowników liczącej ok 130 tys. wierszy chciałem wyłuskać osoby mieszkające pod określonymi kodami pocztowymi (ok. 20 tys.). Dodatkowym warunkiem było podanie numeru telefonu, który znajduje się w innej tabelce oraz wyrażenie zgody na marketing, ale to nie jest istotne dla głównego problemu. Napisałem takie zapytanie:

SELECT
	k.imie,
	k.nazwisko,
	k.kod_pocztowy,
	u.numer_telefonu
FROM klient k
INNER JOIN ustawienia u ON k.id = u.klient_id
INNER JOIN tmp_kody t ON k.kod_pocztowy = t.kod_pocztowy
WHERE k.zgoda_na_marketing = 1
ORDER BY k.id

Szukane kody pocztowe wrzuciłem sobie do tabelki, bo przy próbie umieszczenia ich w treści zapytania phpMyAdmin odmówił współpracy, a wklejanie zapytania na zdalnej konsoli trwało ponad 10 minut.

Pierwszą próbę wykonania zapytania przerwałem po kilku minutach. EXPLAIN pokazał, że MySQL nie bardzo chce korzystać z indeksów i najchętniej przeskanowałby całą tabelkę tmp_kody i ustawienia. Próbowałem przepisać zapytanie na kilka sposobów by skłonić MySQL do ułożenia innego planu, ale bezskutecznie. Skutecznym rozwiązaniem okazało się tymczasowe dodanie indeksu na kolumnę kod_pocztowy. Oryginalne zapytanie wykonało się w mgnieniu oka, a EXPLAIN pokazał, że wszystkie etapy planu wykonania zaczęły korzystać z indeksów.

Wniosek na przyszłość: jeśli MySQL sobie nie radzi to można mu pomóc dodając tymczasowy indeks. To może okazać się szybsze niż czekanie na wynik nieoptymalnego zapytania.

Import dużych liczb z pliku CSV

Od czasu do czasu jestem proszony o wyciąganie z bazy różnych raportów. Prosta sprawa: zapytanie w phpMyAdminie, eksport wyniku do pliku CSV i wysyłamy… o ile w pliku nie ma kolumny zawierającej dużą liczbę. W takim wypadku zarówno LibreOffice Calc jak i Excel mają nieprzyjemny zwyczaj zaokrąglania jej. Przykładowo w 16 cyfrowym numerze karty po którym identyfikujemy klienta i w którym ostatnia cyfra jest sumą kontrolną dostajemy na końcu zawsze 0, co oczywiście skutecznie niweczy zarówno sprawdzanie sumy kontrolnej jak i identyfikację klienta.

Na szczęście w Calcu można to naprawić. W Excelu zapewne też, ale odbiorcy moich raportów nie wiedzą jak, a ja nie mam Excela, żeby eksperymentować. Dlatego przed wysłaniem raportu do działu marketingu importuję plik do Calca. W okienku z opcjami importu należy zaznaczyć kolumnę zawierającą dużą liczbę i zmienić jej typ na Tekst.

Import pliku CSV

Potem należy zapisać arkusz w formacie Excela. Taki plik można dostarczyć do marketingu.