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.
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.
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)
A classe ProcessBuilder aceita parâmetros do tipo vargs ("arg1", "arg2",....), ou através de List. 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 ).
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: 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:
- Faça o download do framework; .
- Baixe a versão 0.8.1 (sshj-0.8.1.zip).
- Descompacte o arquivo zip.
- Localizar o arquivo jar sshj-0.8.1.jar.
Esse jar depende de outros dois frameworks:
Nesse projeto foram configurados os seguintes jars:
Acrescente então, as classes da Listagem 3 e 4.
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);
}
}
}
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");
}
}
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"
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:
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:
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.