Executando Shell Scripts utilizando Java

Você precisa estar logado para dar um feedback. Clique aqui para efetuar o login
Para efetuar o download você precisa estar logado. Clique aqui para efetuar o login
Confirmar voto
0
 (3)  (0)

Veja nesse artigo como executar comandos shell scripts em máquinas Linux localmente e remotamente utilizando Java.

Introdução

Os comandos shell script fazem parte da caixa de ferramentas de muitos programadores e são praticamente indispensáveis para aqueles que desejam trabalham com a máxima produtividade em ambientes Linux / Unix.

Por isso, muitos gostariam de poder executar esses comandos dentro de seus programas Java. Veremos nesse artigo como realizar essa atividade, tanto a nível local como remotamente.

Executando comandos locamente

Para executar comandos shell script em máquinas locais, utilizaremos a classe ProcessBuilder, introduzida no java 1.5.

Listagem 1: Código para executar comandos shell localmente

import java.io.BufferedReader;
import java.io.Closeable;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.logging.Logger;

public class LocalShell {

    private static final Logger log = Logger.getLogger(LocalShell.class.getName());    

    public void executeCommand(final String command) throws IOException {
        
        final ArrayList<String> commands = new ArrayList<String>();
        commands.add("/bin/bash");
        commands.add("-c");
        commands.add(command);
        
        BufferedReader br = null;        
        
        try {                        
            final ProcessBuilder p = new ProcessBuilder(commands);
            final Process process = p.start();
            final InputStream is = process.getInputStream();
            final InputStreamReader isr = new InputStreamReader(is);
            br = new BufferedReader(isr);
            
            String line;            
            while((line = br.readLine()) != null) {
                System.out.println("Retorno do comando = [" + line + "]");
            }
        } catch (IOException ioe) {
            log.severe("Erro ao executar comando shell" + ioe.getMessage());
            throw ioe;
        } finally {
            secureClose(br);
        }
    }
    
    private void secureClose(final Closeable resource) {
        try {
            if (resource != null) {
                resource.close();
            }
        } catch (IOException ex) {
            log.severe("Erro = " + ex.getMessage());
        }
    }
    
    public static void main (String[] args) throws IOException {
        final LocalShell shell = new LocalShell();
        shell.executeCommand("ls ~");
    }
}

Ao rodar essa classe, ela irá executar o comando "ls ~", que listará todos os diretórios e arquivos da minha pasta home.

Listagem 2: Resultado da execução do programa

Retorno do comando = [a.txt]
Retorno do comando = [Desktop]
Retorno do comando = [Documents]
Retorno do comando = [Downloads]
Retorno do comando = [examples.desktop]
Retorno do comando = [Music]
Retorno do comando = [NetBeansProjects]
Retorno do comando = [Pictures]
Retorno do comando = [Public]
Retorno do comando = [Templates]
Retorno do comando = [teste.txt]
Retorno do comando = [Videos]
Retorno do comando = [z.txt]
BUILD SUCCESSFUL (total time: 0 seconds) 

Nota: Essa classe foi rodada no Ubuntu 12.04, usando Netbeans 7.0.1 e Java 6. Para aqueles que têm apenas o Windows instalado, podem usar o WMware Player para instalar o Linux de sua preferência, como uma máquina virtual. O WMware Player é muito simples de configurar.

A classe ProcessBuilder aceita parâmetros do tipo vargs ("arg1", "arg2",....), ou através de List<String>. Note que não basta apenas executar o comando "ls ~", é necessário antes incluírmos o seguinte comando: "/bin/bash -c". Isso se deve porque todo comando precisa ser executado em um shell, e no caso estamos determinando que o shell bash processe o comando (Formato da linha de comando: /bin/bash -c <comando>).

Podemos ainda, recuperar a saída do comando passado como argumento (caso ele gere uma saída), através do método getInputStream().

Através da classe ProcessBuilder, podemos então executar vários comandos linux e até script shells, tornando-a extremamente valiosa para implementar automação de tarefas, processamento de texto e outras atividades feitas através de script shells.

Executando comandos remotamente

Obviamente, não podemos utilizar a classe LocalShell para executar comandos remotos em outras máquinas Linux. Temos uma série de fatores que devem ser equacionados para que isso seja possível, a começar pela comunicação de rede e conexão segura.

Geralmente, usuários Linux utilizam o aplicativo SSH, para estabelecer conexão remota com outras máquinas, e assim podem executar comandos no shell.

Felizmente, é possível utilizar a mesma abordagem com Java, através de um cliente SSH embutido, que irá gerenciar toda a parte de conexão e segurança, possibilitando a execução de comandos remotos.

No GitHub, existe um projeto chamado sshj, criado pelo user: https://github.com/shikhar. Ele contém, entre outras coisas, um cliente de ssh.

Iremos criar então no Netbeans um projeto utilizando essa API. (Desta vez o projeto será criado no Windows, para demonstrar que o sshj pode ser usado em outro SO).

Procedimentos:

Esse jar depende de outros dois frameworks:

Nesse projeto foram configurados os seguintes jars:

Bibliotecas do projeto

Figura 1: Bibliotecas do projeto

Acrescente então, as classes da Listagem 3 e 4.

Listagem 3: Classe RemoteShell

package br.com.devmedia.ssh;

import java.io.BufferedReader;
import java.io.Closeable;
import java.io.IOException;
import java.io.InputStreamReader;
import java.security.PublicKey;
import java.util.concurrent.TimeUnit;
import net.schmizz.sshj.SSHClient;
import net.schmizz.sshj.connection.ConnectionException;
import net.schmizz.sshj.connection.channel.direct.Session;
import net.schmizz.sshj.connection.channel.direct.Session.Command;
import net.schmizz.sshj.transport.TransportException;
import net.schmizz.sshj.transport.verification.HostKeyVerifier;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class RemoteShell {

    private final Logger log = LoggerFactory.getLogger(RemoteShell.class);
    private final String machine;
    private final String user;
    private final String password;
    
    public RemoteShell(final String machine, final String user, final String password) {
        this.machine = machine;
        this.user = user;
        this.password = password;
    }

    public void executeCommand(final String command) throws IOException {

        // Cliente SSH
        final SSHClient ssh = new SSHClient();

        try {
            // Configura tipo de KeyVerifier
            setupKeyVerifier(ssh);
            // Conecta com a maquina remota
            ssh.connect(machine);
            // Autenticacao
            ssh.authPassword(user, password);

            // Executa comando remoto
            executeCommandBySSH(ssh, command);
        } finally {
            ssh.disconnect();
        }
    }

    private void executeCommandBySSH(final SSHClient ssh, final String command) throws ConnectionException, IOException, TransportException {
        
        final Session session = ssh.startSession();
        BufferedReader bf = null;

        try {
            // Executa comando
            final Command cmd = session.exec(command);
            bf = new BufferedReader(new InputStreamReader(cmd.getInputStream()));
            String line;
            // Imprime saida, se exister
            while ((line = bf.readLine()) != null) {
                System.out.println(line);
            }
            // Aguarda
            cmd.join(1, TimeUnit.SECONDS);
        } finally {
            secureClose(bf);
            secureClose(session);
        }
    }

    private void setupKeyVerifier(final SSHClient ssh) {
        ssh.addHostKeyVerifier(
                new HostKeyVerifier() {
                    @Override
                    public boolean verify(String arg0, int arg1, PublicKey arg2) {
                        return true;  // sem verificacao 
                    }
                });
    }

    private void secureClose(final Closeable resource) {
        try {
            if (resource != null) {
                resource.close();
            }
        } catch (IOException ex) {
            log.error("Erro ao fechar recurso", ex);
        }
    }
}

Listagem 4: Classe de Teste

package br.com.devmedia.ssh;

import java.io.IOException;

public class TestRemoteShell {
    
    public static void main(String... args) throws IOException {        
        final RemoteShell shell = new RemoteShell("<IP>", "<user>", "<password>");
        shell.executeCommand("ls ~ | sort");        
    }
}


Listagem 5: Saída da execução
a.txt
Desktop
Documents
Downloads
examples.desktop
Music
NetBeansProjects
Pictures
Public
Templates
teste.txt
Videos
z.txt

Podemos usar um comando mais sofisticado. Por exemplo, procurar todos os arquivos .txt, e executar o cksum neles. Para isso, use o comando a seguir: "find ~ -name '*.txt' -exec cksum {} \\; | sort -k3"

Listagem 6: Saída da execução para o comando find

4294967295 0 /home/senaga/a.txt
4294967295 0 /home/senaga/.config/libreoffice/3/user/uno_packages/cache/log.txt
435791989 154 /home/senaga/.mozilla/firefox/jv93gx4x.default/urlclassifierkey3.txt
2918312305 327 /home/senaga/.netbeans/7.0/var/cache/lastModified/all-checksum.txt
4294967295 0 /home/senaga/teste.txt
4294967295 0 /home/senaga/z.txt

A Classe RemoteShell utiliza a SSHClient, que provê toda a infraestrutura necessária para realizar a conexão, autenticação e execução de comandos remotos via protocolo SSH através de métodos bem intuitivos.

O maior ponto de atenção é o método setupKeyVerifier, onde definimos uma classe interna anônima que implementa a interface HostKeyVerifier, para o método addHostKeyVerifier. Para entender o processo de Host Key Verifier, devemos ter em mente como funciona o SSH.

Quando utilizamos o cliente ssh pela primeira vez para conexão ao servidor ssh, ele irá verificar a chave do servidor no arquivo ~/.ssh/known_hosts. Essa chave é conhecida como fingerprint e possue o seguinte formato: 43:51:43:a1:b5:fc:8b:b7:0a:3a:a9:b1:0f:66:73:a8 (exemplo).

Toda vez que conectamos nesse servidor, o cliente ssh irá verificar se a chave é a mesma. Caso ela mude, o cliente pode emitir um aviso de alerta ou abortar a conexão.

Ao acrescentar a classe anônima:

Listagem 5: Verificando a chave

        ssh.addHostKeyVerifier(
                new HostKeyVerifier() {
                    @Override
                    public boolean verify(String arg0, int arg1, PublicKey arg2) {
                        return true;  // sem verificacao 
                    }
                });

Estamos dizendo ao SSHClient que não efetue nenhum tipo de validação para as chaves. Isso faz sentido ao rodar essa aplicação no Windows, onde geralmente não temos o arquivo known_hosts disponível. Já no Linux, para maior segurança, pode-se eliminar totalmente o método setupKeyVerifier, e utilizar essa função:

Listagem 6: Função loadKnownHosts

client.loadKnownHosts();

O método irá carregar as fingerprints do arquivo ~/.ssh/known_hosts e irá impedir a conexão se o fingerprint mudar.

Espero que esse artigo tenha sido útil para auxiliar os desenvolvedores a criarem ferramentas de automação utilizando shell script, localmente ou remotamente, através da poderosa linguagem Java.

Obrigado e até a próxima!

Referências:


 
Você precisa estar logado para dar um feedback. Clique aqui para efetuar o login
Receba nossas novidades
Ficou com alguma dúvida?