Neste artigo veremos como usar a classe Runtime para construir um emulador de terminal linux. Nosso projeto será construído com base nos seguintes requisitos:
- Possibilitar ao usuário executar comandos no emulador de terminal e obter a saída destes;
- Possibilitar a visualização de históricos dos comandos já executados, possibilitando a escolha de um destes para execução;
- Mostrar erros de saída caso o comando executado seja inválido, colorindo a saída de vermelho quando ERRO e preto quando executado corretamente;
- Possibilitar a execução dos comandos através da tecla de atalho F5 para agilizar e facilitar o uso;
- Possibilitar a limpeza dos históricos dos comandos já executados.
Nosso projeto consistirá de três Classes, que são:
- TerminalEmulator: Será a classe “core” responsável por executar os comandos no terminal real do Linux, capturando seus retornos e realizando os tratamentos necessários;
- Fterminal: Formulário que fará a interface entre o usuário e a classe TerminalEmulator;
- Dhistory: Mostrará os históricos de comandos já executados possibilitando a seleção e re-execução de qualquer comando a qualquer momento.
TerminalEmulator
Vamos começar nosso estudo através da classe que de fato faz todo trabalho árduo, executando os comandos e cuidando dos históricos de comando.
Vejamos ela por completa na Listagem 1.
package br.com.lab;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class TerminalEmulator {
private static List<String> commandHistory = new ArrayList<String>();
public static Map<String, String> exec(String command) {
Process proc = null;
Map<String, String> result = new HashMap<String, String>();
try {
proc = Runtime.getRuntime().exec(command);
result.put("input", inputStreamToString(proc.getInputStream()));
result.put("error", inputStreamToString(proc.getErrorStream()));
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
result.put("error", e.getCause().getMessage());
} finally{
commandHistory.add(command);
}
return result;
}
private static String inputStreamToString(InputStream isr) {
try {
BufferedReader br = new BufferedReader(new InputStreamReader(isr));
StringBuilder sb = new StringBuilder();
String s = null;
while ((s = br.readLine()) != null) {
sb.append(s + "\n");
}
return sb.toString();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
return null;
}
}
public static void clearHistory(){
commandHistory.clear();
}
public static List<String> getCommandHistory() {
return commandHistory;
}
}
Toda vez que um comando for executado, seja ele errado ou certo, ele será gravado na lista 'commandHistory':
private static List<String> commandHistory = new ArrayList<String>();
O método getCommandHistory() possibilita que o histórico de comandos possa ser recuperado de outros pontos do sistema e o método clearHistory() faz a limpeza de todos os históricos. Perceba que este possui a palavra reservada “static”, ou seja, seu estado será mantido enquanto a aplicação estiver em execução.
O método mais importante de todos é o “exec()”, pois é ele quem vai executar o comando no terminal real, ou seja, o terminal do Linux. O exec() retorna um Map contendo sempre a chave “input” e a chave “error”. A chave “input” contém o conteúdo retornando quando a execução for bem-sucedida, enquanto a chave “error” retorna o conteúdo de erro, quando houver.
A linha que faz tudo funcionar é:
proc = Runtime.getRuntime().exec(command);
Esta captura a instância de Runtime e chama o método “exec()”, passando o comando desejado (ex: 'ls -la'). O seu retorno é um objeto do tipo “Process”, e é através deste objeto que capturaremos o retorno caso sucesso ou erro.
Nas linhas a seguir chamamos o método inputStreamToString que faz a conversão de InputStream para String (explicaremos mais à frente):
result.put("input", inputStreamToString(proc.getInputStream()));
result.put("error", inputStreamToString(proc.getErrorStream()));
O objeto Process possui o getInputStream e getErrorStream, onde o primeiro possui o conteúdo de retorno quando sucesso, e o segundo possui o conteúdo de erro quando falha.
Se não houver falhas, então o valor da chave “error” ficará vazia, porém, se houver erro o valor da chave “input” ficará vazio. Se ocorrer um erro interno de IOException gravamos a causa dele na chave “error” do nosso Map:
catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
result.put("error", e.getCause().getMessage());
}
Com isso garantimos que o usuário que estará usando o Emulador de Terminal saberá o que está ocorrendo e que seu comando não foi executado.
O histórico será gravado independente se houve ou não erro, por isso colocamos ele no bloco finally:
finally{
commandHistory.add(command);
}
O método inputStreamToString() converte um objeto do tipo InputStream para String. Para que isso seja possível é criado um InputStreamReader e posteriormente um BufferedReader. É com o BufferedReader que fazemos a leitura das linhas através do “readLine()”.
Fterminal
Dada a explicação necessária para uso do TerminalEmulator.java podemos começar a construção do nosso formulário Fterminal, que fará a “ponte” entre o usuário e o TerminalEmulator. Ele será igual a Figura 1.
Nosso formulário tem dois JtextArea, sendo o primeiro servidor como console para digitação de comandos e o segundo para saída dos comandos. Chamamos de “jTextAreaTerminal” e “jTextAreaOutput”, respectivamente.
Vejamos nossa classe Fterminal por completo na Listagem 2.
package br.com.lab;
import java.awt.Color;
import java.awt.event.KeyEvent;
import java.util.Map;
import javax.swing.JOptionPane;
public class FTerminal extends javax.swing.JFrame {
/**
*
*/
private static final long serialVersionUID = 1L;
/**
* Creates new form FTerminal
*/
public FTerminal() {
initComponents();
}
/**
* This method is called from within the constructor to initialize the form.
* WARNING: Do NOT modify this code. The content of this method is always
* regenerated by the Form Editor.
*/
@SuppressWarnings("unchecked")
// <editor-fold defaultstate="collapsed" desc="Generated Code">
//GEN-BEGIN:initComponents
private void initComponents() {
jButtonExec = new javax.swing.JButton();
jScrollPane1 = new javax.swing.JScrollPane();
jTextAreaTerminal = new javax.swing.JTextArea();
jScrollPane2 = new javax.swing.JScrollPane();
jTextAreaOutput = new javax.swing.JTextArea();
jButtonHistory = new javax.swing.JButton();
setDefaultCloseOperation(javax.swing.WindowConstants.EXIT_ON_CLOSE);
jButtonExec.setText("Executar");
jButtonExec.addActionListener(new java.awt.event.ActionListener() {
public void actionPerformed(java.awt.event.ActionEvent evt) {
jButtonExecActionPerformed(evt);
}
});
jTextAreaTerminal.setBackground(new java.awt.Color(216, 208, 201));
jTextAreaTerminal.setColumns(20);
jTextAreaTerminal.setRows(5);
jTextAreaTerminal.addKeyListener(new java.awt.event.KeyAdapter() {
public void keyPressed(java.awt.event.KeyEvent evt) {
jTextAreaTerminalKeyPressed(evt);
}
});
jScrollPane1.setViewportView(jTextAreaTerminal);
jTextAreaOutput.setEditable(false);
jTextAreaOutput.setColumns(20);
jTextAreaOutput.setRows(5);
jScrollPane2.setViewportView(jTextAreaOutput);
jButtonHistory.setText("Histórico");
jButtonHistory.addActionListener(new java.awt.event.ActionListener() {
public void actionPerformed(java.awt.event.ActionEvent evt) {
jButtonHistoryActionPerformed(evt);
}
});
javax.swing.GroupLayout layout = new javax.swing.GroupLayout(getContentPane());
getContentPane().setLayout(layout);
layout.setHorizontalGroup(
layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
.addGroup(layout.createSequentialGroup()
.addContainerGap()
.addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
.addComponent(jScrollPane1)
.addGroup(layout.createSequentialGroup()
.addComponent(jButtonExec)
.addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
.addComponent(jButtonHistory)
.addGap(0, 0, Short.MAX_VALUE))
.addComponent(jScrollPane2, javax.swing.GroupLayout.DEFAULT_SIZE,
528, Short.MAX_VALUE))
.addContainerGap())
);
layout.setVerticalGroup(
layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
.addGroup(javax.swing.GroupLayout.Alignment.TRAILING, layout.
createSequentialGroup()
.addContainerGap()
.addComponent(jScrollPane1, javax.swing.GroupLayout.
PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.
PREFERRED_SIZE)
.addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.
RELATED)
.addComponent(jScrollPane2, javax.swing.GroupLayout.DEFAULT_SIZE, 134,
Short.MAX_VALUE)
.addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED)
.addGroup(layout.createParallelGroup(javax.swing.GroupLayout.
Alignment.BASELINE)
.addComponent(jButtonHistory)
.addComponent(jButtonExec))
.addGap(14, 14, 14))
);
pack();
}// </editor-fold>//GEN-END:initComponents
private void jButtonHistoryActionPerformed(java.awt.event.ActionEvent evt)
{//GEN-FIRST:event_jButtonHistoryActionPerformed
DHistory dhistory = new DHistory(this, true);
String command = dhistory.getSelectedCommand();
if (command != null){
jTextAreaTerminal.setText(command);
}
}//GEN-LAST:event_jButtonHistoryActionPerformed
private void jButtonExecActionPerformed(java.awt.event.ActionEvent evt)
{//GEN-FIRST:event_jButtonExecActionPerformed
exec();
}//GEN-LAST:event_jButtonExecActionPerformed
private void jTextAreaTerminalKeyPressed(java.awt.event.KeyEvent evt)
{//GEN-FIRST:event_jTextAreaTerminalKeyPressed
if (evt.getKeyCode() == KeyEvent.VK_F5){
exec();
}
}//GEN-LAST:event_jTextAreaTerminalKeyPressed
private void exec(){
String command = jTextAreaTerminal.getText().trim();
if (command == null || command.equals("")){
JOptionPane.showMessageDialog(this, "O comando não pode ser vazio");
}else{
Map<String,String> result = TerminalEmulator.exec(command);
if (result.get("error") != null && !result.get("error").equals("")){
writeOutput(result.get("error"), true);
}else if (result.get("input") != null && !result.get("input").equals("")){
writeOutput(result.get("input"), false);
}
}
}
private void clearOutput(){
jTextAreaOutput.setText("");
}
private void paintOutput(boolean error){
if (error){
jTextAreaOutput.setForeground(Color.RED);
}else{
jTextAreaOutput.setForeground(Color.BLACK);
}
}
private void writeOutput(String text, boolean error){
clearOutput();
paintOutput(error);
jTextAreaOutput.setText(text);
}
/**
* @param args the command line arguments
*/
public static void main(String args[]) {
/* Set the Nimbus look and feel */
//<editor-fold defaultstate="collapsed" desc=" Look and feel setting code (optional) ">
/* If Nimbus (introduced in Java SE 6) is not available, stay with the default look and feel.
* For details see http://download.oracle.com/javase/tutorial/uiswing/lookandfeel/plaf.html
*/
try {
for (javax.swing.UIManager.LookAndFeelInfo info :
javax.swing.UIManager.getInstalledLookAndFeels()) {
if ("Nimbus".equals(info.getName())) {
javax.swing.UIManager.setLookAndFeel(info.getClassName());
break;
}
}
} catch (ClassNotFoundException ex) {
java.util.logging.Logger.getLogger(FTerminal.class.getName()).log(
java.util.logging.Level.SEVERE, null, ex);
} catch (InstantiationException ex) {
java.util.logging.Logger.getLogger(FTerminal.class.getName()).log(
java.util.logging.Level.SEVERE, null, ex);
} catch (IllegalAccessException ex) {
java.util.logging.Logger.getLogger(FTerminal.class.getName()).log(
java.util.logging.Level.SEVERE, null, ex);
} catch (javax.swing.UnsupportedLookAndFeelException ex) {
java.util.logging.Logger.getLogger(FTerminal.class.getName()).log(
java.util.logging.Level.SEVERE, null, ex);
}
//</editor-fold>
/* Create and display the form */
java.awt.EventQueue.invokeLater(new Runnable() {
public void run() {
new FTerminal().setVisible(true);
}
});
}
// Variables declaration - do not modify//GEN-BEGIN:variables
private javax.swing.JButton jButtonExec;
private javax.swing.JButton jButtonHistory;
private javax.swing.JScrollPane jScrollPane1;
private javax.swing.JScrollPane jScrollPane2;
private javax.swing.JTextArea jTextAreaOutput;
private javax.swing.JTextArea jTextAreaTerminal;
// End of variables declaration//GEN-END:variables
}
Se você estiver usando Netbeans para criação de formulários perceberá que alguns métodos são padrões e não perderemos muito tempo neles:
- public Fterminal(): Construtor padrão do formulário;
- initComponents(): Inicializa os componentes do formulário;
- main(): Método que possibilita a execução do formulário como sendo o formulário principal do projeto.
Vejamos agora os métodos importantes a serem explicados:
- clearOutput(): Apenas limpa o conteúdo que de saída do jTextAreaOutput, que pode ser um ERRO retornado ou um conteúdo válido;
- paintOutptu(boolean error): Pinta o jTextAreaOutput de acordo com a condição error, sendo que, ao ocorrer um erro a mensagem será pintada de VERMELHO, caso contrário PRETA;
- writeOutput(String text, boolean error): Ao executar um comando no TerminalEmulator a saída deste comando é escrita no jTextAreaOutput através deste método;
- exec(): Captura o texto digitado no jTextAreaTerminal e chama a sua execução para o TerminalEmulator. Caso a chave “error” não tenha nenhum conteúdo, então ele chama o writeOutput indicando que não há erros e passa o valor contido na chave “input”.
- jTextAreaTerminalKeyPressed: Verifica se foi pressionada a tecla F5 e chama o método exec(), possibilitando sua execução rápida;
- jButtonExecActionPerformed: Chama diretamente o método exec() quando o botão “Executar” for clicado;
- jButtonHistoryActionPerformed: Este método está ligado ao botão “Histórico” e é executado no seu click. A sua função é chamar a classe Dhistory (que será mostrada mais a frente) e capturar o comando que foi selecionado neste formulário. Caso algum comando tenha sido selecionado, então este é setado no jTextAreaTerminal, possibilitando que o usuário possa re-executá-lo.
Dhistory
O objetivo desta classe, que trata-se de um Jdialog, é mostrar o histórico de comandos já executados, possibilitar a limpeza destes ou a sua execução, conforme a Figura 2.
O formulário Dhistory possui um Jlist que contém a lista de comandos já executados, além de um botão Confirmar, que seleciona um comando e o envia para o Fterminal afim de ser executado. O botão “Limpar” faz chamada ao clearHistory() do TerminalEmulator afim de limpar o histórico de comandos executados.
Vejamos a classe Dhistory.java por completo na Listagem 3.
package br.com.lab;
import javax.swing.DefaultListModel;
public class DHistory extends javax.swing.JDialog {
/**
*
*/
private static final long serialVersionUID = 1L;
private DefaultListModel lista = new DefaultListModel();
/**
* Creates new form DHistory
*/
public DHistory(javax.swing.JFrame parent, boolean modal) {
super(parent, modal);
initComponents();
loadHistory();
setVisible(true);
}
private void loadHistory(){
jListHistory.setModel(lista);
for(String hi : TerminalEmulator.getCommandHistory()){
lista.addElement(hi);
}
}
/**
* This method is called from within the constructor to initialize the form.
* WARNING: Do NOT modify this code. The content of this method is always
* regenerated by the Form Editor.
*/
@SuppressWarnings("unchecked")
// <editor-fold defaultstate="collapsed" desc="Generated Code">//GEN-BEGIN:initComponents
private void initComponents() {
jScrollPane1 = new javax.swing.JScrollPane();
jListHistory = new javax.swing.JList();
jButtonConfirm = new javax.swing.JButton();
jButtonClear = new javax.swing.JButton();
setDefaultCloseOperation(javax.swing.WindowConstants.DISPOSE_ON_CLOSE);
jScrollPane1.setViewportView(jListHistory);
jButtonConfirm.setText("Confirmar");
jButtonConfirm.addActionListener(new java.awt.event.ActionListener() {
public void actionPerformed(java.awt.event.ActionEvent evt) {
jButtonConfirmActionPerformed(evt);
}
});
jButtonClear.setText("Limpar");
jButtonClear.addActionListener(new java.awt.event.ActionListener() {
public void actionPerformed(java.awt.event.ActionEvent evt) {
jButtonClearActionPerformed(evt);
}
});
javax.swing.GroupLayout layout = new javax.swing.
GroupLayout(getContentPane());
getContentPane().setLayout(layout);
layout.setHorizontalGroup(
layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
.addGroup(javax.swing.GroupLayout.Alignment.TRAILING,
layout.createSequentialGroup()
.addContainerGap()
.addComponent(jScrollPane1, javax.swing.GroupLayout.
DEFAULT_SIZE, 376, Short.MAX_VALUE)
.addContainerGap())
.addGroup(layout.createSequentialGroup()
.addGap(107, 107, 107)
.addComponent(jButtonConfirm)
.addGap(18, 18, 18)
.addComponent(jButtonClear, javax.swing.GroupLayout.
PREFERRED_SIZE, 79, javax.swing.GroupLayout.PREFERRED_SIZE)
.addContainerGap(javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE))
);
layout.setVerticalGroup(
layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
.addGroup(layout.createSequentialGroup()
.addContainerGap()
.addComponent(jScrollPane1, javax.swing.GroupLayout.
PREFERRED_SIZE, 230, javax.swing.GroupLayout.PREFERRED_SIZE)
.addGap(18, 18, 18)
.addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE)
.addComponent(jButtonConfirm)
.addComponent(jButtonClear))
.addContainerGap(javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE))
);
pack();
}// </editor-fold>//GEN-END:initComponents
private void jButtonConfirmActionPerformed(java.awt.event.ActionEvent evt)
{//GEN-FIRST:event_jButtonConfirmActionPerformed
dispose();
}//GEN-LAST:event_jButtonConfirmActionPerformed
private void jButtonClearActionPerformed(java.awt.event.ActionEvent evt)
//GEN-FIRST:event_jButtonClearActionPerformed
TerminalEmulator.clearHistory();
jListHistory.removeAll();
dispose();
}//GEN-LAST:event_jButtonClearActionPerformed
public String getSelectedCommand(){
if (jListHistory.getSelectedValue() != null){
return jListHistory.getSelectedValue().toString();
}else{
return null;
}
}
/**
* @param args the command line arguments
*/
public static void main(String args[]) {
/* Set the Nimbus look and feel */
//<editor-fold defaultstate="collapsed" desc=" Look and feel
setting code (optional) ">
/* If Nimbus (introduced in Java SE 6) is not available, stay with
the default look and feel.
* For details see http://download.oracle.com/javase/tutorial/uiswing/
lookandfeel/plaf.html
*/
try {
for (javax.swing.UIManager.LookAndFeelInfo info :
javax.swing.UIManager.getInstalledLookAndFeels()) {
if ("Nimbus".equals(info.getName())) {
javax.swing.UIManager.setLookAndFeel(info.getClassName());
break;
}
}
} catch (ClassNotFoundException ex) {
java.util.logging.Logger.getLogger(DHistory.class.getName())
.log(java.util.logging.Level.SEVERE, null, ex);
} catch (InstantiationException ex) {
java.util.logging.Logger.getLogger(DHistory.class.getName())
.log(java.util.logging.Level.SEVERE, null, ex);
} catch (IllegalAccessException ex) {
java.util.logging.Logger.getLogger(DHistory.class.getName())
.log(java.util.logging.Level.SEVERE, null, ex);
} catch (javax.swing.UnsupportedLookAndFeelException ex) {
java.util.logging.Logger.getLogger(DHistory.class.getName())
.log(java.util.logging.Level.SEVERE, null, ex);
}
//</editor-fold>
/* Create and display the dialog */
java.awt.EventQueue.invokeLater(new Runnable() {
public void run() {
DHistory dialog = new DHistory(new javax.swing.JFrame(), true);
dialog.addWindowListener(new java.awt.event.WindowAdapter() {
@Override
public void windowClosing(java.awt.event.WindowEvent e) {
System.exit(0);
}
});
dialog.setVisible(true);
}
});
}
// Variables declaration - do not modify//GEN-BEGIN:variables
private javax.swing.JButton jButtonClear;
private javax.swing.JButton jButtonConfirm;
private javax.swing.JList jListHistory;
private javax.swing.JScrollPane jScrollPane1;
// End of variables declaration//GEN-END:variables
}
Logo no início temos a definição de um DefaultListModel que será usado no Jlist, afim de realizar as operações de inserção e remoção de elementos deste componente. Logo após, no construtor da classe, temos o uso de um método chamado loadHistory() que fará o carregamento de todos os históricos no componente jListHistory e a configuração do DefaultListModel:
private void loadHistory(){
jListHistory.setModel(lista);
for(String hi : TerminalEmulator.getCommandHistory()){
lista.addElement(hi);
}
}
Perceba que é feita uma iteração nos comandos do TerminalEmulator para adicionar um por um no DefaultListModel.
O botão “Confirmar”, que está ligado ao método jButtonConfirmActionPerformed, só tem a função de fechar o formulário, pois o que nos interessa é apenas selecionar um comando para posteriormente capturá-lo de outros formulários. Fazemos isso através do método getSelectedCommand() que retorna o comando selecionado caso ele exista.
Por fim, temos o botão “Limpar” que está ligado ao método jButtonClearActionPerformed(), que por usa vez faz chamada ao método clearHistory() do TerminalEmulator limpando o histórico e os valores do JlistHistory e, finalmente faz o dispose() do formulário, afinal não há porque o usuário continuar naquele formulário se não há mais comandos a serem selecionados.
Lembre-se que após selecionar o comando e clicar em Confirmar o formulário é fechado, voltando a execução do programa para o Fterminal que irá, por sua vez, usar o getSelectedCommand() para inserir o valor selecionado no jTextAreaTerminal.
Vimos neste artigo como criar um emulador de terminal Linux em Java, usando a classe Runtime e diversos outros recursos do Java. É importante salientar que melhorias podem ser realizadas neste projeto, pois trata-se apenas de um protótipo para fins didáticos. Você pode ir muito além, adicionando o histórico de comandos ao banco de dados, hora de execução, quem executou, valor retornado e etc.