Celery com múltiplas filas, retry e tarefas agendadas

No ano passado eu escrevi um post sobre Tarefas demoradas de forma assíncrona com Django e Celery, que serve para a maioria dos casos de uso do celery. O problema é que você pode querer e fazer muito mais com o celery, como ter filas diferentes para tarefas prioritárias, executar um retry após a falha de uma tarefa ou agendar a tarefa para ser executada apenas em outro horário ou outro dia.

Retry em uma Tarefa

Suponhamos que isso seja uma caveira que sua tarefa depende de uma API externa, conexão com algum site ou algum outro motivo que possa levá-la a um ConnectionError, por exemplo.

É provável que dentro de alguns instantes a API, site, etc. esteja no ar e que se você executar sua tarefa novamente ela funcione. Para isso podemos agendar um retry para esta tarefa:


from celery import shared_task
 
@shared_task(bind=True, max_retries=3)
def acessar_sistema_horrivel(self, meu_objeto_id):
    from core.models import MeuObjeto
    from requests import ConnectionError
 
    objeto = MeuObjeto.objects.get(pk=meu_objeto_id)
 
    # Se tiver ConnectionError tenta de novo em 180 segundos
    try:
 
        objeto.acessar_sistema_horrivel()
  
    except ConnectionError as exc:
        self.retry(exc=exc, countdown=180)
 
 

Interessante que dentro de uma função você está usando self.retry. Isso é possível graças ao bind=True no decorator do shared_task, que faz com que nossa função acessar_sistema_horrivel seja na verdade um método da classe Task. Isso nos obriga a colocar self como primeiro argumento da função também.

O número máximo de tentativas para executar essa tarefa é o max_retries=3 definido no decorator

ETA – Agendando a tarefa para começar mais tarde

Agora o cenário é o seguinte, seu sistema vai chamar uma tarefa assíncrona, mais quer que ela seja executada apenas daqui a uma hora.

Para isso podemos chamar a task setando a propriedade ETA (estimated time of arrival) e isso significa que sua tarefa só será executada depois daquele horário (não necessariamente para começar aquele horário, isso vai depender se você possui workers disponíveis, para agendar exatamente como no cron você pode usar o CeleryBeat) .

 


from django.utils import timezone

agora = timezone.now() 
 
# Depois é daqui a uma hora 
#(usei o replace pq quis mostrar, mas vc poderia usar o timedelta para somar também)
depois = agora.replace(hour=agora.hour + 1)
 
acessar_sistema_horrivel.apply_async((meu_objeto_id), eta=depois)
 

Colocando mais de uma fila de execução

Quando você executa o celery ele cria uma fila no seu broker (no caso do post anterior, era o RabbitMQ). A fila padrão chama celery. Qual a implicação? Se você possui várias tarefas assíncronas a serem executadas ficam todas na mesma fila, das qual os workers vão pegar as mensagens e executar.

Imagine que temos além da task acessar_sistema_horrivel, uma outra task chamada tarefa_demorada.

Então dado o seguinte cenário:

3 tarefas: uma lenta e uma mais rápida e a acessar_sistema_horrivel
1 fila
4 workers

E imagine que o sistema produziu 10 requisições da tarefa_demorada para serem executadas e em seguida produziu mais 10 da outra tarefa. O que vai acontecer? Seus workers ficarão todos ocupados buscando executando as requisições de tarefa_demorada que entraram primeiro e nenhum estará trabalhando na tarefa mais rápida.

A solução para isso é produzir filas diferenciadas para as tarefas prioritárias.


# CELERY ROUTES
CELERY_ROUTES = {
    'core.tasks.acessar_sistema_horrivel': {'queue': 'fila_horrivel'},
    'core.tasks.tarefa_demorada': {'queue': 'fila_demorada'},
    'core.tasks.tarefa_mais_rapida': {'queue': 'fila_rapida'},
}

Isso é suficiente para que as mensagens para essas tarefas sejam enviadas para essas filas do RabbitMQ. Detalhe, essas filas são criadas de forma permanente no RabbitMQ, mas isso também pode ser configurado.

Agora para rodar os consumidores para essas filas quando formos executar o celery podemos definir com o parâmetro -Q. No caso fica assim:


celery --app=nome_projeto worker -Q fila_horrivel,fila_demorada,fila_rapida --autoscale=10,5

Estamos iniciando os workers para todas essas filas. Neste caso estou colocando também o parâmetro autoscale, indicando para trabalhar com no mínimo 5 workers e caso haja muitas tarefas o celery adiciona mais workers até o máximo de 10. Isso é bom para economizar recursos do seu sistema, caso você tenha horários com mais produção de tarefas.

Como mostramos no outro post, para usar isto sob o supervisord, você pode modificar aquele mesmo script


[program:celery]
command=/home/deploy/.virtualenvs/meu_virtual_env/bin/celery --app=nome_projeto worker -Q fila_horrivel,fila_demorada,fila_rapida --autoscale=10,5
directory=/home/deploy/webapps/projeto_django
user=nobody
autostart=true
autorestart=true
redirect_stderr=true

Em seguida rodar o reread e o update, porque o arquivo de configuração mudou e reiniciar o supervisord para esse arquivo.


sudo supervisorctl reread
sudo supervisorctl update
sudo supervisorctl restart gunicorn

Com isso seus workers vão executar tarefas de cada uma das filas, não travando os workers para só um tipo de tarefa. Há diversas outras formas de colocar seus workers para funcionar.

Chamando Tarefas Sequenciais

Outro problema comum, é precisar chamar duas tarefas assíncronas , uma depois da outra. Isso pode acontecer em diversos cenários, ex: o caso de a segunda tarefa receber como parâmetro o resultado da primeira, ou da primeira tarefa salvar algum dado e a segunda tarefa trabalhar aquele dado, etc…

Para isso nós podemos usar o Chain, que é o recomendado ao invés de chamar a segunda tarefa assíncrona com o delay dentro da primeira tarefa.


from celery import chain
from tasks import salvar_dados, trabalhar_dados
 
# Estamos chamando o método s das tasks, que as usa como subtasks de uma task gerada pelo chain.
chain(salvar_dados.s(meu_objeto_id) | trabalhar_dados.s())

A tarefa trabalhar_dados vai utilizar o return ou resultado da tarefa salvar_dados como parâmetro.
O chain em sí está gerando uma task, então para ele também serve chamar o apply_async com um eta definido, por exemplo:


chain(salvar_dados.s(meu_objeto_id) | trabalhar_dados.s()).apply_async(eta=depois)

Ignorando os resultados do ResultBackend

Se você só usa as tasks para executar alguma coisa que não precisa do return da task, você pode ignorar o ResultBackend e ter um ganho de performance.

A maioria dos casos não precisa guardar o resultado do return, então é só uma dica interessante se você já está salvando algo nos models, ou fazendo whatever diretamente dentro da sua task, coloque isso no seu settings.py:


CELERY_IGNORE_RESULT = True

 

Na verdade tem muito mais coisas legais no Celery, que ficarão pra um próximo post.

Fontes

http://docs.celeryproject.org/en/latest/userguide/tasks.html

http://docs.celeryproject.org/en/latest/userguide/optimizing.html#guide-optimizing

https://denibertovic.com/posts/celery-best-practices/

https://news.ycombinator.com/item?id=7909201

http://docs.celeryproject.org/en/latest/userguide/workers.html

http://docs.celeryproject.org/en/latest/userguide/canvas.html

 

Super Bônus

Celery Messaging at Scale at Instagram – Apresentação Pycon 2013

De esgotado a empolgado. O que deu certo aceitando um desafio em 2015

“Não será fácil. Mas você já fez coisas difíceis antes.” *

Depois de 10 semanas no vale do silício conversando com investidores, programadores e empreendedores de tecnologia, percebi que o produto que tinha me dedicado por mais de um ano não fazia mais sentido pra mim.

eeeee. nope!

 

Comecei 2015 exausto. Depois de vários anos empreendendo estava me perguntando se era hora de prestar um concurso público ou trabalhar “na minha”.

Pensei em desistir de empreender, pensei em como é difícil tirar algo do papel e sabia que se quisesse empreender teria que passar por várias etapas pelas quais já tinha passado algumas vezes nos últimos anos. Mas as vezes não adianta querer fugir, e o que foi uma simples conversa informal que tive com o Dr Antônio Maia no EC14 acabou se transformando num convite para ser sócio de uma empresa com ideias bastante ambiciosas para transformar a área jurídica no Brasil. As ideias eram excitantes e boas demais para serem preteridas e resolvi então abraçar mais um desafio como CTO de uma empresa de Legal Tech.

Studying law. making a responsible choice for my future

Quem já foi CTO no início de uma startup sabe como é difícil você dar os primeiros passos. Estudar as necessidades de um mercado que não é o seu, planejar as estruturas, começar a escrever o código e o principal e mais difícil: Montar um time de excelência.

Não tem todo mundo, mas tem bastante gente

Não tem todo mundo, mas tem bastante gente

Existe um provérbio grego que diz que O início é metade de qualquer ação. Acho essa metáfora linda, pois não é pra tanto, mas mesmo assim começamos do zero na última semana de fevereiro/15 com uma única posição dentro de um escritório de advocacia e estamos terminando o ano com um conjunto inteiro na Av. Paulista, um time lindo de desenvolvimento, um stande e  apresentação na maior feira da nossa área, com um produto lançado, o LegalNote, crescendo rapidamente com mais de mil usuários cadastrados e milhares de processos e com o nosso outro produto o Diligeiro correndo na fase final de desenvolvimento.

Trabalhamos até a véspera da véspera de ano novo, empolgados por todas essas conquistas que não foram fáceis e eu tenho a certeza de que daqui pra frente tudo continuará sendo difícil, mas nós podemos olhar pra trás e ver que já fizemos coisas difíceis antes.

Muito obrigado à todos! Um excelente 2016!

Um obrigado especial a todos que trabalham duro comigo:

Chuckeeey
Daniel , o mago das regex
Derek Oedenkoven
Dr Antonio Maia
Dr Fabio Abrahao
Gui AMS
Marcus Beckenkamp
Vitão Jar Jar
Gustavo UX
Rafael, o lendário!
Muller #Zueiro
* Li essa frase em algum lugar essa semana e achei fantástica.

E quando uma empresa gigante resolve virar sua concorrente?

Você cria um produto, vira pioneiro em uma área, corre todos os riscos e fica cheio de incertezas, tanto suas, quanto dos clientes confrontados com “A novidade.”

Imagine que nesse cenário, um pequeno grupo abraça aquilo que você fez, ama o seu projeto e você conseguiu criar algo dentro de uma comunidade.

De repente, o gigante acordou, imagine um gigante mesmo, a própria Apple. E ela aprova sua ideia. Aprova tanto que resolve criar uma versão própria do seu produto.

Agora a Apple é sua concorrente!

Steve Jobs - Doctor Evil

E aí, senta e chora?


Isso aqui não é uma anedota, é um caso verídico.

Os criadores do smartwatch Pebble já passaram por vários perrengues, foram acelerados pela YCombinator, o projeto não foi pra frente, não conseguiram levantar capital porque eram uma empresa de Hardware, depois concluíram com sucesso uma campanha no kickstarter e por fim foram lançando e evoluindo o Pebble, tudo a partir da ideia do Eric Migicovsky de criar um display de relógio que mostrasse as notificações do celular.

Bom, imagine a cabeça desse cara quando o Google resolveu lançar o próprio smartwatch e quando a Apple resolveu lançar o Apple Watch.

Agora imagina como ele se sente, vendo que um concorrente desse tamanho, um gigante, está mais abrindo mercado para eles do que prejudicando.

E pode ser que um dia o Pebble se torne tão grande e vire realmente um competidor do Apple Watch. Quem sabe…

 

Por quê estou falando tudo isso?

Porque com o lançamento do Apple Watch muita gente que não tinha ideia do que era um SmartWatch acabou descobrindo o que é isso e, independente de descobrir através da Apple, puderam também se interessar e ir atrás de outros modelos, conhecendo o Pebble, que Dobrou suas vendas!

Veja: Aparentemente Apple Watch ajudou a DOBRAR as vendas do Pebble

 

Me inspirei pra escrever no post do Diogo Novaes no facebook. Aliás, um cara que vale muito a pena seguir.

Tarefas demoradas de forma assíncrona com Django e Celery

Estou escrevendo esse post empolgado por uma lightning talk que fiz no GruPy-SP (16/07). Assim aproveito pra explicar um pouco mais o que estou dizendo nos slides.

O problema de tarefas demoradas do lado do servidor

Sempre que o cliente faz uma requisição web (request), o servidor faz um processamento, normalmente pra quem usa o django, ele lê a requisição, trata os dados recebidos, salva ou recupera registros do banco de dados (através dos models), faz algum processamento do que será exibido para o usuário, renderiza isso em um template e manda uma resposta (response) para o cliente.

Dependendo da tarefa que você executa no servidor a resposta pode demorar muito e isso leva à problemas de TimeOut, a experiência do usuário fica comprometida (quanto mais o servidor demorar pra responder maior a chance do usuário sair do seu site) e também pode acontecer da lentidão ser causada por outros, como por exemplo o uso de uma API externa.

Existem diversas tarefas que podem demorar pra ser executadas. Se você tiver que criar um relatório pesado, acionado por um client web. Se de repente você precisar enviar diferentes emails para uma lista ou por exemplo editar um vídeo depois que o usuário faz o upload na sua página.

Caso real

Esse é um problema que me deparei um dia na geração de um relatório. O relatório levava cerca de 20 minutos para ser gerado, o cliente recebia um erro de timeout e obviamente não dá pra esperar 20 minutos pra gerar alguma coisa. Pra driblar isso e deixar a tarefa em background, alguém resolveu usar um comando do sistema operacional. (No linux se você colocar & no final de um comando esse comando roda em background).

django-view-os-system

Note que em seguida o django envia mostra uma mensagem ao usuário de que ele será avisado por e-mail ao final da geração do relatório.
Alguma coisa não parece certa aqui.

Me arrependo imediatamente dessa decisão

Celery – A solução para esses problemas!

O Celery é um gerenciador de tarefas assíncronas. Com ele você pode executar uma fila de tarefas (que ele recebe por meio de mensagens), pode agendar tarefas direto no seu projeto sem precisar do cron e ele ainda tem integração fácil com a maioria dos frameworks python mais utilizados (django, flask, tornado, etc.)

Como o Celery funciona

Essa imagem kibada da internet dá uma visão geral de como fica o fluxo da sua aplicação.

estrutura-celery

  • O User (ou Client ou Producer) é a sua aplicação Django.
  • O AMPQ Broker é um Message Broker. Um programa responsável por manter a fila de mensagens que serão trocadas entre o seu programa e o Celery, geralmente é o RabbitMQ ou o Redis
  • Os workers (ou consumers) que vão executar as tarefas que você quer que sejam assíncronas.
  • O Result Store, que é onde os workers podem ou não salvar os resultados das tarefas que foram executadas.

O Client pode passar uma tarefa, uma lista de tarefas, tarefas periódicas ou o retry de alguma tarefa pra fila do Message Broker. O Message Broker distribui essas tarefas entre os workers e o resultado dessas tarefas pode ser escrito em um Result Store (Memcahed, RDBMS, MongoDB, etc…) que mais tarde pode ser lido pelo Client novamente.

Qual Broker utilizar?

O recomendado pelo Celery é o RabbitMQ.

Instalando e configurando o RabbitMQ

Existem vários exemplos de como utilizar o Redis, eu vou mostrar como usar o RabbitMQ.

Você só precisa:

  1. Instalar o RabbitMQ (Baixar o pacote no site ou adicionar o repo deles no sources.list e instalar com APT-GET)
    sudo apt-get install rabbitmq-server
  2. Criar um usuário, um virtual host e dar permissões para esse usuário no virtualhost:
    
    sudo rabbitmqctl add_user myuser mypassword
    sudo rabbitmqctl add_vhost myvhost
    sudo rabbitmqctl set_permissions -p myvhost myuser ".*" ".*" ".*"
    

Instalando e configurando o Celery

pip install celery

No seu settings.py:

#Celery Config
BROKER_URL = 'amqp://guest:[email protected]:5672//'
CELERY_ACCEPT_CONTENT = ['json']
CELERY_TASK_SERIALIZER = 'json'
CELERY_RESULT_SERIALIZER = 'json'

Na pasta do projeto (a mesma pasta do settings.py) crie um arquivo chamado celery.py:


from __future__ import absolute_import

import os
from celery import Celery
from django.conf import settings

# set the default Django settings module for the 'celery' program.
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'nome_do_proj.settings')

app = Celery('nome_do_proj')

# Using a string here means the worker will not have to
# pickle the object when using Windows.
app.config_from_object('django.conf:settings')
app.autodiscover_tasks(lambda: settings.INSTALLED_APPS)

Esse autodiscover_tasks permite que seu projeto ache todas as tarefas assíncronas de cada app instalada nele. Nessa mesma pasta do settings.py, você vai modificar o arquivo de pacote __init__.py:


from __future__ import absolute_import

# This will make sure the app is always imported when
# Django starts so that shared_task will use this app.
from .celery import app as celery_app

Criando as tarefas assíncronas do seu app

Na pasta do seu app crie um arquivo chamado tasks.py:

from __future__ import absolute_import
from celery import shared_task
from relatorios import gerar_relatorio_excel


@shared_task
def gerar_relatorio(data_inicial, data_final, email):
    gerar_relatorio_excel(
        data_inicial = data_inicial,
        data_final = data_final,
        email = email
    )
    return "Relatorio Excel gerado"

Agora é só fazer importar essa função que foi criada usando o método delay que foi adicionado à sua função pelo decorator shared_task.

tabelao assincrono

Rodando o Celery

Agora você precisa rodar o celery. Ele vai rodar os seus workers pra executar suas tarefas de forma assíncrona. Por padrão ele inicia um worker para cada núcleo de processador, mas você pode determinar quantos com -c Numero_de_workers

celery --app=nome_projeto worker --loglevel=INFO

E então você verá essa tela:

celery-rodando

Você pode então testar sua função assíncrona com o shell do django mesmo:

chama funcao

E o que aparece na tela que está mostrando o celery:

aparece no celery

Colocando o Celery em Produção

Para usar o Celery em produção eu recomendo que você use o Supervisor

Para instalar o supervisor:

sudo apt-get install supervisor

Depois crie um arquivo de configuração para o celery e coloque um link simbólico para esse arquivo no /etc/supervisor/conf.d/

[program:celery]
command=/home/deploy/.virtualenvs/meu_virtual_env/bin/celery --app=nome_proj worker --loglevel=INFO
directory=/home/deploy/webapps/projeto_django
user=nobody
autostart=true
autorestart=true
redirect_stderr=true

Para o supervisor ler novamente os arquivos que estão em sua pasta de configuração:

sudo supervisorctl reread
sudo supervisorctl update

Agora é só iniciar o celery:

sudo supervisorctl start celery

Bom, acho que é só isso. Esses são os slides da minha apresentação:

Continuação

Celery com múltiplas filas, retry e tarefas agendadas

Fontes

E aqui vão as fontes de pesquisa (ou algumas) que eu utilizei nesse processo:

Bônus:

Como o Feed do Instagram usa o Celery e o RabbitMQ

Um pequeno prazer de uma startup que não deu certo.

 

Quem nunca teve uma empresa que não deu certo, que atire a primeira pedra.

Sempre gostei de empreendedorismo. Eu gostava de ler matérias sobre empresas, gostava de ir nas empresas dos meus pais e gostava dos filmes dos anos 80 estilo Jerry Maguire. Minha família sempre foi empreendedora. Meu pai teve de loja de material de aquarismo até distribuidora de salgadinho de bar, meu tio construiu a maior rede de salões de beleza da zona norte e até hoje não se passa uma semana sem que minha mãe me fale uma ideia nova que ela teve (Se eu publicar alguma, ela me mata). O fato é que o empreendedorismo está no meu sangue.

No final de 2011, eu e meu amigo Daniel resolvemos montar uma startup. Na ocasião eu tinha um site que revendia instrumentos musicais. Nada muito formal, mas era uma escola pra mim. Estava ganhando dinheiro, me mantendo, tive a oportunidade de largar um emprego público e fazer mais dinheiro em casa de cueca do que indo perfumado até o prédio da FEA. Falar sobre a Apoio Musical levaria até mais de um post sobre os 2 anos que eu a mantive no ar e ela me manteve.

Bom, montamos essa startup porque estávamos vidrados em Crowdsourcing. Queríamos muito, que o crowdsourcing fosse um jeito de dar às empresas a oportunidade de gastar pouco pra ter ideias de qualidade e de dar às pessoas a oportunidade de trabalhar em projetos de grandes empresa, mostrando seu potencial.

 

O primeiro problema que enfrentaríamos era o do ovo e o da galinha: como teríamos empresas sem ter pessoas interessadas em anunciar e como teríamos pessoas pra participar dos desafios sem ter empresas com desafios?

Enfim, resolvemos começar buscando às pessoas. (O que hoje eu acredito que não foi a melhor estratégia).

Pra alcançar essas pessoas nós resolvemos criar um Desafio com Ideias que pudessem melhorar a cidade de São Paulo. Chamamos isso de Desafio São Paulo.

Criamos um aplicativo para o facebook pra poder receber as ideias, já que ainda não tínhamos uma plataforma criada e não me lembro como alguém gostou do desafio e ele foi parar no Catraca Livre.

ideias-na-mesa-catraca-livre

Isso foi o suficiente pra várias pessoas entrarem no Desafio. Nisso tivemos um outro problema: Como escolher qual ideia é a melhor? E nisso, a CAOS Focado que é uma empresa de consultoria nos ajudou criando um método objetivo pra definir quais eram as melhores ideias (eu deveria ter filmado pra mostrar quão genial foi o Miguel Chaves resolvendo isso pra gente).

A startup como você já previu no começo do texto, escafedeu-se, fizemos o desafio, não conseguimos fechar com nenhuma outra empresa um desafio sequer, apesar de negociar durante meses com uma empresa grande da área de turismo. Então o que eu estou comemorando aqui como um pequeno prazer? Olhe as ideias escolhidas como as melhores para São Paulo no nosso desafio e pense no que mudou em São Paulo de 2012 para 2015.

  • Ciclo Faixas com acesso a CPTM nas marginais
  • Menos vagas de rua para carros
  • Ruas de lazer aos domingos
  • Transporte Coletivo 24 horas
  • WiFi gratuito em locais públicos

Isso tudo começou a mudar em São Paulo e vai continuar mudando porque agora já é tendência. Se quiser ter certeza, confere meu post original de quando eu publiquei o desafio em março de 2012:

 

http://blog.ideiasnamesa.com.br/desfile-das-campeas-desafio-sao-paulo/

*args e **kwargs não são coisas mágicas em Python

* é um operador e não é só de multiplicação. Hun!?

Muita gente acha que *args e **kwargs são coisas mágicas em Python.

* é um operador que transforma listas em argumentos de uma função.

Por exemplo, imagina que você quer usar a função replace em uma string. Essa função recebe 2 argumentos, o primeiro para o valor que precisa ser substituído e o segundo para o novo valor.

>>> 'a escambau'.replace('a ', 'o ')
>>> 'o escambau'
>>> # Agora tentando passar os parâmetros com uma lista
>>>
>>> args = ['a ','o ']
>>> 'a escambau'.replace(args)
Traceback (most recent call last):
 File "<stdin>", line 1, in <module>
TypeError: replace() takes at least 2 arguments (1 given)
>>> 
>>> #Para que isso funcione basta usar o operador *
>>> 'a escambau'.replace(*args)
'o escambau'


Super simples.

O ** segue a mesma lógica, é um operador que transforma um dicionário em keyword arguments de uma função.

Vou criar uma função que recebe dois argumentos e printa o nome com a idade (como na revista caras)

>>> d = {'nome' : 'Fernando', 'idade': 29}
>>> def caras_me(nome,idade):
...     print("%s(%s)" % (nome,idade))
... 
>>> #Colocando só o dicionário sem o operador:
>>> caras_me(d)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: caras_me() takes exactly 2 arguments (1 given)
>>> # Colocando o dicionário usando o operador **
>>> caras_me(**d)
Fernando(29)

Você pode dar uma olhada na documentação oficial, caso queira:

https://docs.python.org/2/tutorial/controlflow.html#unpacking-argument-lists

Usando Regex para fazer split para mais de um valor

Precisei fazer split em alguns textos jurídicos e como na língua portuguesa os adjetivos sofrem flexão de gênero , tive que separar um texto por mais de um valor.

A sorte é que o módulo re de regex do python é excelente e vem com essa função split, você só precisa passar o pattern e o texto. No caso eu usei ‘Advogado|Advogada ‘, lembrando que o |(pipe) é um “ou” em expressões regulares.

Como criar um instalador MSI utilizando o Inno Setup

Essa é a terceira parte da série de Posts Distribuir Programas em Python para Desktops Windows. Nela você vai aprender como criar um instalador MSI utilizando o Inno Setup e colocando as DLLs do MSVCR para o Python funcionar em qualquer máquina windows.

As outras duas partes são:

Bom, nós vimos como criar um instalador MSI com o cx_freeze e utilizando as DLL’s de MSVCR da própria página no primeiro post, depois vimos como modificar o programa para ele ter um Auto-Update. Só que quando alteramos o setup.py pra gerar o programa com o Auto-Update não conseguimos mais fazer um instalador com o cx_freeze. Isso acontece porque o Esky modifica o seu programa, ele cria um executável que verifica se há algum download disponível no FTP, se tiver ele faz o download, faz o update, verifica se deu certo e exclui as bibliotecas antigas.

Primeira coisa: baixar e instalar o Inno Setup.

O Inno Setup cria um arquivo .iss que nada mais é que um script para criar o seu instalador. Abrindo o Inno você tem a opção de escrever esse script diretamente ou usar o Script Wizard (Um assistente):

inno

 

Nós vamos criar um instalador usando o assistente, primeiro nós vamos utilizar aquele arquivo que geramos no post anterior, o boneca-1.0.1.win32.zip. Você precisa ter descompactado esse arquivo:

boneca-esky-conteudo

No Inno Setup clique em File >> New. Você abrirá o Assistente, ele é fácil de usar, preencha esses dados como quiser:

assistente-inno-setup-1

Na próxima tela você pode escolher onde o Instalador vai colocar o seu programa. O Default é o Program Files folder, mas tem um porém: Se o seu código não tiver Assinatura Digital (Code Signing) você vai ter dificuldades com o UAC do Windows, porque ele não vai conferir a autenticidade do seu código e isso implicará em problemas com antivírus, em bloqueio do programa pelo windows e principalmente ele pode impedir que o programa faça o auto-update, por isso use uma outra pasta a princípio. Você pode escrever um caminho ou usar uma Constante das pastas do Windows do próprio Inno Setup. [Recomendável]

assistente-inno-setup-2

Na terceira tela você vai colocar quais os programas, pastas e arquivos serão instalados pelo instalador, no caso o boneca.exe e o python27.dll ficaram na raiz e também a pasta boneca-1.0.1.win32 com o conteúdo dela.

Então coloque boneca.exe no campo de Application main executable file
Em seguida, clique em Add File para colocar o python27.dll e depois em Add Folder para adicionar a pasta boneca-1.0.1.win32:

assistente-inno-setup-3

Agora é só seguir em diante no assistente, no final ele vai criar o arquivo .iss. Você já pode compilar o arquivo e ele vai gerar o instalador .msi mas está faltando agora adicionar as DLLs do MSVCR. Então baixe de acordo com a sua versão do python:

Agora você precisa modificar o código do seu arquivo .iss pra ele instalar as DLLs junto com o seu programa. Isso aqui eu usei essa solução que achei no StackOverFlow e ela funciona muito bem:

Na seção Files coloque o caminho do vc_redist que você baixou:

[Files]
Source: "vcredist_x86.exe"; DestDir: {tmp}; Flags: deleteafterinstall

No final da seção Run adicione esse código:

[Run]
; add the Parameters, WorkingDir and StatusMsg as you wish, just keep here
; the conditional installation Check
Filename: "{tmp}\vcredist_x86.exe"; Check: VCRedistNeedsInstall

[Code]
#IFDEF UNICODE
 #DEFINE AW "W"
#ELSE
 #DEFINE AW "A"
#ENDIF
type
 INSTALLSTATE = Longint;
const
 INSTALLSTATE_INVALIDARG = -2; // An invalid parameter was passed to the function.
 INSTALLSTATE_UNKNOWN = -1; // The product is neither advertised or installed.
 INSTALLSTATE_ADVERTISED = 1; // The product is advertised but not installed.
 INSTALLSTATE_ABSENT = 2; // The product is installed for a different user.
 INSTALLSTATE_DEFAULT = 5; // The product is installed for the current user.

 VC_2005_REDIST_X86 = '{A49F249F-0C91-497F-86DF-B2585E8E76B7}';
 VC_2005_REDIST_X64 = '{6E8E85E8-CE4B-4FF5-91F7-04999C9FAE6A}';
 VC_2005_REDIST_IA64 = '{03ED71EA-F531-4927-AABD-1C31BCE8E187}';
 VC_2005_SP1_REDIST_X86 = '{7299052B-02A4-4627-81F2-1818DA5D550D}';
 VC_2005_SP1_REDIST_X64 = '{071C9B48-7C32-4621-A0AC-3F809523288F}';
 VC_2005_SP1_REDIST_IA64 = '{0F8FB34E-675E-42ED-850B-29D98C2ECE08}';
 VC_2005_SP1_ATL_SEC_UPD_REDIST_X86 = '{837B34E3-7C30-493C-8F6A-2B0F04E2912C}';
 VC_2005_SP1_ATL_SEC_UPD_REDIST_X64 = '{6CE5BAE9-D3CA-4B99-891A-1DC6C118A5FC}';
 VC_2005_SP1_ATL_SEC_UPD_REDIST_IA64 = '{85025851-A784-46D8-950D-05CB3CA43A13}';

 VC_2008_REDIST_X86 = '{FF66E9F6-83E7-3A3E-AF14-8DE9A809A6A4}';
 VC_2008_REDIST_X64 = '{350AA351-21FA-3270-8B7A-835434E766AD}';
 VC_2008_REDIST_IA64 = '{2B547B43-DB50-3139-9EBE-37D419E0F5FA}';
 VC_2008_SP1_REDIST_X86 = '{9A25302D-30C0-39D9-BD6F-21E6EC160475}';
 VC_2008_SP1_REDIST_X64 = '{8220EEFE-38CD-377E-8595-13398D740ACE}';
 VC_2008_SP1_REDIST_IA64 = '{5827ECE1-AEB0-328E-B813-6FC68622C1F9}';
 VC_2008_SP1_ATL_SEC_UPD_REDIST_X86 = '{1F1C2DFC-2D24-3E06-BCB8-725134ADF989}';
 VC_2008_SP1_ATL_SEC_UPD_REDIST_X64 = '{4B6C7001-C7D6-3710-913E-5BC23FCE91E6}';
 VC_2008_SP1_ATL_SEC_UPD_REDIST_IA64 = '{977AD349-C2A8-39DD-9273-285C08987C7B}';
 VC_2008_SP1_MFC_SEC_UPD_REDIST_X86 = '{9BE518E6-ECC6-35A9-88E4-87755C07200F}';
 VC_2008_SP1_MFC_SEC_UPD_REDIST_X64 = '{5FCE6D76-F5DC-37AB-B2B8-22AB8CEDB1D4}';
 VC_2008_SP1_MFC_SEC_UPD_REDIST_IA64 = '{515643D1-4E9E-342F-A75A-D1F16448DC04}';

 VC_2010_REDIST_X86 = '{196BB40D-1578-3D01-B289-BEFC77A11A1E}';
 VC_2010_REDIST_X64 = '{DA5E371C-6333-3D8A-93A4-6FD5B20BCC6E}';
 VC_2010_REDIST_IA64 = '{C1A35166-4301-38E9-BA67-02823AD72A1B}';
 VC_2010_SP1_REDIST_X86 = '{F0C3E5D1-1ADE-321E-8167-68EF0DE699A5}';
 VC_2010_SP1_REDIST_X64 = '{1D8E6291-B0D5-35EC-8441-6616F567A0F7}';
 VC_2010_SP1_REDIST_IA64 = '{88C73C1C-2DE5-3B01-AFB8-B46EF4AB41CD}';

 // Microsoft Visual C++ 2012 x86 Minimum Runtime - 11.0.61030.0 (Update 4) 
 VC_2012_REDIST_MIN_UPD4_X86 = '{BD95A8CD-1D9F-35AD-981A-3E7925026EBB}';
 VC_2012_REDIST_MIN_UPD4_X64 = '{CF2BEA3C-26EA-32F8-AA9B-331F7E34BA97}';
 // Microsoft Visual C++ 2012 x86 Additional Runtime - 11.0.61030.0 (Update 4) 
 VC_2012_REDIST_ADD_UPD4_X86 = '{B175520C-86A2-35A7-8619-86DC379688B9}';
 VC_2012_REDIST_ADD_UPD4_X64 = '{37B8F9C7-03FB-3253-8781-2517C99D7C00}';

function MsiQueryProductState(szProduct: string): INSTALLSTATE; 
 external 'MsiQueryProductState{#AW}@msi.dll stdcall';

function VCVersionInstalled(const ProductID: string): Boolean;
begin
 Result := MsiQueryProductState(ProductID) = INSTALLSTATE_DEFAULT;
end;

function VCRedistNeedsInstall: Boolean;
begin
 // here the Result must be True when you need to install your VCRedist
 // or False when you don't need to, so now it's upon you how you build
 // this statement, the following won't install your VC redist only when
 // the Visual C++ 2010 Redist (x86) and Visual C++ 2010 SP1 Redist(x86)
 // are installed for the current user
 Result := not (VCVersionInstalled(VC_2010_REDIST_X86) and 
 VCVersionInstalled(VC_2010_SP1_REDIST_X86));
end;


Agora salve e compile seu arquivo. Você terá um arquivo setup.exe como saída na pasta Output (ou o nome que você colocou) e ele vai instalar o seu programa da boneca com as DLLs necessárias pra rodar em qualquer Windows.

Conclusão:

Se você acompanhou os 3 posts você conseguiu criar um programa em python com auto-update e distribuí-lo para qualquer computador windows.

Observações Final sobre Code Signing:

Fiz uma pesquisa sobre code signing no ano passado(2014) , pode ser interessante para alguém:

Autoridade Certificadora Link Preço / ano
Global Sign https://www.globalsign.com/code-signing/ 229 usd
Thawte http://www.thawte.com/code-signing/index.html?tid=a_box_buycs 299 usd
Symantec Verisign http://www.symantec.com/code-signing 499 usd
Godaddy Code Signing http://br.godaddy.com/ssl/code-signing-certificate.aspx 479,99 reais / 199.99 usd
Ksoftware (Comodo partner) http://codesigning.ksoftware.net/ 95 usd
Digicert http://www.digicert.com/code-signing/ev-code-signing-compared.htm 223 usd
Obs:
Comparativo interessante: https://www.sslshopper.com/cheap-code-signing-certificates.html

Guia Rápido para Instalar e rodar o Cordova / Phonegap no Windows

Algumas pessoas que eu conheço tiveram problemas na hora de instalar o Phonegap no Windows e resolvi fazer esse guia rápido de como instalar e rodar o phonegap sem problemas.

1. Instale o Node.js
Vá na página http://nodejs.org/ e clique no botão install que você vai baixar um arquivo de instalação para windows .msi

2. Instale o Git

Vá em http://git-scm.com/downloads e baixe o Git Installer

Depois configure as variáveis globais do git, então abra o cmd e coloque essas duas linhas:

$ git config --global user.name "Seu Nome"
$ git config --global user.email [email protected]

3. Instale o  Phonegap

Abra o cmd e digite:

npm install -g phonegap

4. Faça o Download do Java JDK

Faça Download do Java JDK desse endereço: http://www.oracle.com/technetwork/java/javase/downloads/jdk8-downloads-2133151.html

5. Faça o Download do ANT

Dessa página aqui: http://ant.apache.org/bindownload.cgi depois descompacte os arquivos em algum lugar

6. Faça Download do ADT Bundle

Dessa página http://developer.android.com/sdk/index.html#download e descompacte os arquivos em algum lugar.

7. Configure o Path do Windows

Do desktop, clique com o botão direito em Meu Computador e depois em Propriedades
Clique em Configurações Avançadas na coluna da esquerda
Na janela de Propriedades de Sistema clique no botão Variáveis de Ambiente.
Selecione a variável PATH da seção de Variáveis do Sistema
Clique em Editar.
Dentro da pasta do ADT Bundle que você descompactou pegue o caminho completo até a pasta \sdk\tools e adicione no final da variável PATH.
Copie o caminho para \sdk\platform-tools para a variável PATH também

8. Configure o JAVA_HOME

Crie uma nova variável de ambiente chamada JAVA_HOME e coloque o caminho para o JDK nessa variável. Fica alguma coisa como “C:\Program Files\Java\jdk1.8.0_05

9. Configure o ANT_HOME

Crie uma nova variável de ambiente chamada ANT_HOME e coloque o caminho da pasta ANT que você descompactou.

10. Crie e Lance um Hello World

Usando o cmd escreva:

phonegap create hello
cd hello 
phonegap run android

Dentro da pasta do projeto hello, crie um arquivo index.html e coloque na pasta www.
Conteúdo de index.html:

<html><body>Hello World</body></html>

Depois use o cmd novamente :

phonegap run android

E veja o Hello World na tela do seu telefone

 

Como criar um programa com Auto-Update utilizando Python e Esky

Essa é a segunda parte da série de Posts Distribuir Programas em Python para Desktops Windows. Se quiser, veja o primeiro post: Como criar um executável com Instalador MSI em Python

Sempre que um programa precisa ser atualizado parece um parto. Lembre-se de todas as vezes você teve que baixar a versão atualizada do Java, lembra bem dessa raiva que você sentiu ao ter que fazer isso e imagine que seu usuário sinta a mesma raiva que você, então seja legal e faça um auto-update no seu programa para que ninguém precise baixar as versões novas. Para aprender como criar um programa com Auto-Update utilizando Python e Esky eu vou usar o aplicativo boneca criado na parte 1.

Até aqui tudo normal, o programa está feito, foi compilado mas não tem auto-update, você simplesmente gerou um instalador da boneca e nunca mais vai atualizar esse programa no cliente que o instalou, então você precisa fazer alterações e gerar um novo executável.

Bom, vamos ter que instalar um package chamado Esky:

pip install esky

Agora vamos fazer alterações no código, primeiro no script da Boneca.py precisamos importar o esky e pedir pra ele procurar por atualizações na internet quando estiverem disponíveis.

#Coloque abaixo de import win32con
import esky

if hasattr(sys,"frozen"):
    app = esky.Esky(sys.executable,"http://teenspirit.com.br/exemplo_boneca/")
    app.auto_update()

Toda vez que você iniciar o programa ele vai procurar no site se tem alguma atualização pra baixar. Ele conseguirá reconhecer pelo número da versão. Eu simplesmente deixei esse diretório (http://teenspirit.com.br/exemplo_boneca/) pra fazer listagem no Apache.

Agora vamos fazer alterações no programa, eu vou colocar outra imagem no lugar da boneca, nesse caso, usarei o chuck.jpg

chuck


#Troque o texto de boneca.jpg para chuck.jpg
os.startfile(os.path.join(os.path.realpath(os.path.dirname(sys.argv[0])),"chuck.jpg"))

 

Agora, vamos alterar o setup.py:

#setup.py
import esky.bdist_esky
from esky.bdist_esky import Executable as Executable_Esky
from cx_Freeze import setup, Executable

include_files = ['boneca.jpg','chuck.jpg']

setup(
    name = 'boneca',
    version = '1.0.1',
    options = {
        'build_exe': {
            'packages': ['os','sys','ctypes','win32con'],
            'excludes': ['tkinter','tcl','ttk'],
            'include_files': include_files,
            'include_msvcr': True,
        },
        'bdist_esky': {
            'freezer_module': 'cx_freeze',
        }
    },
    data_files = include_files,
    scripts = [
        Executable_Esky(
            "boneca.py",
            gui_only = True,
            #icon = XPTO #Coloque um icone aqui se quiser ,
            ),
    ],
    executables = [Executable('boneca.py',base='Win32GUI')]
    )

Reparem que eu mudei o número da Versão para 1.0.1, e a partir dessa versão o programa terá o auto-update. Esse arquivo ficou bem diferente, vou tentar explicar tudo que aconteceu nele:

1. Importei a opção de setup bdist_esky e um tipo de executável do próprio Esky

import esky.bdist_esky
from esky.bdist_esky import Executable as Executable_Esky

2. Coloquei as opções para o novo argumento bdist_esky:

        'bdist_esky': {
            'freezer_module': 'cx_freeze',
        }

3. Adicionei o data_files, porquê é isso que o Esky usa para ver quais arquivos também precisam ir para a pasta do programa

      data_files = include_files,

4. Coloquei o scripts porque é daí que o Esky vê quais scripts se tornaram os executáveis.

    scripts = [
        Executable_Esky(
            "boneca.py",
            gui_only = True,
            #icon = XPTO #Coloque um icone aqui se quiser ,
            ),
    ],

Agora para executar você precisa usar o setup.py com o argumento bdist_esky

python setup.py bdist_esky

Ele vai gerar um arquivo compactado que vai ficar na sua pasta dist:

boneca_esky
E dentro desse arquivo você tem isso aqui:

boneca-esky-conteudo

 

O que aconteceu? O Esky criou um programa, boneca.exe que vai ser responsável pela atualização do seu programa, inclusive dele mesmo. Quando ele for aberto ele vai buscar no endereço que eu coloquei (http://teenspirit.com.br/exemplo_boneca/) e se achar um outro arquivo compactado com uma versão mais recente ele vai baixar o arquivo e trocar o conteúdo, então na próxima vez que ele for aberto ele abrirá com a versão mais nova e limpará a versão mais antiga.

O bom é que se der algum problema no pacote, ou na internet ou na energia o Esky gerencia isso para que o programa sempre funcione. (só não pode subir uma versão bichada).

Então, quem de agora em diante for baixar o seu programa terá que baixar essa versão do Esky, que tem o arquivo zipado, aquele que geramos na Parte 1 não tem auto-update. E é por isso que vou mostrar na Parte 3 como fazer uma distribuição bem mais legal com o instalador do Inno Setup. Outra coisa que não é muito legal é que essa versão que criamos não tem o suporte ao MSVCR, mais um motivo para você ter um instalador customizado que vai incluir isso pra você.

Pra testar a atualização eu vou criar mais uma nova versão, que vou chamar de 1.0.2, nela vou trocar a foto, para isso mudo o boneca.py:

os.startfile(os.path.join(os.path.realpath(os.path.dirname(sys.argv[0])),"seu-boneco.jpg"))

e vou adicionar esse novo arquivo seu-boneco.jpg no include_files do setup.py:

include_files = ['boneca.jpg','chuck.jpg', 'seu-boneco.jpg']

Agora é só gerar o novo arquivo do Esky:

python setup.py bdist_esky

Na sua pasta dist vai aparecer o arquivo boneca-1.0.2.win32.zip. É só colocá-lo no link informado e na próxima vez que seus usuários abrirem o programa, ele será atualizado.

Se você quiser testar esse programa, faça o seguinte: baixe o boneca-1.0.1.win32.zip, descompacte o arquivo em uma pasta e abra o boneca.exe .

Quando você apertar o Print Screen ele vai mostrar a foto do Chuck, mas perceba que ele também já estará fazendo a atualização para a versão 1.0.2 e depois de fechado, na próxima vez que o programa rodar ele exibirá a foto do Seu Boneco.

seu-boneco

 

O código continua no Github.

E segura que em breve posto a Parte 3.