Emulador de Terminal Linux com a classe Java Runtime

Veja neste artigo como usar a classe Runtime para criar um emulador de terminal linux.

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:

Nosso projeto consistirá de três Classes, que são:

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; } }
Listagem 1. TerminalEmulator.java

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.

Figura 1. Fterminal

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 }
Listagem 2. Fterminal.java

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:

Vejamos agora os métodos importantes a serem explicados:

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.

Figura 2. Dhistory

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 }
Listagem 3. Dhistory.java

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.

Aprenda ainda mais

Artigos relacionados