Python – Demonologia
Python jest świetnym językiem do całej palety zastosowań. Służy on pomocą zarówno administratorom systemów, osobom które potrzebują napisać prosty parser, a nawet twórcom aplikacji webowych – dzięki takim frameworkom jak Django. W poniższym tekscie chciałbym pokazać jak łatwo jest stworzyć usługę systemową (ang. daemon) w języku Python, która będzie wykonywała dla nas określoną czynność – jako przykład przedstawię fakedns – bardzo prostego daemona DNS, który na dowolne zapytanie o adres IP będzie zwracał zawsze ten sam.
Dlaczego Python? Co z innymi językami?
Nie lepiej mieć aplikację którą można skompilować? Absolwenci kierunków informatycznych jeszcze kilka lat temu dowiadywali się na studiach, że najbardziej zaawansowanym językiem do wszelakich zastosowań jest Java. Mieli praktyczne doświadczenie z tworzenia aplikacji w Javie… i zaczęli pisać. Kod który chciałbym tu przedstawić pisałem dwa lata temu. Zanim się za niego zabrałem, zacząłem od szukania już gotowego rozwiązania za pomocą Google. Znalazłem, nawet kilka. Jedno w Javie. O zgrozo. Ja wiem, że się da. Wiem, że Java jest naprawde zaawansowanym językiem. Gotowa aplikacja wymaga jednak dużego nadkładu. Trzeba przygotować środowisko, przygotować konkretną ilość pamięci… i czekać kiedy aplikacja się wyłoży. Takie mam przynajmniej doświadczenia z aplikacjami w Javie, które pisali programiści zaraz po studiach, pracując w profesjonalnej firmie software’owej.
A inne języki? Natualnie. Jest C. Świetny język, ale czy mamy wystarczająco dużo czasu aby w nim pisać nawet małe aplikacje? .NET ? Wolne żarty. Nie dość, że pod Linuxem jest bardzo grubymi nićmi przyszywany, to i nie mamy pewności, czy Microsoft nie powie pewnego dnia hola, jedynie na ich, komercyjnej platformie. Naddatek potrzebnej mocy obliczeniowej, pamięci, wymaganej przestrzeni dyskowej… i ta świadomość, że nie jest to dopracowane i pewnie nigdy nie będzie – niczym Samba próbująca naśladować również kalekie i dziurawe, Microsoftowe domeny.
A co ze skryptowymi? Mnie na myśl przychodzą trzy alternatywy – Lua, Ruby i skrypt w bashu.
Lua – niesamowicie przejrzysty język. Piekielnie szybki, realizowany przez wręcz mikroskopijną bibliotekę. Na jego niekorzyść trzeba jednak zaliczyć wręcz niemal konieczność liczenia się z faktem, że jeśli rozwiązanie które potrzebujemy stworzyć, będzie wymagało biblioteki – będziemy zmuszeni długo jej szukać lub przygotować samemu jej obsługę za pomocą języka C.
Ruby – chyba najbliższy Pythonowi. Dlaczego nie. Bibliotek jest coraz więcej, jest wydajny. Miałem okazję jakiś czas temu nawet coś napisać w tym języku (a dokładnie – przepisać mały framework z Pythona), ale… jakoś w Pythonie pisze mi się lepiej. Co ważne – Ruby jest głęboko zakorzeniony w Perlu. Programistom mówi się jak powinni pisać kod, jednak zawsze mogą robić w to w stylu Perlowym – pisać szybko i wydajnie, jednak przeczytanie i przeanalizowanie go po jakimś czasie staje się piekłem. Kod w Pythonie jest de facto zawsze taki sam, nie zależnie kto by go pisał – czytanie go jest proste i przyjemne.
No i jeszcze uzupełnie o język skryptowy – Bash. Można w nim naprawdę dużo zrobić. Warto jednak zastanowić się na ile aplikacja będzie zaawansowana i czy zależy nam na wydajności. Podam przykład. Stworzyłem kiedyś skrypt w bashu generujący w oparciu o kilka plików wejściowych reguły dla firewalla, reguły shapera oraz treść dhcpd.conf. Skrypt wewnątrz wywoływał po kilka tysięcy razy awk/sed’a itp. Na maszynie jednoprocesorowej (Pentium 4 1.4 GHz), która pełniła funkcję routera dla sieci 100 Mbit/s, przy mniejszym obciążeniu – skrypt wykonywał się kilka minut. Przy większym – godzinę. Za każdym razem uruchamiając awk, spawnował odrębnego basha. Cóż. Przepisałem rozwiązanie na język Lua. Skrypt niezależnie od obciążenia maszyny wykonywał się w 0.02 sekundy. Jeśli byłby to Python czy Ruby – efekt byłby zapewne zbliżony do Lua.
Tworzenie usługi systemowej dla Linux/Unix
OK, zaczynamy. Usługa systemowa powinna przede wszystkim:
- wiedzieć jak pisać do sysloga
- zrzucić uprawnienia root’a
- odkleić się od stdin i stdout i przejść w tło
Jeśli taka usługa dodatkowo potrzebuje obsługiwać jakiś systemowy port TCP/UDP (o numerze niższym niż 1024) – zanim zrzuci ona prawa roota, powinna otworzyć sobie określonego socketa.
Nie będę tłumaczył poszczególnych fragmentu kodu ponieważ uważam, że jest on wystarczająco przejżysty.
fakedns.py
#!/usr/bin/python -u
# -*- coding: utf-8 -*-
" Fake DNS server: Always answer with the same, defined as conf.destip reply, listen at conf.listen "
class conf:
listen='ADRES_IP_NA_KTÓRYM_APLIKACJA_MA_NASŁUCHIWAĆ'
destip='ADRES_IP_NA_KTÓRY_APLIKACJA_BĘDZIE_KIEROWAĆ'
import os,sys,pwd,grp
import logging,logging.handlers
import socket
log=logging.getLogger('fakedns')
logging.basicConfig()
logging.root.setLevel(level=logging.INFO)
#logging.root.setLevel(level=logging.DEBUG)
hdlr=logging.handlers.SysLogHandler('/dev/log')
log.addHandler(hdlr)
def drop_privileges(uid_name='nobody',gid_name='nogroup'):
starting_uid=os.getuid()
starting_gid=os.getgid()
starting_uid_name=pwd.getpwuid(starting_uid)[0]
log.info('fakedns: drop_privileges: started as %s/%s'%(pwd.getpwuid(starting_uid)[0],grp.getgrgid(starting_gid)[0]))
if os.getuid()!=0:
log.info("fakedns: drop_privileges: already running as '%s'"%starting_uid_name)
return
if starting_uid==0:
# Get the uid/gid from the name
running_uid = pwd.getpwnam(uid_name)[2]
running_gid = grp.getgrnam(gid_name)[2]
# Try setting the new uid/gid
try:
os.setgid(running_gid)
except OSError,e:
log.error('Could not set effective group id: %s'%e)
try:
os.setuid(running_uid)
except OSError,e:
log.error('Could not set effective user id: %s'%e)
# Ensure a very convervative umask
new_umask=077
old_umask=os.umask(new_umask)
log.info('fakedns: drop_privileges: Old umask: %s, new umask: %s'%(oct(old_umask),oct(new_umask)))
final_uid=os.getuid()
final_gid=os.getgid()
log.info('fakedns: drop_privileges: running as %s/%s'%(pwd.getpwuid(final_uid)[0],grp.getgrgid(final_gid)[0]))
def become_daemon(home='.',stdin='/dev/null', stdout='/dev/null', stderr='/dev/null'):
log.info('fakedns: deamonize')
# perform first fork
try:
if os.fork()>0:
sys.exit(0)
except OSError,e:
log.info("fakedns: 1st fork failed: %s"%e.strerror)
sys.exit(1)
os.setsid()
os.chdir(home)
os.umask(0)
# second fork
try:
if os.fork()>0:
sys.exit(0)
except OSError,e:
log.info("fakedns: 2nd fork failed: %s"%e.strerror)
sys.exit(1)
i=open(stdin,'r')
o=open(stdout,'a+')
e=open(stderr,'a+',0)
os.dup2(i.fileno(),sys.stdin.fileno())
os.dup2(o.fileno(),sys.stdout.fileno())
os.dup2(e.fileno(),sys.stderr.fileno())
class DNSQuery:
def __init__(self, data):
self.data=data
self.dominio=''
tipo = (ord(data[2]) >> 3) & 15 # Opcode bits
if tipo == 0: # Standard query
ini=12
lon=ord(data[ini])
while lon != 0:
self.dominio+=data[ini+1:ini+lon+1]+'.'
ini+=lon+1
lon=ord(data[ini])
def respuesta(self, ip):
packet=''
if self.dominio:
packet+=self.data[:2] + "\x81\x80"
# Questions and Answers Counts
packet+=self.data[4:6] + self.data[4:6] + '\x00\x00\x00\x00'
# Original Domain Name Question
packet+=self.data[12:]
# Pointer to domain name
packet+='\xc0\x0c'
# Response type, ttl and resource data length -> 4 bytes
packet+='\x00\x01\x00\x01\x00\x00\x00\x3c\x00\x04'
# 4bytes of IP
packet+=str.join('',map(lambda x: chr(int(x)), ip.split('.')))
return packet
if __name__ == '__main__':
udps = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
try:
udps.bind((conf.listen,53))
except socket.error, (val,e):
log.info("fakedns: %s"%e)
sys.exit(1)
log.info('fakedns: dom.query. 60 IN A %s' % conf.destip)
drop_privileges()
become_daemon()
try:
while 1:
data, addr = udps.recvfrom(1024)
#log.info(addr)
p=DNSQuery(data)
udps.sendto(p.respuesta(conf.destip), addr)
#log.info('%s -> %s' % (p.dominio, conf.destip))
log.info('fakedns: %s -> %s'%(p.dominio,addr[0]))
except KeyboardInterrupt:
log.info('fakedns: done.' % (p.dominio, conf.destip))
udps.close()
except:
log.info('fakedns: crashed serving %s (%s)'%(addr, data))
Instalacja Mercurial z GUI TortoiseHG pod Ubuntu 9.04
Mercurial jest jednym z najpopularniejszych ostatnio systemów kontroli wersji. Został on napisany w języku Python i coraz więcej projektów jest do niego migrowanych. Google Code umożliwia wybór jednego z dwóch systemów kontroli wersji – jednym z nich jest SVN, drugim właśnie Mercurial.
Różnice między scentralizowanym a rozproszonym systemem kontroli wersji
Zasada działania Mercuriala jest zbliżona do Git‘a – oba są tzw. rozproszonymi systemami kontroli wersji. SVN z drugiej strony jest systemem zcentralizowanym. Praca na systemie rozproszonym jest o tyle wygodna, że każda lokalna kopia repozytorium może być traktowana jako pełnoprawna i całkowicie niezależna. Rozwijając kod który jest obsługiwany w takim systemie kontroli wersji, commitowanie, czyli zatwierdzanie mniejszych porcji zmian jest znacznie wygodniejsze i nie wymaga zaprzątania głowy głównego repozytorium… którego tak naprawdę wcale nie musi być.
Jak to robi Linus Torvalds?
Większe projekty, takie jak jądro Linuxa coraz częściej przechodzą na rozproszone systemy kontroli wersji. Linus Torvalds bardzo zachęca wszystkich do migrowania do Gita z SVN’a. Git został napisany właśnie z myślą o developerach jądra systemu Linux. Głównymi zaletami według niego są dwie kwestie – pierwsza: znacznie wygodniejsze merge’owanie (łączenie zmian wprowadzonych przez różnych developerów) oraz druga – rozproszone repozytoria pozwalają na hierarchiczne scalanie kodu (z punktu widzenia pracy grupowej). Dzięki tej drugiej, Linus może powierzyć np. opiekę nad modułami jądra dotyczącymi sieci komuś, kto zna się na tym konkretnym temacie lepiej, potrafi weryfikować zmiany i poprawki. Zebrane do repozytorium przez opiekuna drzewa sieci poprawki, Linus może włączyć do jego repozytorium, które traktowane jest jako główne mając pewność, że wszystkie zmiany zostały odpowiednio zweryfikowane.
Kontrola wersji z linii komend
Mercuriala można zainstalować ze standardowego pakietu w Ubuntu. Na pakiet składa się jedna aplikacja – hg. Za jej pomocą można wykonać wszystkie operacje na repozytorium z poziomu linii komend.
$ hg Mercurial Distributed SCM basic commands: add add the specified files on the next commit annotate show changeset information by line for each file clone make a copy of an existing repository commit commit the specified files or all outstanding changes diff diff repository (or selected files) export dump the header and diffs for one or more changesets forget forget the specified files on the next commit init create a new repository in the given directory log show revision history of entire repository or files merge merge working directory with another revision parents show the parents of the working directory or revision pull pull changes from the specified source push push changes to the specified destination remove remove the specified files on the next commit serve export the repository via HTTP status show changed files in the working directory update update working directory view start interactive history viewer use "hg help" for the full list of commands or "hg -v" for details
Kontrola wersji z poziomu interfejsu graficznego
Istnieje jednak możliwość pracy graficznej. Windowsowi użytkownicy jako graficzny interfejs zwykle wykorzystują TortoiseHG, który wywodzi się z TortoiseSVN. Pod Linuxem możemy również wykorzystać TortoiseHG.
Instalacja Mercuriala i TortoiseHG pod Ubuntu 9.04
Uruchomienie komend zamieszczonych poniżej sprawi, że nasz Mercurial zostanie “podniesiony” do najnowszej stabilnej wersji, pojawi się nam TortoiseHG również w wersji stabilnej, a następnie TortoiseHG zostanie włączone do domyślnego menadżera plików w Gnome – Nautilusa. Zainstalowana zostanie również aplikacja Meld, która czyni merge’owanie całkiem przyjemną czynnością w interfejsie graficznym
# Najpierw przechodzimy na roota, tego którego w Ubuntu nie masudo -i # Dodajemy drzewo stable repozytorium Mercuriala z Ubuntu Launchpad PPA cat >/etc/apt/sources.list.d/mercurial.list << EOF deb http://ppa.launchpad.net/mercurial-ppa/stable-snapshots/ubuntu jaunty main deb-src http://ppa.launchpad.net/mercurial-ppa/stable-snapshots/ubuntu jaunty main EOF sudo apt-key adv --keyserver keyserver.ubuntu.com --recv-key 323293EE # Dodajemy drzewo stable repozytorium TortoiseHG z Ubuntu Launchpad PPA cat >/etc/apt/sources.list.d/tortoisehg.list << EOF deb http://ppa.launchpad.net/tortoisehg-ppa/stable-snapshots/ubuntu jaunty main deb-src http://ppa.launchpad.net/tortoisehg-ppa/stable-snapshots/ubuntu jaunty main EOF sudo apt-key adv --keyserver keyserver.ubuntu.com --recv-key D5056DDE # odświeżamy listę repozytoriów i pobieramy pakiety oraz dodajemy obsługę bindingsów Pythona w Nautilusie aptitude update aptitude install -y python-nautilus mercurial tortoisehg tortoisehg-nautilus meld # doinstalowujemy pakiet iniparse wget http://iniparse.googlecode.com/files/python-iniparse_0.3.1-1_all.deb dpkg -i python-iniparse_0.3.1-1_all.deb rm python-iniparse_0.3.1-1_all.deb
Po wykonaniu tych komend najlepiej się przelogować ponownie, aby Nautilus zdał sobię sprawę z wprowadzonych zmian. Można też (już na swoim użytkowniku) uruchomić komendę:
nautilus -q
Która spowoduje przeładowanie Nautilusa (może nam zniknąć do następnego zalogowania zawartość pulpitu). Jeśli wszystko pójdzie pomyślnie, w efekcie możemy zarządzać repozytorium w następujący sposób:

Powiadomienia SMS mBank mobile – drugie podejście
Dwa miesiące temu przedstawiłem dwa sposoby w jaki można sobie uruchomić powiadomienia SMS od aplikacji lub o przychodzących mailach. Oba miały pewne wady więc zdecydowałem się powrócić do tematu raz jeszcze.
Pierwszym rozwiązaniem, było wykorzystanie +48xxxxxxxxx@text.plusgsm.pl
Rozwiązanie dobre, bo dostajemy natychmiast powiadomienie na komórkę. Dodatkowo większość skrzynek pocztowych ma możliwość ustawienia automatycznej kopii na drugi adres – więc w dosłownie chwilę możemy sobie ustawić automatyczne powiadomienie o mailach.
Rozwiązanie to ma jednak swoją wadę – dostajemy pełną treść, która nie zmieści się w jednym SMS’ie jeśli w mailu jest jakakolwiek treść poza samym tematem.
Drugim zasugerowanym przeze mnie rozwiązaniem było wykorzystanie możliwości wysyłania powiadomień SMS z Google Calendar. To rozwiązanie również jest dobre. Możemy wykorzystać konto w Google Apps, dzięki czemu adres powiadomień jest bardziej uniwersalny, ale ma też swoje wady – chodzi mianowicie o dzienny limit powiadomień SMS z kalendarza Google. Jak się przekonałem, taki limit jest. Dokładnej liczby nie znam, ale w przybliżeniu to około 25 SMS’ów.
Rozwiązanie optymalne?
Postanowiłem zatem poszukać czegoś po środku. Wykorzystałem obsługę skrzynki Google do pobierania wiadomości, a następnie jedynie nadawcę i temat przekazuje do usługi text.plusgsm.pl. Wydaje mi się, że rozwiązanie jest optymalne. Oto kod:
#!/usr/bin/env python
# -- encoding: utf8 --
__author__ = 'mw AT nme.pl'
__version__ = '1.4'
__date__ = 'pią, 25 wrz 2009, 13:22:28 CEST'
class setup:
verbose = False
class google:
login = 'KONTO@GOOGLE'
password = 'HASŁO'
class notify:
recipient="+48xxxxxxxxx@text.plusgsm.pl"
import poplib
import email
import email.Header
#import sms
import smtplib
import re
class Fwd:
def __init__(self,address,subject):
mesg = "From: %s\nTo: %s\nSubject: %s\n\n" % (address, setup.notify.recipient, subject)
server = smtplib.SMTP('smsb.plusgsm.pl')
if setup.verbose:
server.set_debuglevel(1)
server.sendmail (address, setup.notify.recipient, mesg)
server.quit()
class Mailbox:
def __init__(self):
self.server = poplib.POP3_SSL('pop.gmail.com', 995)
self.server.user(setup.google.login)
self.server.pass_(setup.google.password)
self.server.set_debuglevel(0)
# fetch
count = self.server.stat()[0]
if setup.verbose:
print count
if count:
for i in range(1,count+1):
(header, msg, octets) = self.server.retr (i)
mail = email.message_from_string('\n'.join(msg))
address = email.Header.decode_header(mail['From'])[1]
if address[1]:
address = address[0].decode(address[1],'ignore')
else:
address = address[0]
address = re.sub('>\s*$','',re.sub('^\s*< ','',address))
subject = email.Header.decode_header(mail['Subject'])[0]
if subject[1]:
subject = subject[0].decode(subject[1],'ignore')
else:
subject = subject[0]
#print subject
#sms.parse(subject.split())
Fwd(address,subject)
#self.server.dele (i)
# commit
self.server.quit()
mbox = Mailbox()
Aby te powiadomienia działały, na koncie shellowym umieszczamy skrypt, nadajemy mu prawa do uruchomienia, przechodzimy do edycji crona:
chmod +x pop2smsV2.py crontab -e
dodajemy zadanie do crona, aby był uruchamiany co minutę:
* * * * * /home/NAZWA/bin/pop2smsV2.py >/dev/null 2>&1
Podsumowanie
Teraz to rozwiązanie najbardziej przypomina mi to które miałem w Erze. Trzeba niestety użyć do tego shella z cronem. OVH odpada z dwóch powodów – brak crona oraz brak dostępu do sieci z poziomu shella. Może i by się dało uruchomić to jako CGI z ich webowym cronem, ale niestety mija się to trochę z celem, ponieważ cron OVH może być uruchamiany najczęściej co godzinę.
Przewagą tego rozwiązania nad powiadomieniem z Ery jest brak limitu skrzynki 12 MB. Limitem jest ponad 7 GB co nie stanowi już problemu. Jeśli nie chcemy zostawiać na skrzynce śladu – możemy usunąć komentarz z linii self.server.dele (i) – co spowoduje kasowanie przesłanych jako SMS wiadomości.
Zaletą obsługi powiadomień za pomocą crona jest to, że możemy ustalić sztywne godziny w których skrypt ma działać – możemy ustawić, że powiadomienia mogą przychodzić pomiędzy 8:00, a 16:00 oraz 18:00 – 22:00 (trzeba się wtedy liczyć z faktem, że o ósmej rano otrzymamy wszystkie SMS’y z nocy). Jeśli chcemy mieć powiadomienia aktywne w godzinach 8:30 – 22:30, robimy to następującymi regułami:
30-59 8 * * * /home/nme/sms-notify/pop2smsV2.py >/dev/null 2>&1 * 9-21 * * * /home/nme/sms-notify/pop2smsV2.py >/dev/null 2>&1 0-30 22 * * * /home/nme/sms-notify/pop2smsV2.py >/dev/null 2>&1
Dodatkowo – dzięki temu rozwiązaniu mamy adres do powiadomień, znacznie łatwiejszy do zapamiętania – bo w postaci np. powiadomienia@nasza-domena.pl
Kontrolka button w jQuery UI
Ogólny trend rozwoju interfejsów aplikacji spycha je coraz bardziej w kierunku webowym. Zmieniają się dzięki temu developerzy i sposób postrzegania aplikacji. Lekkość aplikacji zaczyna być w cenie. Krowiaste applety Javy, przerośnięte Flashe zaczynają powoli ustępować lżejszym rozwiązaniom. Jednym z nich jest jQuery UI.
Czym jest jQuery UI
jQuery UI to mini-framework dla aplikacji po stronie klienta. Powstał on przy projekcie jQuery, a zespoły tych projektów są dość mocno zintegrowane – świadczy o tym m.in About jQuery, wspólna dokumentacja oraz tempo rozwoju. Zespół UI jest dodatkowo zrzeszony w Filament Group, pod szyldem którego publikuje całkiem często dość ciekawe artykuły bądź to opisując UI, Aria, bądź uzupełniając kontrolki o nowe i jednocześnie je omawiając. Na ich stronie nie znalazłem RSS’a, ale o nowościach zawsze wspominają na swoim Twitterowym profilu filamentgroup – którego kanał RSS sobie zasubskrybowałem w moim Google Readerze – rozwiązanie spisuje się dobrze
Za pomocą jQuery UI możemy przygotować graficzny interfejs naszej aplikacji webowej. Mamy do dyspozycji bibliotekę funkcji rozszerzających standardowe efekty o nowe, własne kontrolki, tematy graficzne (które możemy rozszerzać o własne) wraz z rewelacyjnie narysowanymi ikonkami, dostosowanymi do każdego z tematów. Żeby zobaczyć jak to wszystko wygląda w różnych standardowych tematach graficznych – przykładowy podgląd ThemeRoller.
Standardowa baza kontrolek (widget’ów) jest niewielka – kontrolka daty, okno dialogowe, progress bar, taby. Dodatkowych można się doszukać na SVN’ie w labsach albo poprzez wspomnianą wyżej stronę filamentgroup.
Wprowadzenie do jQuery UI w Twitterowym stylu
Do html’a naszej aplikacji załaczamy CSS z wybranego tematu jQuery UI oraz skrypty jQuery i jQuery UI – przykładowo:
<link href="theme/start/jquery-ui.css" type="text/css" rel="stylesheet"/>
<script src="js/jquery.js" type="text/javascript"></script>
<script src="js/jquery-ui.js" type="text/javascript"></script>
Mamy teraz dostępną całą paletę styli i narzędzi jQuery UI. Zarówno jedno jak i drugie jest bardzo łatwo rozszerzalne.
Jak to rozbudować?
Stworzymy sobie przycisk (ang. button) – przyda się nam omówiona w poprzednim wpisie biblioteka do generowania dynamicznego CSS. Tak mniej więcej wygląda początkowy szkielet aplikacji:
$(document).ready(function() {
var self = this;
self.css = new CSS('dynamic');
//include('button.js');
});
Jak wszyscy wiemy, w JavaScript nie ma include. Można zrobić tak aby był, ale o tym napiszę niebawem. Póki co w jego miejsce możemy wstawić kod właściwej kontrolki.
// ui.button
self.css.replace('.cx-button',{
'outline':'0',
'margin':'0 4px 0 0',
'padding':'.4em 1em',
'text-decoration':'none !important',
'cursor':'pointer',
'position':'relative',
'text-align':'center',
'zoom':'1'
});
// remove extra button width in IE
self.css.replace('a.cx-button',{
'float':'left'
});
(function($) {
$.widget('ui.button', {
_init: function() {
this.element
.addClass('cx-button ui-state-default ui-corner-all')
.hover(function() {
if (! $(this).hasClass('ui-state-disabled')) {
$(this).addClass('ui-state-hover');
}
},function() {
$(this).removeClass('ui-state-hover');
})
.mousedown(function() {
if (! $(this).hasClass('ui-state-disabled')) {
$(this).addClass('ui-state-active');
}
})
.mouseup(function() {
$(this).removeClass('ui-state-active');
})
.disableTextSelect();
},
disabled: function() {
this.element.addClass('ui-state-disabled');
},
enabled: function() {
this.element.removeClass('ui-state-disabled');
},
destroy: function() {
this.element
.removeClass('cx-button ui-state-default ui-corner-all ui-state-hover ui-state-active ui-state-disabled')
.unbind('mouseenter mouseleave');
}
});
})(jQuery);
Takie właśnie przyciski zostały przeze mnie użyte na przykładowej stronie flatforma.appspot.com – należy najpierw przekliknąć się w accordeonie po lewej, a wtedy pojawią się przyciski, które dodają nowe zakładki. Dla leniwych zrzut ekranu:

Zachęcam do analizy przedstawionego w tym tekscie kodu – można go uznać za bazowy szkielet do tworzenia kontrolek.
Styling Buttons and Toolbars with the jQuery UI CSS Framework
Dynamiczny CSS z poziomu JavaScript
Jak zapewne większość developerów używających JavaScriptu, mam swoją własną biblioteczkę skryptów oraz dodatkowych narzędzi która stanowi dla mnie formę bootstrapu do tworzenia nowej aplikacji. Z każdym projektem biblioteczka rośnie i staje się bardziej dojżała. Przymierzam się od jakiegoś czasu do opublikowania całego bootstrapu, bo uważam, że jest warty uwagi, ale jakoś nie miałem do tej pory zbytnio na to czasu.
W tym tekscie poniżej przedstawie jeden ze skryptów który należy do mojej biblioteczki. Załączam go standardowo do każdej swojej aplikacji, szczególnie teraz, kiedy zacząłem przygotowywać swoje własne rozszerzenia JQuery UI.
Swobodniejsze podejście
Bardzo lubie dbać o to, aby struktura aplikacji była zwięzła, łatwa do ogarnięcia oraz estetyczna. W dobie Javascriptu mamy coraz to większe możliwości aby samemu sobie ustrukturyzować aplikacje.
Wielu doświadczonych developerów zakłada, sztywny podział – czyli, że wygląd powinien być w plikach CSS, funkcjonalność w JavaScript, a dane na serwerze. Oczywiście z trzecim się zgadzam, ale z podziałem na wygląd i funkcjonalność – już nie koniecznie.
Przykładowo – w naszym pliku CSS wykorzystujemy -moz-border-radius i -webkit-border-radius, dzięki czemu mamy zaokrąglone rogi wszędzie, poza Internet Explorerem. Musimy dodać zaokrąglone rogi w Explorerze. Co począć? Nabrudzić sobie w CSSie? A później za pomocą JQuery sprawdzić wersję przeglądarki i ustawić te klasy? Można i tak. Możemy jednak sprawdzić czy mamy do czynienia z IE i jeśli tak – dopiero wtedy dodać nowe klasy CSS.
Dynamiczne ładowanie lub generowanie CSS nie jest czymś bez czego nie możnaby się obyć, ale dzięki warstwom abstrakcji możemy sobie nie śmiecić w plikach z layoutem.
Poniższy kod działa bez problemu w każdej zaawansowanej przeglądarce WWW oraz w Internet Explorerze
/* * CSS Gen * Copyright (c) 2009 nme.pl * Dual licensed under MIT and GPL. * * from scratch, based on article http://www.hunlock.com/blogs/Totally_Pwn_CSS_with_Javascript * * Example usage: * * var css = new CSS('dynamic'); // Create new stylesheet * css.replace('.asd',{'color':'green'}); // If there is no asd class definition yet, it will be created * */ var CSS = function(name,url) { if (name === undefined) { return false; } var styles = document.styleSheets; if (!styles) { return null; } this.find_style = function(name) { if (styles) { for (var i in styles) { if (styles[i].title === name) { return styles[i]; } } } return null; }; var create_style = function(name) { var node = document.createElement('style'); node.type = 'text/css'; node.rel = 'stylesheet'; node.media = 'screen'; node.title = name; document.getElementsByTagName(”head”)[0].appendChild(node); return node; }; var load_style = function(name,url) { var node = document.createElement('link'); node.type = 'text/css'; node.rel = 'stylesheet'; node.href = url; node.media = 'screen'; node.title = name; document.getElementsByTagName(”head”)[0].appendChild(node); return node; }; var style = this.find_style(name); if (style === null) { if (url === undefined) { style = create_style(name); } else { style = load_style(name,url); } } var sheet = null; if (style !== null) { if (style.styleSheet !== undefined) { sheet = style.styleSheet; } else { sheet = style.sheet; } } this.index = function(search) { search = search.toLowerCase(); var i=0; var rule=null; if (sheet === undefined) { return null; } do { if (sheet.cssRules) { rule = sheet.cssRules[i]; } else if (sheet.rules) { rule = sheet.rules[i]; } if (rule) { if (rule.selectorText.toLowerCase() == search) { return i; } i++; } } while (rule); return null; }; this.rule = function(search) { search = search.toLowerCase(); var i=0; var rule=null; if (sheet === undefined) { return null; } do { if (sheet.cssRules) { rule = sheet.cssRules[i]; } else if (sheet.rules) { rule = sheet.rules[i]; } if (rule) { if (rule.selectorText.toLowerCase() == search) { return rule; } i++; } } while (rule); return null; }; this.add = function(search, body) { if (sheet === undefined) { return null; } if (typeof body === 'object') { var rules=''; for (var i in body) { if (body.hasOwnProperty(i)) { rules = rules+i+':'+body[i]+';\n'; } } body = rules; } if (!this.rule(search)) { if (sheet.cssRules) { sheet.insertRule(search+' {'+body+'}', 0); } else if (sheet.rules) { sheet.addRule(search, body); } } return this.rule(search); }; this.remove = function(search) { var i = this.index(search); if (i !== null) { if (sheet.cssRules) { sheet.deleteRule(i); } else if (sheet.rules) { sheet.removeRule(i); } } }; this.replace = function(search, body) { this.remove(search); return this.add(search,body); }; };
Jak tego używać? Na początku przygotowujemy nowy, dynamiczny stylesheet:
var css = new CSS('dynamic');
Jeśli klasa asd nie została jeszcze zdefiniowana, robimy to teraz oraz ustawiamy jej następujące atrybuty:
css.replace('.asd',{'color':'green'});
Pseudoklasa CSS umożliwia również dynamiczne ładowanie CSS’ów czy usuwanie poszczególnych reguł styli. W razie wątpliwości – zachęcam do zaglądania do kodu oraz do kontaktu ze mną
Przykładową aplikacją, która dość intensywnie korzysta z tego fagmentu kodu jest http://mlchat.appspot.com – czat, który uruchomiłem na Google App Engine, który jednak zarzuciłem, bo nie znalazłem już sił po stworzeniu go na jego odpluskwianie i promocję
Wygląd aplikacji miał być z zamierzenia łudząco podobny do Google Web Toolkit‘u i co za tym idzie takich aplikacji jak Google Reader, ale tak naprawdę było to JQuery i trochę pracy
Zachęcam do poklikania – ludzi tam raczej nie ma, jak sugerowałyby liczby, to tylko duchy – stare sesje się nie kasują jak powinny
Podgląd strony do pliku jpg?
Poruszenie w Internecie tematyką wprowadzania cenzury, o której ostatnio pisałem, podsunęło mi pewien pomysł, który mam nadzieję niebawem Wam przedstawię. W między czasie uznałem, że w projekcie przyda mi się funkcjonalność zautomatyzowanego robienia podglądów (thumbnail) stron WWW do obrazków. Miałem przy tym całkiem dużo frajdy i mam nadzieję, że efekty mojej pracy komuś się to przydadzą.
Serwisów które przygotowują podgląd stron jest w sieci niewiele, a takich, które za darmo udostępniają wygodne API, nie mają śmiesznie małych limitów przygotowań podglądów nie znalazłem. Napisałem sobie takie narzędzie sam. Jeśli ktoś chce wykorzystać ten kod – proszę bardzo – proszę tylko o informację gdzie jest on dostępny oraz liczę na darmowy dostęp
Twoja własna mini-przeglądarka
Jak szybko zrobić sobie przeglądarkę z obsługą Flasha, JavaScriptu i tych wszystkich urozmaiceń? Wystarczyło mi kilkanaście minut
Zdecydowałem się na użycie GTK (domyślnego zestawu kontrolek dla m.in. Gnome) oraz Webkit’a – silnika do renderowania wykorzystywanego w takich przeglądarkach jak Safari (Mac OS), Google Chrome (Windows) czy Midori (Linux):
#!/usr/bin/env python
url = 'http://www.nme.pl'
size = [800, 600]
import gtk,webkit
window = gtk.Window(gtk.WINDOW_TOPLEVEL)
window.set_default_size(size[0], size[1])
window.connect('destroy', gtk.main_quit)
preview = webkit.WebView()
preview.open(url)
scrolled = gtk.ScrolledWindow()
scrolled.add(preview)
window.add(scrolled)
window.show_all()
gtk.main()
Właściwy podgląd strony
Kod trzeba było trochę rozbudować. Konfiguracja jest w klasie conf. Uruchamiając kod w pierwszym parametrze należy podać adres strony (nie zapominając o nazwie protokołu, np. http://www.nme.pl).
w katalogu w którym znajduje się skrypt trzeba założyć podkatalog thumb, w którym zapisywane będą obrazki jpg.
shot.py:
#!/usr/bin/env python
__author__ = 'mw AT nme.pl'
__version__ = '1.0'
__date__ = 'wto, 25 sie 2009, 09:15:48 CEST'
import pygtk
pygtk.require('2.0')
import gtk,gtk.gdk,gobject
import webkit
import hashlib
import os,sys
class conf:
show_scrollbars = True
show_progress = True
script_timeout_secs = 15
size = [1024, 768]
class Snapshot:
def load_progress_changed(self, view, progress):
print '%d%%' % progress
def load_finished(self, view, frame):
print 'loading finished'
w = gtk.gdk.get_default_root_window()
wp = self.window.get_position()
ws = self.window.get_size()
sz = w.get_size()
pb = gtk.gdk.Pixbuf(gtk.gdk.COLORSPACE_RGB,False,8,ws[0],ws[1])
pb = pb.get_from_drawable(w,w.get_colormap(),wp[0],wp[1],0,0,ws[0],ws[1])
if (pb != None):
pb.save('thumbs/'+self.file+'.jpg','jpeg',{'quality':'95'})
gtk.main_quit()
def __init__(self, url=None):
self.url = url
self.file = hashlib.sha512(url).hexdigest()
self.overtime = False
self.window = gtk.Window(gtk.WINDOW_TOPLEVEL)
self.window.set_default_size(conf.size[0], conf.size[1])
self.window.set_decorated(False)
self.window.connect('destroy', gtk.main_quit)
self.preview = webkit.WebView()
self.preview.open(self.url)
if conf.show_progress:
self.preview.connect('load-progress-changed',self.load_progress_changed)
self.preview.connect('load-finished',self.load_finished)
self.scrolled = gtk.ScrolledWindow()
if conf.show_scrollbars:
self.scrolled.props.hscrollbar_policy = gtk.POLICY_AUTOMATIC
self.scrolled.props.vscrollbar_policy = gtk.POLICY_AUTOMATIC
else:
self.scrolled.props.hscrollbar_policy = gtk.POLICY_NEVER
self.scrolled.props.vscrollbar_policy = gtk.POLICY_NEVER
self.scrolled.add(self.preview)
self.window.add(self.scrolled)
self.window.show_all()
gobject.timeout_add(conf.script_timeout_secs*1000, self.timeout)
gtk.main()
def timeout(self):
gtk.main_quit()
sys.exit(1)
if len(sys.argv)==2:
Snapshot(url=sys.argv[1])
else:
print >> sys.stderr, "Usage: %s url\n" % sys.argv[0]
sys.exit(1)
Wrapper do generowania właściwego poglądu
Skrypt działa, ale wymaga działania X’ów, coś nam jeszcze wyświetla i zapisuje obrazek w skali 1:1. Da się to zrobić w tle. Potrzebujemy do tego dwóch narzędzi – convert z pakietu imagemagick oraz pakietu xvfb – Virtual Framebuffer ‘fake’ X server. Mając już te narzędzia – całą robotę zrobi za nas skrypt o nazwie snap:
#!/bin/bash
state=0
plik=`python -c "import hashlib; print hashlib.sha512('$1').hexdigest()+'.jpg'"`
[ -e "thumbs/$plik" ] || {
xvfb-run -a --server-args='-screen 0 1024x768x16' python shot.py $1
}
state=$?
[ "$state" -eq "0" ] && {
[ -e "thumbs/$plik" ] && {
convert -scale 310x -quality 75 thumbs/$plik thumbs/$plik
} || {
exit 1
}
} || {
exit 1
}
skryptowi nadajemy prawa do jego uruchamiania:
chmod +x snap
i uruchamiamy:
./snap http://www.nme.pl
w katalogu thumb odnajdujemy gotowego jpg’a (jego nazwa jest hashem sha512 URL’a). Wygląda to tak:
![]()
Powiązane, teksty w języku angielskim:
- Podobne rozwiązanie w oparciu o Qt, warto obejrzeć.
- Przykładowy opis jak można uruchomić serwis do generowania podglądów w oparciu o Django z nawiązaniem do gotowych już rozwiązań.
Oskryptowanie Gmaila
Google nie udostępniło do Gmaila tak wygodnego API jak dla Google Calendar, którego przykład zastosowania umieściłem w poprzednim wpisie. Zastanawiając się jednak chwilę – mamy przecież dostęp do protokołu POP, więc jakieś API się znajdzie
Kontynuując myśl z poprzedniego tekstu – jeśli mam już gotowy skrypt który potrafi wysłać powiadomienie SMS o konkretnej treści, można zrobić skrypt, który jako SMS wyśle np. temat z maila. Et voila:
Wymagania: Python 2.4+, skrypt z sms.py z poprzedniego wpisu.
#!/usr/bin/env python
__author__ = 'mw AT nme.pl'
__version__ = '1.2'
__date__ = 'pią lip 31 13:17:45 CEST 2009'
class setup:
class google:
login = 'KONTO@GOOGLE'
password = 'HASŁO'
import poplib
import email
import email.Header
import sms
class Mailbox:
def __init__(self):
self.server = poplib.POP3_SSL('pop.gmail.com', 995)
self.server.user(setup.google.login)
self.server.pass_(setup.google.password)
self.server.set_debuglevel(0)
# fetch
count = self.server.stat()[0]
print count
if count:
for i in range(1,count+1):
(header, msg, octets) = self.server.retr (i)
mail = email.message_from_string('\n'.join(msg))
subject = email.Header.decode_header(mail['Subject'])[0]
if subject[1]:
subject = subject[0].decode(subject[1],'ignore')
else:
subject = subject[0]
#print subject
sms.parse(subject.split())
self.server.dele (i)
# commit
self.server.quit()
mbox = Mailbox()
Bramka SMS od Google
Oficjalnie Google takiej usługi przynajmniej w Polsce nie udostępnia. Można to jednak osiągnąć w sposób pośredni, który chciałbym dzisiaj przedstawić. Jest bardzo prawdopodobne, że już niebawem, stosowanie takich rozwiązań będzie zbędne, bo ceny usług transmisji danych tanieją, a telefony swoją funkcjonalnością przypominają coraz bardziej PDA.
Spośród aplikacji Google, powiadomienia SMS na tą chwilę możemy otrzymywać jedynie Google Calendar. Dzięki bardzo dobrej polityce tej firmy, do większości ich aplikacji dostępne jest API, które sprawia, że ich integracja ze swoimi rozwiązaniami jest znacznie prostsza. Calendar API jest świetnie udokumentowane, wystarczyła chwila i narzędzie do wysyłania SMS’ów było gotowe.
Do pełni szczęścia potrzebne jest konto w Google, a na nim Gmail i kalendarz. Posiadając własną domenę oraz uruchomione Google Apps, można założyć sobie dedykowane konto np. powiadomienia@domena.pl co jest znacznie elastyczniejszym rozwiązaniem.
Wymagania: Python 2.4+, Python Client Library
#!/usr/bin/python
__author__ = 'mw AT nme.pl'
__version__ = '1.1'
__date__ = 'pią lip 31 10:42:26 CEST 2009'
class setup:
class google:
login = 'KONTO@GOOGLE'
password = 'HASŁO'
retries = 3
from elementtree import ElementTree
import gdata.calendar.service
import gdata.service
import atom.service
import gdata.calendar
import atom
import getopt
import sys
import string
import time
class CalendarNotify:
def __init__(self):
self.cal_client = gdata.calendar.service.CalendarService()
self.cal_client.email = setup.google.login
self.cal_client.password = setup.google.password
self.cal_client.source = 'api'
self.cal_client.ProgrammaticLogin()
def reminder(self,event,minutes):
for a_when in event.when:
if len(a_when.reminder) > 0:
a_when.reminder[0].minutes = minutes
else:
a_when.reminder.append(gdata.calendar.Reminder(minutes=minutes))
self.cal_client.UpdateEvent(event.GetEditLink().href, event)
def event(self,title,where=None):
try:
# add event
event = gdata.calendar.CalendarEventEntry()
event.title = atom.Title(text=title)
#event.content = atom.Content(text=content)
if where:
event.where.append(gdata.calendar.Where(value_string=where))
start_time = time.strftime('%Y-%m-%dT%H:%M:%S.000Z', time.gmtime(time.time() + 180))
end_time = time.strftime('%Y-%m-%dT%H:%M:%S.000Z', time.gmtime(time.time() + 180 + 60))
event.when.append(gdata.calendar.When(start_time=start_time, end_time=end_time))
new_event = self.cal_client.InsertEvent(event, '/calendar/feeds/default/private/full')
# add reminder
self.reminder(new_event,1)
except:
return False
return True
def parse(args):
where = None
if len(args):
if args[0][0] == '@':
where = args.pop(0)
count = 0
while count < setup.retries:
if cal.event(' '.join(args),where):
break
count += 1
try:
cal = CalendarNotify()
except:
print >>sys.stderr, 'Unable to login to Google Calendar!'
exit(1)
if __name__ == '__main__':
args = sys.argv
name = args.pop(0)
if not len(args):
print >>sys.stderr, 'Usage: %s [@where] message\n' % name
exit(1)
parse(args)
Uruchamiając skrypt można podać opcjonalny parametr @lokalizacja, a następnie podajemy treść powiadomienia – przykładowa komenda:
./sms.py @firma awaria bazy danych

