Projeto: Java Shell

shell

Datas de entrega: 18/10/2013

Forma de entrega: Todos os alunos devem enviar via e-mail um arquivo .zip com todo o código fonte do programa e uma breve descrição sobre seu funcionamento.

Grupos: Este projeto deverá ser realizado individualmente.

O objetivo deste projeto é refrescar a sua habilidade em programação Java e introduzi-lo, quando necessário, a utilização de threads e processos. Sua tarefa será implementar um interpretador de comandos (shell) que funcionará de forma similar (mas não identica) a um shell UNIX.

Quando você digitar um comando o seu shell deve criar um thread para executar o comando fornecido. Múltiplos comandos podem ser fornecidos em uma única linha de entrada, separados por um  ‘&’. Nesse caso, seu shell deverá executar todos os comandos concorrentemente e só solicitar um novo comando do usuário quando todos os comandos tiverem sido concluídos.

Você não precisa implementar pipes ou redirecionamento de entrada ou saída padrão. Além disso, um ‘&’no final do comando não terá nenhum significado especial (não significa que o comando deva ser executado em “background” , como ocorre no UNIX).

Seu programa deve ser capaz de lidar com um número arbitrário de comandos por linha, cada um com um número arbitrário de argumentos separados por um número também arbitrário de espaços em branco.  O sistema deve ser capaz de se recuperar de erros como a digitação de comandos desconhecidos imprimindo uma mensagem de erro e continuando a execução.

Sugestões

Este projeto não é tão difícil quanto possa parecer na primeira leitura pois a maioria das partes complicadas já está pronta e disponível na biblioteca padrão de Java. O trabalho será basicamente encontrar as biblioteca relevantes e invocar os métodos corretamente. Seu programa final terá provavelmente algo em torno de 200 linhas, incluindo os comentários.

Não esqueça, este projeto deve servir principalmente para sua familiarização com a utilização de Threads em java. Ele valerá apenas 5% da sua média final no curso.
O método public static void main() da sua classe principal será bastante simples. Ele terá um loop infinito que imprime um prompt, lê uma linha, faz o parsing da linha (quebra em vários comandos), cria um novo thread para cada um dos diferentes comandos e em seguida aguarda pela finalização da execução dos comandos para imprimir o próximo prompt para o usuário.

Parsing

O stream System.in, responsável por capturar a entrada vinda do teclado, é do tipo InputStream, portanto ele pode fornecer tanto bytes isoladamente quanto arrays de bytes. Você poderia representar a linha de entrada como um array de bytes mas verá que é muito mais simples utilizar uma String ao invés disso. Você pode querer dar uma olhada na classe BufferedReader para descobrir como ler uma linha em uma String. O processo de quebrar a linha em comandos separadas por ‘&’ é facilmente implementado com os métodos indexOf e substring da classe String ou utilizando um StringTokenizer(str, delim). Quebrar os comandos individuais em palavras é praticamente trivial utilizando um StringTokenizer.

Comandos

Uma vez que você conseguiu quebrar o comando em palavras é simples fazer o sistema executa-lo utilizando as classes Runtime e Process. Utilize o código Runtime r = Runtime.getRuntime() para obter uma referência para um objeto do tipo Runtime e invoque o método Process p = r.exec(argv) para executar um comando. Aqui, argv é um array de Strings contendo as palavras do comando (o nome do comando está em argv[0]) e o p resultante é uma referência para um objetivo do tipo Process.

Há, no entanto, uma pegadinha. A saída produzida pelos comandos executados irá desaparecer em um buraco negro a menos que você explicitamente a capture e exiba para o usuário do seu shell. Para fazer isso, você deve utilizar o método p.getInputStream() onde p é uma referência para o objeto Process retornado pelo comando Runtime.exec(String []). Ao ler do stream resultante você tem acesso a saída padrão do processo filho, criado para executar o comando. Alguns comandos enviam parte de sua saída para um stream alternativo chamado “saída padrão de erro”. Por exemplo, o comando do Linux cat foo imprime o conteúdo do arquivo foo na saída padrão, porém, se o arquivo foo não existe o comando cat imprime uma mensagem de erro na sua saída padrão de erro. Você pode capturar também a saída padrão de erro de um processo utilizando o método getErrorStream(). 

Você pode também enviar comandos para a entrada padrão de um processo utilizando o getOutputStream() mas isso não é necessário para este projeto. Seu shell não precisa ser capaz de executar comandos que precisam ler dados de sua entrada padrão.

Um processo pode produzir dados tanto na saída padrão quanto na saída de erro padrão e essa saída pode ser produzida em qualquer ordem e ser intercalada. Seu shell deve então utilizar dois threads por comando para garantir que saída será apresentada para o usuário obedecendo esta mesma ordem.

O comando exit deve  fazer seu shell ser finalizado imediatamente. Esse comando não funcionará se for executado utilizando o Runtime.exec(String[]) (por que não?) . Para este comando em particular você deverá utilizar o método System.exit(). Assim como ocorre com qualquer execução concorrente, se o comando exit for executado concorrentemente com outros comandos, a ordem exatos em que os eventos ocorrerão é imprevisível. Por exemplo, no comando

cat foo & exit

seu shell pode terminar antes de exibir o conteúdo do arquivo foo ou mesmo na metade da exibição do arquivo.

Utilizando Threads

Sua classe principal lerá uma linha de comando do usuário e criará threads para fazer a execução dos comandos fornecidos. Em seguida ela esperará até que todos os threads tenham concluído sua execução antes de solicitar uma nova linha de comandos do usuário. Para este fim, você precisará de uma classe Command que implementa a interface Runnable e que permitirá que você crie Threads como no seguinte trecho de código:

Thread t = new Thread(new Command(/* algum comando */));
t.start();
/* mais tarde ... */
try {
   t.join();
} catch (InterruptedException e) {
   e.printStackTrace():
}

Para exibir tanto a saída padrão quanto a saída de erro padrão na ordem em que os dados são produzidos você precisará de pelo menos dois threads por comando. Porém, você verá que é mais simples fazer isso utilizando três threads: um para a saída padrão, um para a saída de erro padrão e um thread mestre para criar os outros dois e esperar que eles sejam finalizados. Se você achar tudo isso muito confuso, deixe de lado a saída de erro padrão e imprima inicialmente apenas a saída padrão, criando um único thread por comando. Quando você tiver essa versão inicial funcionando corretamente você provavelmente achará bem mais simples modificar o programa para lidar corretamente com a saída de erro padrão.

Exceções

Java exige que você coloque em um bloco try – catch qualquer método que possa  causar uma exceção na execução do programa. O seu shell deve lidar com exceções que possam ocorrer na execução dos comandos fornecidos pelo usuário. Por exemplo,  pode ocorrer uma exceção caso um comando tente abrir um arquivo que não existe. Neste caso, seu shell deve reportar tal erro para o usuário e continuar normalmente com sua execução. Exceções mais sérias, no entanto, podem exigir que o shell seja finalizado após imprimir uma mensagem de erro para o usuário.

Critérios de Avaliação

Para este projeto, a avaliação será baseada na corretude do programa (50%), estilo de programação (40%) e testes (10%). Os próximos projetos darão menos ênfase ao estilo, mas queremos que vocês comecem com o pé direito e forçar a quebra que qualquer mal hábito que possa trazer do passado. Em particular:

  • Cada declaração de classe, método ou atributo deve ser precedida por um comentário que descreve o seu propósito. Comentários devem ser utilizados livremente para explicar qualquer coisa que não seja óbvia para um programador Java experiente que é familiar com a especificação do projeto (este documento). Comentários utilizando Javadoc são recomendados mas não exigidos.
  • Identificadores devem ser nomeados de acordo com as convenções da linguagem: CAIXA_ALTA com underscores para nomes de constantes, IniciaisEmMaiusculo para nomes de classes e iniciaisAPartirDaSegundaEmMaiusculo para outros identificadores
  • A indentação deve ser consistente e clara. Use um caractere tab ou quatro espaços para cada nível de indentação. Não misture tabs e espaços. Use sempre um ou outro.
  • Cada linha de código não deve ter mais do que 80 caracteres, incluindo indentação.

Esses são apenas alguns dos aspectos a serem considerados mas você deve garantir que seu código será legível para um programador Java experimente que está acostumado com as convenções padrão da linguagem e não com o seu estilo pessoal favorito.

Utilize dados de testes adequados. Teste situações normais e também condições de contorno (comandos vazios, comandos muito longos, etc). Em alguns casos, testar insuficientemente o seu programa pode lhe custar, além dos pontos referentes aos testes propriamente ditos, pontos em relação a corretude do programa.

Você pode testar o seu programa em qualquer computador mas tenha certeza de que ele funcionará em uma máquina com uma distribuição padrão Linux. Se você precisar o professor pode disponibilizar uma conta para acesso a uma máquina Linux para realização dos testes.