Sign nginx website and dovecot imap server on debian with let’s encrypt

If you have a setup with a single server with multiple services (web, IMAP etc.), and one CNAME per service (www.somedomain.com, imap.somedomain.com), and you would like to get the services signed in a manner that doesn’t give warnings or errors in browsers  (especially browsers in phones and tablets with iOS and Android), then this article may be of interest.

Self-signed certificates is a nuisance and the cacert.org initiative has been losing support. Let’s encrypt offers the possibility of having free (as in both cost and feedom) SSL certificates that don’t give warnings in web browsers. The only problem the threshold of taking the time to figure out how to use it.

It turned out there wasn’t much figuring to do: on a debian jessie GNU/linux system, the certbot program from eff.org takes care of everything, including keeping the certificates automatically updated (the .deb package for certbot sets up a cronjob that does the right thing).

The way certbot works is that it requires that each server you wish to sign must be accessible on http (port 80) and the local path “/.well-known/” on each server must be accessible and map to a file area that certbot can put files in.

The certbot program works by contacting let’s encrypt saying that it wants a certificate for a DNS name,  and let’s encrypt will then access the HTTP URL to verify that certbot is indeed running on a server that can be found using that DNS name.

This means that, for certbot to work:

  1. Even if your HTTP server responds only on HTTPS and/or requires authentication, you will need to make a plain HTTP connection available and have the local path “/.well-known/” map to a part of the file system, and be available without authentication
  2. Even if you’re making a certificate for a non-HTTP service (e.g. an IMAP server), you will need to make a plain http (port 80) server responding to that DNS CNAME that can serve the local parth “/.well-known/” from the local

This article explains how to set up free SSL certificates signed with let’s encrypt on an nginx web server and a dovecot IMAP server, on a debian jessie GNU/linux system.

The certbot instructions takes you part of the way, but it has some holes and not a lot of explanation, which is why I wrote this article.

The steps are:

  1. Add jessie-backports to APT (click the link and follow the instructions)
  2. Install certbot from jessie-backports:
    1. Open a command shell as root and give the following command:
      apt-get install certbot -t jessie-backports
      
  3. Disable the default nginx site
    1. Edit the /etc/nginx/sites-available/default file to have the following content:
      server {
              listen 80 default_server;
              listen [::]:80 default_server;
      
              root /var/www/html;
      
              server_name _;
      
              location / {
                      deny all;
              }
      }
      
    2. Run the following command in the command shell openes as root
      systemctl reload nginx
      
  4. Add DNS CNAME-records for the virtual hosts you are going to sign.
    In the examples used in this procedure, the host is hostname.somedomain.com and it has two CNAME aliases: http://www.somedomain.com and imap.somedomain.com.
  5. Add a http://www.mydomain.com nginx site
    1. Create a file /etc/nginx/available-sites/www with the following contents:
      server {
              listen 80;
              listen [::]:80;
      
              server_name www.mydomain.com;
      
              root /var/www/html;
      
              index index.html index.htm index.nginx-debian.html;
      
              location / {
                      allow all;
              }
      }
      
    2. Give the following commands in the command shell opened as root:
      cd /etc/nginx/enabled-sites/
      ln -s /etc/nginx/available-sites/www .
      systemctl reload nginx
      
  6. Add an imap.mydomain.com nginx site
    Note! This isn’t a real website but it is necessary to give HTTP access to a web server listening to this CNAME alias so that the certbot program can create and auto-update the certificate that dovecot uses.

    1. Create a file /etc/nginx/available-sites/imap with the following contents:
      # The port 80 listener only gives access to certbot
      server {
              listen 80;
              listen [::]:80;
      
              server_name imap.bang.priv.no;
      
              root /var/www-imap/;
      
              location /.well-known/ {
                      allow all;
              }
      
              location / {
                      deny all;
              }
      }
      
    2. Give the following commands in the command shell opened as root:
      cd /etc/nginx/enabled-sites/
      ln -s /etc/nginx/available-sites/imap .
      systemctl reload nginx
      
  7. Add a certificate for http://www.mydomain.com
    1. Give the following command in the command shell opened as root:
      certbot certonly --webroot -w /var/www/html -d www.mydomain.com
      
  8. Configure certificates for the http://www.mydomain.com nginx web site
    1. Change the /etc/nginx/available-sites/www file to the following:
      server {
              listen 80;
              listen [::]:80;
      
              server_name www.mydomain.com;
      
              # SSL configuration
              #
              listen 443 ssl default_server;
              listen [::]:443 ssl default_server;
              ssl_certificate     /etc/letsencrypt/live/www.mydomain.com/fullchain.pem;
              ssl_certificate_key /etc/letsencrypt/live/www.mydomain.com/privkey.pem;
      
              root /var/www/html;
      
              location / {
                      allow all;
              }
      }
      
    2. Give the following command in the command shell opened as root
      certbot certonly --webroot -w /var/www-imap -d imap.mydomain.com
      
    3. Open the https://www.somedomain.com server (replace with your actual URL) and observe that the browser reports it as secure with a valid certificate
  9. Add a certificate for imap.mydomain.com
    1. Give the following command in the command shell opened as root:
      certbot certonly --webroot -w /var/www-imap -d imap.mydomain.com
      
  10. Configure dovecot to use the imap.mydomain.com certificate
    1. Change/modify the following lines in the /etc/dovecot/conf.d/10-ssl.conf file:
      # SSL/TLS support: yes, no, required. 
      ssl = yes
      
      # PEM encoded X.509 SSL/TLS certificate and private key. They're opened before
      # dropping root privileges, so keep the key file unreadable by anyone but
      # root. Included doc/mkcert.sh can be used to easily generate self-signed
      # certificate, just make sure to update the domains in dovecot-openssl.cnf
      ssl_cert = </etc/letsencrypt/live/imap.bang.priv.no/fullchain.pem
      ssl_key = </etc/letsencrypt/live/imap.bang.priv.no/privkey.pem
      
    2. Give the following command in the command shell opened as root:
      /etc/init.d/dovecot reload
      

The certificates have a 90 day lifetime, but as mentioned earlier, the certificates will be automatically updated by certbot when they have 30 days valid time remaining. The certbot deb package installs a cronjob that runs twice every day at random second in the hour following 00:00 and 12:00 and checks if certificates needs to be updated and updates the ones that are ready for updating.

Making a Java windows service in 10 minutes

This blog post describes how to create a windows service from a Java application, it is a slightly more fleshed out version of the JavaZone 2016 lightning talk “A Java windows service in 10 minutes”.

A problem sometimes encountered by a Java programmer, is to make your Java program into a Windows Service. This is may be a bump in your project, particularly if you don’t know anything about windows services, or much about windows for that matter.

The demo created a running, working, Windows service server using 14 lines of Java code, and some maven configuration.

Before starting on the demo, a few words on what windows services are (from a GNU/linux/UNIX perspective):

  • Windows services are the “daemons” of the windows world
  • Windows services are normally started when the windows system starts, and stopped when the windows system shuts down
  • Windows services can be stopped and started by administrator users, both using a GUI and using command line commands
  • Windows services can be configured to run with a particular user, restricting what the service can do (default is the local user “Local System”)

To create the installer the demo use a maven plugin called maven-windows-service-installer-plugin. The maven plugin in turn relies on izpack for the installer and uses the apache commons daemon to execute the Java program.

The Java program turned into a windows service during the demo, is the Wiser test SMTP server. Wiser was picked, because:

  1. It has an appropriate API
  2. An SMTP service is easy to demonstrate, and it is something other than yet another HTTP service

Since the demo might be hard to follow (a lot of information in 10 minutes), this blog post describes all steps of the demo (note: the complete code can be found on github at https://github.com/sbang/ansmtpserver ).

Required prior knowledge:

  • Java programming
  • Apache maven

Required software to retrace the demo:

  • Apache maven (any maven 2 or 3 will do)
  • A Java SDK (I’m using the newest Java 1.8, but any Java SDK 1.7 will probably do)
  • An eclipse IDE (I’m using Eclipse Neon, but any recent eclipse will probably do)
  • A telnet command line application (since this is for windows, just use Windows cygwin bash with the inetutils package, just run the installer and include inetutils)

To retrace the demo, do the following operations:

  1. Start eclipse and open the Workspace “C:\workspace”
  2. Right click the package explorer and select New->Other…
  3. In the “New” dialog box:
    1. Select Maven->Maven Project
    2. Click the “Next>” button
    3. Checkmark the checkbox “Create a simple project (skip archetype selection)” at the to of the dialogbox
    4. Click the “Next>” button
    5. In the “Group id” text box, type
      ansmtpserver
    6. In the “Artifact id” text box, type
      ansmtpserver
    7. Click the “Finish” button
  4. Open the “ansmtpserver” project and double click “pom.xml” to open it
  5. In the pom.xml editor (title “ansmtpserver/pom.xml”):
    1. Select the Dependencies tab
    2. Click the “Add…” button
    3. In the “Select Dependency” dialog box:
      1. In the field “Enter groupId, artifactId or sha1 prefix or pattern (*)”, type
        windows-installer
      2. Select “com.alexkasko.installer windows-service-installer-common”
      3. Click the “OK” button
    4. Click the “Add…” button
    5. In the “Select Dependency” dialog box:
      1. In the field “Enter groupId, artifactId or sha1 prefix or pattern (*)”, type
        subethamail
      2. Select “org.subethamail subethasmtp”
      3. Click the “OK” button
    6. Click the “Add…” button
    7. In the “Select Dependency” dialog box:
      1. In the field “Enter groupId, artifactId or sha1 prefix or pattern (*)”, type
        slf4j-simple
      2. Select “org.slf4j slf4j-simple”
      3. Click the “OK” button
    8. Save the pom.xml file
  6. Right-click ansmtpserver->src/main/java in the “Package Explorer” and select New->Package
  7. In the “New Java Package” dialog box:
    1. Let the “Name” field have its default (“ansmtpserver”)
    2. Click the “Finish” button
  8. Right-click the ansmtpserver->src/java/main->ansmtpserver package in the “Package Explorer” and select New->Class
  9. In the “New Java Class Dialog”
    1. In the “Name” field, type
      AnSmtpServer
    2. In “Interfaces”, click the “Add…” button
    3. In the “Implemented Interfaces Selection” dialog box:
      1. In “Choose interfaces”, type
        dae
      2. In “Matching items”, select “DaemonLauncher – com.alexkasko.installer”
      3. Click the “OK” button
    4. Click the “Finish” button
  10. Modify the generated AnSmtpServer.java file in the following way
    package ansmtpserver;
    
    import org.subethamail.wiser.Wiser;
    
    import com.alexkasko.installer.DaemonLauncher;
    
    public class AnSmtpServer implements DaemonLauncher {
    
    	private Wiser server;
    
    	public AnSmtpServer() {
    		super();
    		server = new Wiser();
    		server.setHostname("javazone");
    		server.setPort(2200);
    	}
    
    	public void startDaemon() {
    		server.start();
    	}
    
    	public void stopDaemon() {
    		server.stop();
    	}
    
    }
    1. Add a Wiser field
    2. In the constructor, create an Wiser instance, set the host name, and the port number
    3. In the startDaemon() method start the Wiser server
    4. In the stopDaemon() method stop the Wiser server
  11. Save the modified AnSmtpServer.java file
  12. Right-click ansmtpserver->src/main/resources in the “Package Explorer” and select New->File
  13. In the “New File” dialog box
    1. In “File name”, type
      simplelogger.properties
    2. Click the “Finish” button
  14. Modify the “simplelogger.properties” file to have the following content
    org.slf4j.simpleLogger.defaultLogLevel=debug

    and save the file

  15. Select the “ansmtpserver/pom.xml” editor, and select the “pom.xml” tab, and paste the following before the </project> end tag. This configuration will be the same for all installers with the exception of the <prunsrvDaemonLauncherClass> tag
     <build>
      <plugins>
       <plugin>
        <groupId>com.alexkasko.installer</groupId>
        <artifactId>maven-windows-service-installer-plugin</artifactId>
        <version>1.0.6</version>
        <dependencies>
         <dependency>
          <groupId>com.alexkasko.installer</groupId>
          <artifactId>windows-service-installer-common</artifactId>
          <version>1.0.6</version>
         </dependency>
        </dependencies>
        <configuration>
         <prunsrvDaemonLauncherClass></prunsrvDaemonLauncherClass>
         <use64BitJre>true</use64BitJre>
        </configuration>
        <executions>
         <execution>
          <id>build-installer</id>
          <phase>package</phase>
          <goals>
           <goal>installer</goal>
          </goals>
         </execution>
        </executions>
       </plugin>
      </plugins>
     </build>
  16. Open ansmtpserver->src/main/java->ansmtpserver->AnSmtpServer.java in the “Package Explorer”, right-click the “AnSmtpServer” class, and select “Copy Qualified Name” and paste the name into the <prunsrvDaemonLauncherClass> element
  17. Save the pom.xml file
  18. Open a cmd.exe window, and type the following commands to build the installer
    cd c:\windows\ansmtpserver
    mvn clean install
  19. Open a windows explorer on C:\Windows\ansmtpserver\target
  20. Right click the ansmtpserver-0.0.1-SNAPSHOT-installer.zip file and select “Extract all…” to the folder “C:\workspace\ansmtpserver\target”
  21. Open the folder “C:\workspace\ansmtpserver\target\ansmtpserver-0.0.1-SNAPSHOT-installer”, right-click the “install.exe” file and select “Run as administrator”
  22. Open a “Cygwin 64 terminal” window and type the following command
    telnet localhost 2022

    The expected response is

    Trying 127.0.0.1...
    telnet: Unable to connect to remote host: Connection refused

    since nothing is listening to port 2200

  23. Click the installer all the way to the end, using defaults for everything
  24. Open the windows services window and there will be a new windows service “ansmtpservice” shown as “Running”Windows services with the ansmtpserver shown
  25. Try the “telnet localhost 2200” command again, and this time there will be a response, and it will be possible to talk SMTP over the connectiontelnet_session
  26. Stop the “ansmtpservice” service and the telnet connection will be disconnected

Thus ends the installer part.

Some simple improvements to this installer are possible:

  • Better descrption for the service in “Windows Services”
    • Just add the following to the <configuration> setting of the maven-windows-service-installer-plugin:
      <prunsrvServiceName>AnSmtpServer</prunsrvServiceName>
      <prunsrvDisplayName>An SMTP server</prunsrvDisplayName>
      <prunsrvDescription>This service responds to incoming STMP connections on port 2200.</prunsrvDescription>
  • Install the service under “C:\Programs and Files”
    • Just add the following to the <configuration> setting of the maven-windows-service-installer-plugin:
      <izpackDefaultInstallDir>$APPLICATIONS_DEFAULT_ROOT\ansmtpserver</izpackDefaultInstallDir>
  • Attach the zip file containing the installer to the maven artifact, so that the installer can be deployed to a maven repository, where other maven files can download and unpack the installer from (easy distribution)
    • Add the following inside <build><plugins></plugins></build> of the pom.xml build
         <plugin>
          <groupId>org.codehaus.mojo</groupId>
          <artifactId>build-helper-maven-plugin</artifactId>
          <version>1.10</version>
          <executions>
           <execution>
            <id>attach-artifacts</id>
            <phase>package</phase>
            <goals>
             <goal>attach-artifact</goal>
            </goals>
            <configuration>
             <artifacts>
              <artifact>
               <file>target/${project.artifactId}-${project.version}-installer.zip</file>
               <type>zip</type>
               <classifier>installer</classifier>
              </artifact>
             </artifacts>
            </configuration>
           </execution>
          </executions>
         </plugin>
        </plugins>
      

A windows-service-installer that contains the above improvements and more, is this installer for Apache Jena Fuseki.

Get update notifications in the MATE desktop on debian jessie

One thing I have been missing since Gnome 2 was suceeded by the (IMO) horrible Gnome 3, is a tool tray notification icon for pending debian updates.

When someone continued Gnome 2 as MATE and MATE became available on debian, there was no notification tooltray icon to be found.

But now there is such a tooltray icon: pk-update-icon and since debian with MATE again is my primary desktop this I was something I was happy to discover.

When there are packages available, the icon looks like this:software-updates-mate-jessie

Then you can either click on “Install updates” and use the GUI to inspect, and install the updates, or you can pop to a root terminal window do:

apt-get dist-upgrade

To install the pk-update-icon:

  1. the apt-line for “jessie-backports”First ensure that you have the apt-line for “jessie-backports”, by adding the following line to /etc/apt/sources.list :
    deb http://http.debian.net/debian jessie-backports main contrib non-free
  2. Update APT to get the index files for “jessie-backports”, do the following command in a root command line window:
    apt-get dist-upgrade
  3. Install pk-update-icon from jessie backports, give the following command in a root command line window:
    apt-get install -t jessie-backports pk-update-icon

    Answer yes to the question about if the install should proceed

  4. Log out of the desktop and log back in, and the next time updates arrive the icon will show up, like in the screen shot above

Logging to persistent tmpfs on Raspbian “jessie”

At the end of Using a Raspberry Pi 2 Model B as a router/firewall for the home LAN I wrote that I decided not to put /var/log into tmpfs, because:

  1. I wanted the logs to be persistent
  2. I thought that the wear would result in less and less of the sd card to become available (and 16GB for logs should last a loong time)

As it turned out the sd card died after one month.

I don’t know if the cause was excessive logging, the use of ntopng (which did write quite a lot, both in the number of files, the number of files, and in the total storage used, which was approximately 0,5GB after 30 days of uptime) or simply a bad sd card.

However, going forward with a new sd card, I’ve done the following:

  1. Removed ntopng
  2. Put /var/log on tmpfs (limited to 100MB in size), synced to a backing store on the sd card using rsync

For setting up the logging I found some existing web pages that took me part of the way, but not all the way:

Here is what I did:

  1. Logged in as root and did everything below as root
  2. Edited /etc/fstab and added the following line:
    tmpfs    /var/log    tmpfs    defaults,noatime,nosuid,mode=0755,size=100m    0 0
  3. Created an /etc/init.d/ramdiskvarlog file with the following contents
    #!/bin/sh
    ### BEGIN INIT INFO
    # Provides:          ramdiskvarlog
    # Required-Start:    $local_fs $time
    # X-Stop-After:      $time
    # Required-Start:    $local_fs $time
    # Required-Stop:     $local_fs
    # Default-Start:     S
    # Default-Stop:      0 1 6
    # Short-Description: Restore to and save logs from tmpfs filesystem
    # Description:       Restore to and save logs from tmpfs filesystem
    ### END INIT INFO
    
    # /etc/init.d/ramdiskvarlog
    #
    
    case "$1" in
      start)
        echo "Copying files to ramdisk"
        rsync -av /var/backup/log/ /var/log/
        echo [`date +"%Y-%m-%d %H:%M"`] Ramdisk Synched from HD >> /var/log/ramdisk_sync.log
        ;;
      sync)
        echo "Synching files from ramdisk to Harddisk"
        echo [`date +"%Y-%m-%d %H:%M"`] Ramdisk Synched to HD >> /var/log/ramdisk_sync.log
        rsync -avy --delete --recursive --force /var/log/ /var/backup/log/
        ;;
      stop)
        echo "Synching logfiles from ramdisk to Harddisk"
        echo [`date +"%Y-%m-%d %H:%M"`] Ramdisk Synched to HD >> /var/log/ramdisk_sync.log
        rsync -av --delete --recursive --force /var/log/ /var/backup/log/
        ;;
      *)
        echo "Usage: /etc/init.d/ramdisk {start|stop|sync}"
        exit 1
        ;;
    esac
    
    exit 0
    
  4. Made /etc/init.d/ramdiskvarlog executable:
    chmod +x /etc/init.d/ramdiskvarlog
  5. Created a directory to store the logs persistently, and populated it initially with the contents of the existing /var/log with the following command line commands :
    mkdir -p /var/backup/log
    /etc/init.d/ramdiskvarlog sync
  6. Made the /etc/init.d/ramdiskvarlog script be run at boot time and during orderly shutdown with the following command line command
    systemctl enable ramdiskvarlog
  7. Made the /etc/init.d/ramdiskvarlog script copy the contents of /var/log to the sd card once every 24 hours
    1. At the command line gave the command
      crontab -e
    2. In the editor that opened on the crontab, added a line with the following contents
      2 7 * * * /etc/init.d/ramdiskvarlog sync >> /dev/null 2>&1
  8. Created a test file with “touch /var/log/test.log”, rebooted the raspberry pi with “sync; reboot”, and then:
    1. Checked with the mount command that /var/log was on tmpfs, found the following line in the output, which meant that /var/log was on tmpfs
      tmpfs on /var/log type tmpfs (rw,nosuid,noatime,size=102400k,mode=755)
    2. Checked that the /var/log/test.log file was present (and the file was present, which meant that it had been synced to persistent storage on shutdown and restored on boot)

After completing the setup, I popped the sd card out and put it into a card reader on a debian desktop computer. Then I made an image of the working sd card, so that if/when the sd card dies, getting a working router again should be as quick as just dd’ing the image to a new sd card and then switching sd card on the raspberry Pi.

Lesson learned!

Using a Raspberry Pi 2 Model B as a router/firewall for the home LAN

Since 1999 I have been using a 1996 vintage DEC PII desktop as the router/firewall between the internet and my home network.  The DEC computer came to me with Win95 (or possibly Win98) in 1998, got SuSE linux and started its mission as router and firewall (and CUPS server, and IMAP server, and various other server stuff). When upgrading the SuSE installation to a newer version went south, it spent a while running ThomasEz’s floppyfw, until I used a floppy net install to install debian potato, immediately switched it to debian testing, until debian woody arrived, when it was moved to debian stable, and then I just kept running “apt-get dist-upgrade” until I finally had it running debian 8 “jessie” on june 6 in 2015.

The old DEC desktop has survived its maker company, survived lightning strikes that have sent the power supplies and/or main boards of other computers on the same LAN into continously beeping mode (i.e. broken). However, in December 2015 it started acting up, and crashing with irregular intervals (sometimes two weeks, sometimes one day).

So… the time for a replacement would have to be not too far ahead. The question was what to replace it with?

The simplest solution would be to just get a wireless router with a cabled switch. But that would mean:

  • No possibilities for SSH or mosh into the home LAN
  • No ntop
  • No support for netboot and TFTP in the home LAN
  • Limited, cumbersome and inflexible firewall setup

My requirements were:

  • Cheap
  • Two wired NICs
  • The ability to run debian
  • Preferrably fanless
  • Compact

ThomasEz immediately suggested using a raspberry pi with two NICs, but I thought that would be too puny, and I investigated alternatives like Shuttle Barebone DS57U but I found that the raspberry pi alternative was so cheap, I might as well order one.

And then it turned out to be so simple to set up so I had it up and running before I really had decided on anything, so now the r-pi is what I have.

This is what I ordered:

Here’s what I did:

  1. Downloaded the Raspbian Jessie Lite image to a debian jessie computer and unpacked it into the /tmp directory
  2. Plugged an USB SD card reader into the debian computer, and followed the instructions in Installing operating system images on Linux 
  3. I plugged the cheapest USB keyboard I could get from my local teknikmagasinet store into one of the USB port, yanked the HDMI cable from the DVD player and plugged the r-pi into the TV, plugged a network cable into the local LAN, and plugged in the power… and the raspberry pi booted quickly into the familiar debian login
  4. I logged in with the built-in “pi” user with password “raspberry”, and created my own user with the following command line command:
    adduser sb

    the changed the password of the root user and removed the pi user

  5. I copied in a public ssh keys from my other computers, and put them into the ~/.ssh/authorized_keys file and then opened /etc/ssh/sshd_conf in a text editor and modified it in the following way:
    1. Disabled root login by changing
      PermitRootLogin without-password

      to

      PermitRootLogin no
    2. Disabled password login by changing
      #PasswordAuthentication yes

      to

      PasswordAuthentication no

      (removed the comment and changed “yes” to “no”)

  6. Edited /etc/hostname to change the name from the default “raspberrypi” to “ocon”
  7. Rebooted the pi to check the startup state of the ssh daemon and ssh’d in
  8. Resized the disk to fill the entire SD card:
    1. Typed the command
      raspi-config
    2. Selected
      1 Expand Filesystem            Ensures that all of the SD card storage is available to the OS

      and got the response

      Root partition has been resized.
      The filesystem will be enlarged upon the next reboot
    3. Rebooted the system to get the full 16GB in the file system
  9. Updated the system by giving the following command line commands:
    apt-get update
    apt-get dist-upgrade

    (the “update” command updates the local package database against the package servers. The “dist-upgrade” command upgrades all packages that have a newer version, and the required dependencies)

  10. Installed some useful software:
    1. GNU emacs (my favorite text editor)
      apt-get install emacs
    2. mosh
      apt-get install mosh
    3. git (I’ve got my home directory versioned in git)
      apt-get install git
    4. rcs (I use it to version control operating system configuration files)
      apt-get install rcs
  11. I cloned my home directory in git and created a new branch (I have a different branch for each computer)
  12. I set the built-in NIC permanently as eth0:
    export INTERFACE=eth0
    export MATCHADDR=`ip addr show $INTERFACE | grep ether | awk '{print $2}'`
    /lib/udev/write_net_rules
  13. I added configuration for a second NIC by adding the following to /etc/network/interfaces:
  14. # The internal network card
    allow-hotplug eth1
    iface eth1 inet static
       address 10.10.10.1
       netmask 255.255.255.0
  15. I plugged in the USB NIC to have it appear, and then made the USB NIC permanently eth1 with the following command line commands:
    export INTERFACE=eth1
    export MATCHADDR=`ip addr show $INTERFACE | grep ether | awk '{print $2}'`
    /lib/udev/write_net_rules
  16. Installed dnsmasq
    apt-get install dnsmasq
  17. Edited /etc/dnsmasq.conf to make dnsmasq respond to DHCP requests on eth1:
    1. Removed the comment in front of
      #interface=

      and set “eth1” as the value:

      interface=eth1
    2. Uncommented the domain directive
      #domain=thekelleys.org.uk

      and changed it to my domain

      domain=hjemme.lan
    3. Uncommented the dhcp-range directive
      #dhcp-range=192.168.0.50,192.168.0.150,12h

      and changed it to a 10.10.10.* range with a 5h lease on the addresses

      # Our HOME LAN 5h lease time
      dhcp-range=10.10.10.6,10.10.10.40,5h
  18. Opened the /etc/hosts file in a text editor and added the raspberry pi itself, to so that DNS lookups of the raspberry pi will work in a LAN where the raspberry pi is handling the DHCP requests (dnsmasq will handle DNS requests for the IP addresses it has given DHCP leases to, as well as what it finds in the hosts file.  The rest is delegated to the upstream DNS server)
    127.0.0.1       localhost
    ::1             localhost ip6-localhost ip6-loopback
    ff02::1         ip6-allnodes
    ff02::2         ip6-allrouters
    
    127.0.1.1       ocon
    
    # local hosts
    10.10.10.1  hjemme ocon hjemme.hjemme.lan ocon.hjemme.lan
    
  19. Edited the /etc/sysctl.conf file to set up IPv4 routing in the linux kernel, removed the comment in front of the net.ipv4.ip_forward line:
    # Uncomment the next line to enable packet forwarding for IPv4
    net.ipv4.ip_forward=1
    
  20. ferm is a utility that makes it easy to set the routing and firewall rules at boot time
    1. Installed ferm using apt-get from a command line:
      apt-get install ferm
    2. Modified the /etc/ferm/ferm.conf file to allow everything inside t oroute out, but only allow ssh in
      @def $DEV_WORLD = eth0;
      @def $DEV_PRIVATE = eth1;
      
      def $NET_PRIVATE = 10.10.10.0/24;
      
      table filter {
          chain INPUT {
              policy DROP;
      
              # connection tracking
              mod state state INVALID DROP;
              mod state state (ESTABLISHED RELATED) ACCEPT;
      
              # allow local packet
              interface lo ACCEPT;
      
              # allow private net
              interface $DEV_PRIVATE ACCEPT;
      
              # respond to ping
              proto icmp ACCEPT;
      
              # allow IPsec
              proto udp dport 500 ACCEPT;
              proto (esp ah) ACCEPT;
      
              # allow SSH connections
              proto tcp dport ssh ACCEPT;
          }
          chain OUTPUT {
              policy ACCEPT;
      
              # connection tracking
              #mod state state INVALID DROP;
              mod state state (ESTABLISHED RELATED) ACCEPT;
          }
      
          chain FORWARD {
              policy DROP;
      
              # connection tracking
              mod state state INVALID DROP;
              mod state state (ESTABLISHED RELATED) ACCEPT;
      
              # connections from the internal net to the internet or
              # to other internal nets are allowed
              interface $DEV_PRIVATE ACCEPT;
      
              # the rest is dropped by the above policy
          }
      }
      
      table nat {
          chain POSTROUTING {
              # masquerade private IP addresses
              saddr $NET_PRIVATE outerface $DEV_WORLD MASQUERADE;
          }
      }
      
  21. The version of ferm in “jessie” doesn’t start at boot, because “jessie” dropped SYSV init in favour of systemd, and the version of ferm in “jessie” doesn’t have a systemd configuration, so I needed to manually download and install the version of ferm from debian testing (I downloaded from regular debian, since ferm doesn’t have anything platform specific):
    cd /tmp
    wget http://ftp.no.debian.org/debian/pool/main/f/ferm/ferm_2.2-5_all.deb
    dpkg --install /tmp/ferm_2.2-5_all.deb
    
  22. fail2ban monitors log files of daemons and adjust the firewall rules to temporary ban hosts it suspects of intrusion attempts. The debian (and raspbian) version of fail2ban will out of the box scan the logs for ssh intrusion attempts, so no configuration is necessary
  23. To have an easy way of monitoring the network traffic in and out of the home LAN, I installed ntop ng
    apt-get install ntopng

    after the installation it is possible to monitor the network traffic by accessing http://ocon.hjemme.lan:3000 (the interesting traffic will be seen after selecting eth1)

  24. The Network Time Protocol is how computers stay in sync, installing the ntp package will make the gateway keep network time, a
    apt-get install ntp
  25. Opened the /etc/ntp.conf file in a text editor, and modified it to provide an NTP deamon for the home LAN, uncommented the “broadcast” line and modified the network match to match the 10.10.10.* network:
    # If you want to provide time to your local subnet, change the next line.
    # (Again, the address is an example only.)
    broadcast 10.10.10.255
    
  26. Installed the apticron utility to make sure that the APT database is updated daily with new candidates for update
    apt-get install apticron

The original plan was to run the raspberry pi headless, but since I had an old VGA only LCD display for the old DEC computer I might as well hook it up the raspberry pi, together with the cheap USB keyboard used for setup.

I bought an HDMI to VGA converter with the manufacturer id VLMP34900W0.20. I plugged it in between the display and the raspberry-pi the display stayed black.  I edited the /boot/config.txt file, removing the comment in front of the hdmi_safe line:

# uncomment if you get no picture on HDMI for a default "safe" mode
hdmi_safe=1

I rebooted the raspberry pi, and this time the LCD displayed showed the boot messages as well as a normal console login prompt.

The raspberry pi 2 model B, with an extra USB NIC, a USB keyboard and connected to a VGA display using an HDMI to VGA converter
The raspberry pi 2 model B, with an extra USB NIC, a USB keyboard and connected to a VGA display using an HDMI to VGA converter

And this is where the current state is. One initial concern was flash wear on the SD card, which doesn’t have the wear leveling features of a “real” SSD, so I had some plans on making the /var/log use tmpfs.

But I decided not to, since having real persistent logs is a useful thing for a gateway, and since 16GB is actually an awful lot of data if all you do is to write textual files. And ff the SD card wears out I’ll just by a new SD card, and make a new system. Since I now know how, this shouldn’t take long

Debian “jessie” on Intel “Skylake”

Except for work computers with GNU/linux, the last of which was retired in 2008, my GNU/linux computers have been outdated hand-me-downs. And when the P4 I got back in 2010 went belly up, I figured it was time for trying a modern machine.

Note: I wasn’t going for a top-of-the-line gaming computer with high performance everything. Just a modern state of the art computer.

I wasn’t satisfied with the combination of price and specs on the desktop computers sold by the consumer electronic retailers, so I asked an old colleague who likes building his own computers (thanks Alexey!) to help me come up with an order for components that would work when I put it together. This is what I ordered:

  • Main board: ASUS H170M-PLUS, Socket-1151
  • CPU: Intel Core i5-6600 Skylake
  • Memory: Corsair Vengeance LPX DDR4 2133MHz 16GB
  • SSD: Kingston SSDNow V300 120GB 2.5″ OEM
  • Hard disk: Seagate Barracuda® 1TB
  • Cabinet: Fractal Design Define S Black
  • Power supply: Corsair CX500, 500W PSU

I won’t spend much time on the task of putting the parts together to make a working computer, suffice to say that with re-watching this video, frequent phone calls to Alexey, compared with close reading of the documentation (Alexey told me to do that), I got it working and was greeted by the fancy screen that has taken the place of the BIOS.

UEFI Boot screen

I tried, and gave up on making PXE boot work for the debian install on the UEFI BIOS, and put the debian-8.3.0-amd64-netinst.iso image on an USB flash drive. I then inserted the USB flash drive in one of the USB3 connectors on the front of the cabinet, pressed F10 in the UEFI BIOS, and then kept F8 pressed F8 until I got to the boot menu.

In the boot menu, I selected

UEFI: Generic Flash Disk 8.07, Partition 1 (7640MB)

and then pressed ENTER.

In the debian installer:

  1. Selected the “Graphical installer”
  2. Selected “English” as the installer language
  3. Selected “Norway” as the time zone
  4. Selected “en_US.UTF-8” as the locale
  5. Selected “Norwegian” as the keyboard layout
  6. Gave “lorenzo” as the computer name
  7. Gave “hjemme.lan” as the domain name
  8. Set the root password
  9. I created a user for myself, and set the password
  10. Partitioned the disks manually:
    1. Partitioned the 120GB SSD. I put the root partition on the SSD to get a quick startup of the system, and to get fast startup of applications. I also had to put an EFI partition here. Without an EFI partition, the base-installer failed with a “No space left on device” error message:
      Number Size File system Name Flags
      #1 1GB fat32 efi boot,esp
      #2 119GB ext4 root
    2. Partitioned the 1TB HDD:
      1. I put the swap, sized to twice the physical memory, (something I’ve been doing since I installed my first GNU/linux box back in the 90-ies)
      2. To avoid SSD wear from frequent writing, I put the /var partition (where /var/log resides) on the spinning disk
      3. Finally, I made the rest of the disk the /home directory
      Number Size File system Name Flags
      #1 32GB linux-swap(v1) swap
      #2 100GB ext4 var
      #3 868GB ext4
  11. In the installer, I selected a package mirror from Norway (it doesn’t really matter which one, because of the NIX), selected “No proxy”, and continued
  12. I let the installer install GRUB on the hard disk
  13. During the installation of the system, the installer stopped with the following error message:
    Unable to install busybox
    An error was returned while trying to instal lthe busybox package
    onto the target system.
    
    Check /var/log/syslog or see virtual console 4 for details
  14. I googled for the error message, found this ubuntu bug report, and tried the following workaround from a comment on the bug, and the installer continued past the problem spot.The workaround/hack, was to boot the installer, press Ctrl-Alt-F2 to get a virtual console, and at the prompt in that console, type:
    # while true; do rm /var/run/chroot-setup.lock; sleep 1; done

    and then switch back to the installer in Ctrl-Alt-F1 and continue with the installation

  15. I let the installer run until completion, and pulled the USB flash drive from the USB3 connection (probably not necessary, since pressing F8 was necessary to get to the boot menu in the first place), and let the computer reboot
  16. The computer booted with the familiar debian gdm login screen, and a disappointing 1024×768 screen resolution
  17. I logged in to see what the display settings of the desktop had to say, but the display setting had 1024×768 as the only choice
  18. I let apt-get update the distribution
    apt-get update
    apt-get dist-upgrade
  19. I rebooted again after the update had completed, but the update wasn’t enough to fix the screen resolution, the display still had 1024×768 as the only available resolution
  20. This was my first test of Gnome 3 (when “gnome” in debian changed its meaning from the quite usable “Gnome 2” to “Gnome 3”, the old hardware on my previous debian computer wasn’t able to display anything at all), and I found it ugly and incomprehensible
  21. So I installed MATE
    apt-get install mate-desktop-environment

    and rebooted and logged in again

  22. This time, after logging in, I met something that looked very much like the old and familiar “Gnome 2” desktop, but still with 1024×768 as the only available display resolution
  23. I edited /etc/apt/sources.list and added apt lines for jessie-backports
    # jessie backports (4.3 kernel)
    deb http://http.debian.net/debian jessie-backports main contrib non-free
    
  24. I installed the kernel, firmware and xserver-xorg-video-intel
    apt-get -t jessie-backports install linux-image-amd64 firmware-linux
    apt-get -t jessie-backports xserver-xorg-video-intel
  25. After a new reboot I was up and running, and this time with 1600×1200 resolution on the display, which is the maximum the old display I was using would support
  26. Since I got a working system by using packages from backports, I didn’t make the jump to debian testing immediately, but I figured I might as well get as new packages as possible from jessie-backports, so I created an /etc/apt/preferences file with the following contents:
    Package: *
    Pin: release a=jessie
    Pin-priority: 700
    
    Package: *
    Pin: release a=jessie-updates
    Pin-priority: 710
    
    Package: *
    Pin: release a=jessie-backports
    Pin-priority: 720
    
  27. Then I did apt-get update followed by dist-upgrade and pulled in new versions of many packages
    apt-get update
    apt-get dist-upgrade
  28. I used apt-get to install many familiar packages from my old system
    apt-get install xscavenger
    apt-get install default-jdk
    apt-get install chromium
    apt-get install flightgear
    apt-get install oolite
  29. Like I always do on debian systems, I pulled in “real” firefox from Mint debian edition:
    1. I edited /etc/apt/sources.list file and added the apt lines for Mint debian edition
      # Linux Mint Debian Edition (has firefox)
      deb http://packages.linuxmint.com debian import
    2. I installed the key for Mint debian edition
      apt-get update
      apt-get install linuxmint-keyring
      apt-get update
    3. I used apt to install firefox
      apt-get install firefox
  30. I installed apticron that will check for updates daily and notify me about updates
    apt-get install apticron
  31. Then I rebooted into the system I’m currently running

That’s it basically. Things seems to work out of the box, sound, video etc. (youtube doesn’t play in chromium, but it does play in firefox).

Installing debian “squeeze” with PXE boot on a Samsung N145 Plus netbook

Introduction

This article describes the steps necessary to install debian 6 “squeeze” on a Samsung N145 Plus netbook, with the following specification:

  • Intel Atom processor
  • 10.1″ display
  • 1GB RAM
  • 340GB HDD
  • Windows 7 preinstalled

Setting up netboot of the debian installer

DHCP requests in my home LAN network is provided by dnsmasq on a desktop PC running GNU/linux debian stable (which at the time of writing, was Debian 6 squeeze). One nice feature of dnsmasq is that it can provide PXE network boot.

So what I did was to download the i386 network boot image and put the contents in the /var/tftpd/debian-installer/i386 directory of the computer running dnsmasq, and then edit the /etc/dnsmasq.conf file in the following way:

  1. Remove the comment in front of the dhcp-boot config line:
    dhcp-boot=pxelinux.0
  2. Set the tftp-root pointing to the directory containing the pxelinux.0 file:
    tftp-root=/var/tftpd/debian-installer/i386

Installing debian

Booting from the network

I connected the netbook with to the switch in my home LAN an RJ45  twisted pair cable, and powered on the netbook, and kept the F12 button pressed during boot, and ended up in the debian text based installer.

I set the time zone and location of the install (Oslo, Norway), created an initial user and set the root password.

Partitioning

The netbook came with a 340GB and Windows 7 preinstalled.  The hard disk was partitioned so that the Win7 system had both a C: and a D: drive, with the operating system installed on the C: drive.

The plan was to keep the Windows 7 installation, sans its D: drive and install debian in the part of the hard disk occupied by the D: drive.

The initial partitioning table looked like this:

#1 primary 104.9 MB B ntfs
#2 primary 93.4 GB ntfs
#5 logical 138.3 GB ntfs
#4 primary 28.2 GB ntfs

I guessed that partititon #1 was the boot partition, and that partition #2 was the C: drive containing the Windows 7 installation, and that #4 was either some kind of Samsung software (diagnostics possibly) or something belonging to the Windows 7 installation.

I left partition #1, #2 and #4 alone, and deleted the partition containing the D: drive (partition #5), and turned that into free space:

#1 primær 104.9 MB B ntfs
#2 primær 93.4 GB ntfs
pri/log 138.3 GB FREE SPACE
#4 primær 18.2 GB ntfs

I added a swap partition twice the size of the physical memory i.e. 2GB, and added an ext3 partition using the rest of the free space, and ended up with a partitioning table looking like this:

#1 primary 104.9 MB B ntfs
#2 primary 93.4 GB ntfs
#5 logical 136.3 GB B f ext3 /
#6 logical 2.0 GB f swap swap
#4 primary 18.2 GB ntfs

I saved the partitioning table and continued.

Installing the system

After completing the partitioning, I selected the following items to install:

  • SSH server
  • Laptop
  • Base tools

I let the installer run, using defaults for all questions. I answered YES to the question of whether GRUB should be installed on MBR. The installer found the Windows 7 installation and added it to the GRUB boot menu.  When the time came to reboot, I let the installer reboot.

After the reboot I logged in as root and installed the “KDE Plasma netbook” package:

apt-get install plasma-netbook kde-l10n-nb

I opened the /etc/apt/sources.list in a text editor, and modified it:

I then updated the APT database with the new sources and added all updates to the already installed software:

apt-get update
apt-get install linuxmint-keyring
apt-get update
apt-get dist-upgrade

I then installed all software I assumed was necessary:

apt-get install ttf-mscorefonts-installer
apt-get install openoffice.org openoffice.org-l10n-nb
apt-get install firefox firefox-l10n-nb

I rebooted the laptop and then logged into the plasma desktop using the user created at the start of the installation process. The desktop was missing network support and other useful software.

I logged in as root using the “failsafe” alternative, and installed missing software in the terminal window:

apt-get install network-manager-kde update-notifier-kde
apt-get install synaptic software-center gdebi

I rebooted and logged into plasma again. I tried to plug in an USB flash memory, and discovered that the desktop had no file manager, konqueror was missing. I installed konqueror (and discovered I should have picked the package “kde-plasma-netbook”, rather than just “plasma-netbook”):

apt-get install konqueror

The plasma desktop looked great, but was way to slow on an atom processor without much in the way of graphical hardware acceleration.

So I decided to try gnome and installed gnome with the command:

apt-get install gnome

I let apt set gdm3 as the default login instead of kdm.

I rebooted and logged into the gnome desktop, and it performed a lot better than the plasma desktop.

I rebooted again chose Windows 7 from the grub menu, and Windows 7 booted and logging into the desktop worked.

Making the Fn keys adjust the display brightness

The Fn keys for the adjusting the brightness didn’t work. I googled, and found two promising web pages:

  1. Fixing brightnes control, etc. on a Samsung R510 with Debian Squeeze
  2. InstallingDebianOn Samsung Samsung N150

I decided to try the first approach, and downloaded the packages created for Ubuntu Natty from https://launchpad.net/~voria/+archive/ppa

I then installed the downloaded .deb packages in the following way:

  1. Installed the easy-slow-manager:
    1. I let gdebi pull in all depdendencies (gcc, the linux-headers, make, etc)
  2. Installed samsung-backlight:
    1. Edited /etc/default/grub changing the line GRUB_CMDLINE_LINUX_DEFAULT
      GRUB_CMDLINE_LINUX_DEFAULT="quiet"
      to
      GRUB_CMDLINE_LINUX_DEFAULT="quiet acpi_backlight=vendor"
    2. Ran the command
      update-grub
  3. Installed samsung-tools:
      1. Installed the devscripts
        apt-get install devscripts
      2. Unpacked the samsung-tools tarball
        cd /tmp
        tar zxvf samsung-tools_1.4~ppa3~loms~natty.tar.gz
        cd /tmp/samsung-tools_1.4~ppa3~loms~natty
        dch -l sb
        1. Added “Compiled for debian squeeze” as the final comment

     

     

  4. Built the deb package
    cd /tmp/samsung-tools-1.4~ppa3~loms~nattysb1
    dpkg-buildpackage -rfakeroot -us -uc
  5. Installed the deb package
    gdebi /tmp/samsung-tools_1.4~ppa3~loms~nattysb1_all.deb
    1. I let gdebi install all of the required dependencies
  6. Rebooted

After the reboot I tried the Fn+Up and Fn+Down keys to adjust the display brightness and the keys worked fine.

Objektorientering i Java: “Monstersim”

English summary: This article is in Norwegian and presents a step-by-step receipe for creating a Java program that simulates a dragon and a troll in a cave. The purpose is to give an introduction to the Java programming language, object orientation, computer simulation, and the eclipse Java IDE’s support for aiding coding (in particular “Quick Fix”).

Denne artikkelen tar deg gjennom en steg-for-steg bruksanvisning for å lage et program som simulerer en drage og et troll i ei grotte. Artikkelen er ganske lang, men bør være rett fram å følge.

Hensikten er å gi et innblikk i Java, objektorientering, objektorienterte simuleringer, og eclipses støtte for å skrive kode (spesielt “Quick Fix”).

Artikkelen nevner begrep som “klasse” og “type” og “objekt” og “felt” og “metode” og “konstruktør”, men vil ikke gjøre noe forsøk på å forklare dem. Google er fin og ty til om man lurer på noe her.

Artikkelen hinter om begrepet arv/supertyper (Actor), men gjør ikke noe forsøk på å forklare dem. Meningen er at man skal lære (og forstå) av å gjøre heller enn å høre.

Bittelitt om Java

Java, som er språket eclipse er skrevet i, er et objektorientert språk. Begrepet “objektorientert programmeringsspråk” kommer fra Universitetet i Oslo, på 1960-tallet og fra et språk som heter Simula.

Som navnet “Simula” antyder, så var språket beregnet på å gjøre datasimuleringer. Ideen var at man skulle beskrive ting i den virkelige verden med objekter i datamaskinen, og at objektene skulle ha sin egen kode som beskrev oppførsel og hvordan de reagerte mot andre objekt.

Hva som skal simuleres

I simuleringa skal vi la en drage og et troll vandre fra rom til rom i ei grotte.

Om dragen og trollet møtes i samme rom, vil de slåss. Normalt vil trollet vinne over dragen.

Men det vil også finnes en gullskatt i et av rommene, og om dragen kommer over gullskatten før den treffer trollet, så vil dragen bli i det rommet og beskytte gullskatten. Gullskatten vil også gi dragen styrke sånn at den klarer å vinne over trollet, om trollet skulle virre inn i rommet til dragen.

Prosjekt, program og simulator

Start eclipse og i “Select a workspace”-dialogen, angi

C:userssteinarbworkspacesmonstersim

(bytt ut “steinarb” med navnet på din egen bruker)

Velg du fra menyen

File -> Java Project

I “Project name:” skriver du

monstersim

og klikk på “Finish”-knappen, og klikk på teksten “Workbench” i vinduet under.

Høyreklikk så på “monstersim” i “Package Explorer” (til venstre i Eclipse) og velg “Package”. I Name-ruta, gi teksten:

no.example.monstersim

Høyreklikk så på teksten “no.example.monstersim”, og velg

New -> Class

I “Name:” skriver du:

Monstersim

Klikk også i sjekkboksen ved teksten “public static void main(String[] args”, og klikk så på “Finish”-knappen.

I Monstersim.java som dukker opp i Eclipse, endre main()-metoden, til:

public static void main(String[] args) {
        int numberOfSimulations = 10;
        int caveWidth = 3;
        int caveHeight = 3;
        Simulator simulator = new Simulator(caveWidth, caveHeight);

        simulator.simulate(numberOfSimulations);
}

I venstre marg på Monstersim.java, så vises et ikon som består av ei lyspære, delvis overlappet av en rød firkant med en hvit “X” i seg.

Når du klikker på dette ikonet vil meny (“quick fix”-menyen) åpne seg. Dobbeltklikk på valget

Create class "Simulator"

I “Java Class”-dialogen, klikk på “Finish”-knappen. En ny tab med “Simulator.java” vil åpne seg.

Nede i eclipse er det en tab som heter “Problems”. Velg “Problems. I “Problems”, er det to linjer som starter med samme ikon som det du klikket på. Høyreklikk på linja som sier

The constructor Simulator(int, int) is undefined

og velg “Quick Fix”.

Velg

Create constructor "Simulator(int, int)"

og klikk på “Finish”-knappen. Eclipe bytter til Simulator.java igjen. Bytt ut TODO-linja i “Simulator(int, int)” sånn at konstruktøren blir seende sånn ut:

public Simulator(int caveWidth, int caveHeight) {
        this.caveWidth = caveWidth;
        this.caveHeight = caveHeight;
}

Set cursoren på “this.caveWidth” i første linje, og trykk “Ctrl-1” (hold nede “Ctrl”-tasten mens du trykker og slipper “1”). Dette aktiverer vår venn “Quick Fix”. Velg

Create field 'caveWidth'

Trykk Ctrl-. (hold nede Ctrl og trykk og slipp punktum-tasten) for å gå til neste feil, og trykk Ctrl-1 og bruk “Quick Fix” for å lage et felt for “caveHeight” også.

Velg fra toppmenyen

File -> Save All

I Problems-tabben står ei linje med teksten “The method simulate(int) is undefined for the type Simulator” (det er også to Warnings, men dem bryr vi oss ikke om nå). Høyreklikk på linja, velg “Quick Fix” og i “Quick Fix”-dialogen, velg linja

Create method 'simulate(int)' in type 'Simulator'

og klikk på “Finish”-knappen.

Velg nok en gang fra toppmenyen

File -> Save All

Lage simulator-“motoren”

Endre Simulator.simulate()-metoden sånn at den ser sånn ut:

public void simulate(int numberOfSimulations) {
        System.out.println("Simulating a " + caveWidth + "x" + caveHeight + " cave:");
        Cave cave = new Cave(caveWidth, caveHeight);
        List<Actor> actors = setupActors();
        cave.addActorsToRooms(actors);
        cave.printDescriptions();
        for (int simulationStepNumber=0; simulationStepNumber<numberOfSimulations; ++simulationStepNumber) {
                Actor deadActor = oneStep(simulationStepNumber, actors);
                if (null != deadActor) {
                        System.out.println(deadActor.getName() + " is dead.  Ending simulation!");
                        return ;
                }
        }
}

Sett kursoren på “Cave” i “Cave cave”, og så Ctrl-1, og velg

Create class 'Cave'

Ta Ctrl-Shift-s for å lagre alle filer.

Ta Ctrl-F6 for å gå til “Simulator.java”. Ta Ctrl-. for å gå til neste feil, og så “Quick Fix” og velg

Create constructor 'Cave(int, int)'

Trykk RET to ganger.

Ta Ctrl-Shift-s for å lagre alle filer.

Ta Ctrl-F6 for å gå til “Simulator.java”. Ta Ctrl-. for å gå til neste feil, og så “Quick Fix” og velg

Import 'List' (java.util)

Ny Ctrl-., og så ny “Quick Fix” og velg

Create class 'Actor'

klikk på Finish.

Ta Ctrl-Shift-s for å lagre alle filer, og så Ctrl-F6 for å gå tilbake til Simulator.java.

Gå til feilen ved “setupActors()” og ny “Quick Fix” og velg

Create method 'setupActors()'

Gå til feilen ved “addActorsToRoom” og ta “Quick Fix” og velg

Create method 'addActorsToRoom(List<Actor>)' in type 'Cave'

Trykk RET to ganger for å velge default-verdier, og så lagre alle filer.

Gå tilbake til Simulator.java og velg feilen ved “printDescription” og ta “Quick Fix” og velg

Create method 'printDescriptions() in type 'Cave'

trykk RET to ganger, og så Ctrl-Shift-s for å lagre alle filer.

Gå til feilen ved “oneStep”, ta “Quick Fix” og velg

Create method 'oneStep(int, List<Actor>)'

Gå til feilen ved “getName”, ta “Quick Fix” og velg

Create method 'getName()' in type 'Actor'

trykk RET to ganger svar for å velge default-verdier og lagre alle filer, med Ctrl-Shift-s.

Bruk Ctrl-F6 til å bytte tilbake til Simulator.java og endre “oneStep” sånn at metoden blir seende sånn ut:

private Actor oneStep(int simulationStepNumber, List<Actor> actors) {
        System.out.println("Simulation step: "+(simulationStepNumber+1));
        moveTheActors(actors);
        letActorsAct(actors);
        return checkForDeadActors(actors);
}

Velg feilen i “checkForDeadActors”.

Bruk så “Quick Fix” (først Ctrl-1, så Ctrl-, til å gå til forrige feil, så Ctrl-1 for å trigge “Quick Fix”, så Ctrl-, for å gå til forrige feil osv.) til å lage de tre manglende metodene. Grunnen til å ta “Quick Fix” baklengs, er for å få de nylagde metodene i logisk rekkefølge i fila. “Quick Fix” lager alltid nye metoder rett under den metoden du står i når du kjører “Quick Fix”.

Bytt ut TODO i “moveTheActors”:

private void moveTheActors(List<Actor> actors) {
        for (Actor actor : actors) {
                actor.move();
        }
}

(om du bruker kopiering og liming herfra, vil innrykket på siste klammeparantes være feil)

Ta “Quick Fix” på feilen i “move” og velg

Create method 'move()' in type 'Actor'

trykk RET to ganger for å velge default, og lagre alle filer. Ta Ctrl-F6 for å gå tilbake til Simulator.java.

Ta Ctrl-Shift-f (hold nede Ctrl og Shift, mens du trykker og slipper f-tasten). Det vil reformatere Simulator.java og sørge for at innrykket på siste klammeparantes blir riktig.

Bytt ut TODO i “letActorsAct”:

private void letActorsAct(List<Actor> actors) {
        for (Actor actor : actors) {
                actor.act();
        }
}

Ta “Quick Fix” på feilen i “move” og velg

Create method 'act()' in type 'Actor'

trykk RET to ganger for å velge default, og lagre alle filer. Ta Ctrl-F6 for å gå tilbake til Simulator.java.

Om innrykket på nest siste klammeparantes er feil, ta en ny Ctrl-Shift-f.

Bytt ut TODO i “checkForDeadActors”:

private Actor checkForDeadActors(List<Actor> actors) {
        for (Actor actor : actors) {
                if (actor.isDead()) {
                        return actor;
                }
        }

        return null;
}

Ta “Quick Fix” på “isDead” og velg

Create method 'isDead()' in type 'Actor'

trykk RET to ganger for å velge default, og lagre alle filer. Ta Ctrl-F6 for å gå tilbake til Simulator.java.

Om innrykket på nest siste klammeparantes er feil, ta en ny Ctrl-Shift-f.

“Motoren” i simuleringen består av løkker. Ytterst er en løkke som teller gjennom alle stegene i simuleringen. For hvert steg går man gjennom løkker som animerer alle deltagerene i simuleringen.

Bygge grotta

Gå til Cave.java og bytt ut TODO i konstruktøren “Cave(caveWidth, caveHeight)” med:

public Cave(int caveWidth, int caveHeight) {
        this.caveWidth = caveWidth;
        this.caveHeight = caveHeight;
        createCave();
        connectRooms();
}

Sett kursoren på “caveWidth” i “this.caveWidth” og ta “Quick Fix”, og velg:

Create field 'caveWidth' in type 'Cave'

Gjør det samme for “caveHeight”

La “Quick Fix” lage metodene “createCave” og “connectRooms” og lagre alle filer.

Merk: “Outline”-vinduet til høyre i Eclipse viser metodene etterhvert som du lager dem. Ved å klikke på metodene der kan du navigere rett til metoden. Du kan også endre rekkefølgen på metodene med å dra og slippe dem i Outline-vinduet.

Merk2: På høyre side av Cave.java-vinduet vises blå firkanter for alle linjene som inneholder TODO. Ved å klikke på en blå firkant kan du navigere rett til TODO-linja.

Bytt ut TODO i “createCave” med dette:

private void createCave() {
        rooms = new Room[caveWidth][caveHeight];
        for (int xPos=0; xPos<caveWidth; ++xPos)
        {
                for (int yPos=0; yPos<caveHeight; ++yPos)
                {
                        rooms[xPos][yPos] = new Room(xPos, yPos);
                }
        }
}

Sett kursoren over “Room” i første linje og la “Quick Fix” lage klassen “Room”. Lagre alle filer og bruk Ctrl-F6 til å gå tilbake til Cave.java.

Sett kursoren over “rooms” i første linje og la “Quick Fix” lage feltet rooms, som skal se slik ut når det er ferdig:

private Room[][] rooms;

La “Quick Fix” lage konstruktøren “Room(int, int)”, og endre metoden til dette:

public Room(int xPos, int yPos) {
        super();
        name = "Room at (" + xPos + "," + yPos + ")";
}

La “Quick Fix” lage feltet “name”, og lagre alle filer.

Ta Ctrl-F6 for å gå tilbake til Cave.java. Bytt så ut innholdet i “connectRooms” med dette:

private void connectRooms() {
        for (int xPos=0; xPos<caveWidth; ++xPos)
        {
                for (int yPos=0; yPos<caveHeight; ++yPos)
                {
                        Room room = rooms[xPos][yPos];

                        if (xPos>0)
                        {
                                Direction direction = Direction.WEST;
                                Room neighbour = rooms[xPos-1][yPos];
                                room.setNeighbour(direction, neighbour);
                        }

                        if (xPos<caveWidth-1)
                        {
                                room.setNeighbour(Direction.NORTH, rooms[xPos+1][yPos]);
                        }

                        if (yPos>0)
                        {
                                room.setNeighbour(Direction.EAST, rooms[xPos][yPos-1]);
                        }

                        if (yPos<caveHeight-1)
                        {
                                room.setNeighbour(Direction.SOUTH, rooms[xPos][yPos+1]);
                        }
                }
        }
}

Sett kursoren på første feil i “Direction direction”, ta “Quick Fix” og velg:

Create enum 'Direction'

og klikk på “Finish”-knappen (eller bare trykk RET), lagre alle filer.

Ta Ctrl-F6 for å komme tilbake til Cave.java. Ta Ctrl-. for å gå til “WEST” og Ctrl-1 for å velge “Quick Fix” og velg så

Create enum constant 'WEST' in 'Direction'

Lagre alle filer og ta Ctrl-F6 for å komme tilbake til Cave.java og ta “Quick Fix” på “setNeighbour” og velg

Create method 'setNeighbour(Direction, Room)' in type 'Room'

og trykk RET to ganger for å velge default-verdier. Endre metoden til dette:

public void setNeighbour(Direction direction, Room neighbour)
{
        neighbours.put(direction, neighbour);
}

Gjør så en “Quick Fix” på “neighbours” og lag et felt. Endre feltet så det blir seende sånn ut (det blir default av type Object):

Map<Direction, Room> neighbours = new EnumMap<Direction, Room>(Direction.class);

Lagre alle filer og sett kursoren på “Map”, ta “Quick Fix”, og velg:

Import 'Map' (java.util)

Ta Ctrl-. og så Ctrl-1 og velg:

Import 'EnumMap' (java.util)

og lagre så alle filer.

Lagre alle filer og ta Ctrl-F6 for å komme tilbake til Cave.java og bruk så Ctrl-. og Ctrl-1 for å lage de resterende enum-konstantene. Lagre alle filer når du er ferdig, og gå til Cave.java.

Gå til “printDescriptions”, og endre TODO sånn at metoden blir seende sånn ut:

void printDescriptions() {
        for (int xPos=0; xPos<caveWidth; ++xPos)
        {
                for (int yPos=0; yPos<caveHeight; ++yPos)
                {
                        System.out.print(rooms[xPos][yPos].describe());
                }
        }
}

Gjør “Quick Fix” på linja med “describe()” og velg

Create method 'describe()' in type 'Room'

trykk RET en gang for å velge default, og skriv så “strin” (uten anførselstegn), skriv Ctrl-SPC og trykk RET.

Endre innhold i den nylagde metoden sånn at den blir seende sånn ut:

public String describe()
{
        if (contents.size()>0)
        {
                StringBuffer sb = new StringBuffer(name);
                sb.append(" contains:n");
                for (Actor actor : contents)
                {
                        sb.append(" ");
                        sb.append(actor.describe());
                        sb.append("n");
                }

                return sb.toString();
        }

        return "";
}

Gjør “Quick Fix” på feilen i “describe” og velg

Create field 'contents'

Trykk F3 for å gå til “contents” og endre feltet fra en Map til et Set:

Set<Actor> contents = new HashSet<Actor>();

“Quick Fix” feilen i Set og velg:

Import 'Set' (java.util)

“Quick Fix” feilen i HashSet og velg:

Import 'HashSet' (java.util)

Lagre alle filer med Ctrl-Shift-s.

Trykk Ctrl-. for å gå til feilen i “describe” og bruk “Quick Fix” og velg

Create method 'describe()' in type 'Actor'

trykk RET en gang for å velge default, og skriv så “strin” (uten anførselstegn), skriv Ctrl-SPC og trykk RET.

Endre “describe()” fra dette:

public String describe() {
        // TODO Auto-generated method stub
        return null;
}

til dette

public abstract String describe();

Lagre alle filer.

Ta “Quick Fix” på “describe”, og velg:

Make type 'Actor' abstract

Lagre alle filer og bruk Ctrl-F6 til å gå til Cave.java.

Sett kursoren på “name” og gjør “Quick Fix”, og velg:

Create getter and setter for 'name'

I dialogen “Encapsulate Field”, trykk på “OK”-knappen.

Lagre alle filer.

Gå til Cave.java, finn metoden “addActorsToRooms” og endre den til å se sånn ut:

void addActorsToRooms(List<Actor> actors) {
        for (Actor actor : actors) {
                rooms[randX()][randY()].enter(actor);
        }
}

Bruk “Quick Fix” for å lage metoden “randX” og endre den til å se sånn ut:

private int randX() {
        return randomGenerator.nextInt(caveWidth);
}

Bruk “Quick Fix” til å lage feltet “randomGenerator”, trykk F3 for å navigere til feltet og endre det fra

private Object randomGenerator;

til

private Random randomGenerator = new Random();

Bruk “Quick Fix” på “Random” og velg

Import 'Random' (java.util)

Gå til feilen med Ctrl-. og bruk “Quick Fix” til å lage metoden “randY”, som så endres til:

private int randY() {
        return randomGenerator.nextInt(caveHeight);
}

Sett kursoren i “enter”, bruk “Quick Fix” til å lage metoden i Room, og endre metoden “enter” sånn at den blir seende sånn ut:

public void enter(Actor actor) {
        contents.add(actor);
        actor.setCurrentLocation(this);
}

Lagre alle filer.

Gjør “Quick Fix” på “setCurrentLocation” og velg:

Create method 'setCurrentLocation(Room)' in type 'Actor'

trykk RET to ganger for å velge default

Endre innholdet i “setCurrentLocation” sånn at den blir seende sånn ut:

public void setCurrentLocation(Room room) {
        currentLocation = room;
}

Bruk “Quick Fix” til å lage feltet “currentLocation”.

Bygge deltagerene i simuleringen

Dette er objektene som blir animert av Simulator.

Actor og Monster

Gå til Actor.java (Ctrl-F6 er din venn).

Endre “getName()” til dette:

public String getName() {
        return name;
}

Bruk “Quick Fix” til å lage feltet “name”.

Endre “move” og “act” til dette:

public void move() {}
public void act() {}

Endre “isDead” til dette:

public boolean isDead() {
        return dead;
}

Bruk “Quick Fix” til å lage feltet “dead”.

Velg fra menyen:

Source -> Generate Getters and Setters...

I dialogen “Generate Getters and Setters”, klikk på “Select All” og klikk så på “OK”-knappen. Lagre alle filer.

Merk: om du ikke liker rekkefølgen på metodene så er det lett å sjonglere på rekkefølgen med å klikke og dra i dem i Outline-vinduet.

Så er det tid for monstrene.

I “Package Explorer”, høyreklikk på “no.example.monstersim” og velg:

New -> Class

I “New Java Class”-dialogen:

  • I Name: skriv
    Monster
    
  • Kryss av for “abstract”
  • I SuperClass: skriv
    Actor
    
  • Fjern avkryssing for “Inherited abstract methods”

Klikk på “Finish”-knappen

Velg fra menyen

Source -> Override/Implement Methods...

I dialogen “Override/Implement Methods”:

  • Fjern avkryssing for “describe”
  • Kryss av for “act”
  • Kryss av for “move”

Klikk på “OK”-knappen

Endre “move” til å se sånn ut:

@Override
public void move() {
        if (isDead()) {
                return;
        }

        Room thisRoom = getCurrentLocation();
        Direction direction = randomDirection();
        Room nextRoom = thisRoom.getNeighbour(direction);
        while (null == nextRoom) {
                nextRoom = thisRoom.getNeighbour(randomDirection());
        }

        thisRoom.leave((Actor)this);
        nextRoom.enter(this);
        System.out.println(" " + getName() + " moves from " + thisRoom.getName() + " to " + nextRoom.getName());
}

Gjør “Quick Fix” på “randomDirection” og lag en ny metode, som du endrer til å se sånn ut (merk, du må endre returtypen til Direction):

Direction randomDirection() {
        double number = Math.random();
        if (number < 0.25) {
                return Direction.WEST;
        } else if (number < 0.5) {
                return Direction.NORTH;
        } else if (number < 0.75) {
                return Direction.EAST;
        }

        return Direction.SOUTH;
}

Gjør “Quick Fix” på “getNeighbour” og velg

Create method 'getNeighbour(Direction)' in type 'Room'

trykk RET to ganger.

Endre “getNeighbour” til å se sånn ut:

public Room getNeighbour(Direction direction)
{
        return neighbours.get(direction);
}

Lagre alle filer og gå tilbake til Monster.java med Ctrl-F6.

Gjør “Quick Fix” på “thisRoom.leave” og velg

Create method 'leave(Monster)' in type 'Room'

Trykk RET to ganger. Endre metoden “leave” til:

public void leave(Actor actor) {
        actor.setCurrentLocation(null);
        contents.remove(actor);
}

Lagre alle filer og gå tilbake til Monster.java.

Endre “act” til å se sånn ut:

@Override
public void act() {
        if (isDead()) {
                return;
        }

        Set<Actor> roomContents = getRoomContentsExceptForMe();
        look(roomContents);
        take(roomContents);
        fight(roomContents);
}

Merk: om “act()” har fått feil innrykk, så bare merk hele metoden og ta Ctrl-i for å fikse innrykket.

(grunnen til at ting kan få feil innrykk er om de har feil i seg. I dette tilfelle er det Set og metoden getRoomContentsExceptForMe, som ikke er definert)

Sett kursoren på linja med “Set” og ta “Quick Fix” og velg

Import 'Set' (java.util)

Ny “Quick Fix”, og velg

Create method 'getRoomContentsExceptForMe'

Trykk F3 for å gå til den nye metoden og endre den til å se sånn ut:

private Set<Actor> getRoomContentsExceptForMe() {
        Set<Actor> roomContents = getCurrentLocation().getContents();
        roomContents.remove(this);
        return roomContents;
}

Ta “Quick Fix” på “getContents” og velg

Create method "getContents()" in type 'Room'

og trykk RET to ganger for å velge default-verdier.

Endre så metoden til å se sånn ut:

public Set<Actor> getContents() {
        return new HashSet<Actor>(contents);
}

Lagre alle filer og returner til Monster.java.

Sett kursoren på feilen i linja med “fight”, og bruk Ctrl-1 og velg

Create method 'fight(Set<Actor>)

og gå baklengs gjennom feilene med Ctrl-, og gjør “Quick fix” og lag metodene “take” og “look”.

Endre metoden “look”, til:

public void look(Set<Actor> roomContents)
{
        if (roomContents.size() > 0) {
                System.out.println(" " + getName() + " sees the following actors: ");
                for (Actor actor : roomContents) {
                        System.out.println("  " + actor.describe());
                }
        }
}

Ta Ctrl-Shift-f for å fikse innrykket til nest siste parantes (om du har kopiert og limt inn innholdet fra eksempelet).

Endre “take” til dette:

public void take(Collection<Actor> roomContents)
{
        Actor thePicker = this;
        for (Actor actor : roomContents) {
                actor.pickedUpBy(thePicker);
        }
}

Kjør “Quick Fix” på feilen i “pickedUpBy” og velg

Create method 'pickedUpBy(Actor)' in type 'Actor'

trykk RET to ganger for å ta defaulter.

Endre methoden “pickedUpBy” sånn at det blir en tom metode, dvs. fra

public void pickedUpBy(Actor thePicker) {
        // TODO Auto-generated method stub

}

til

public void pickedUpBy(Actor thePicker) {}

Lagre alle filer og gå tilbake til Monster.java.

Gå til feilen i “fightMe” og ta “Quick Fix” og så

public void fight(Collection<Actor> roomContents)
{
        Actor opponent = this;
        for (Actor actor : roomContents) {
                actor.fightMe(opponent);
        }
}

Endre metoden “fightMe” sånn at det blir en tom metode, dvs. fra

public void fightMe(Actor opponent) {
        // TODO Auto-generated method stub

}

til

public void fightMe(Actor opponent) {}

Mens vi er i Actor.java, så legger vi inn noen metoder til, som vi får bruk for seinere:

public double strength() { return 0.0; }
public void takeMe(Actor pickedUpThing) {}

Lagre alle filer og gå til Monster.java. Velg fra menyen

Source -> Override/Implement Methods...

I dialogen “Override/Implement Methods”

  • Fjern avkryssing for “describe”
  • Kryss av “fightMe”

Klikk på “OK”-knappen.

Endre metoden til dette:

@Override
public void fightMe(Actor opponent) {
        System.out.println(" " + opponent.getName() + " fights " + getName() + "!");
        if (opponent.strength() > strength()) {
                setDead(true);
                System.out.println(" Arrrrgh! " + getName() + " dies!");
        }
}

Lagre alle filer.

Enter the Dragon

Nå er det omsider tid for det første ordenlige monsteret. I “Package Explorer”, høyreklikk på no.example.monstersim, og velg

New -> Class

I dialogen “New Java Class”

  • I Name: skriv
    Dragon
    
  • I Superclass: skriv
    Monster
    

Klikk på “OK”-knappen.

Endre “describe”-metoden til å se sånn ut:

@Override
public String describe() {
        return "A dragon with green shimmering scales";
}

Velg fra menyen

Source -> Generate Constructors from Superclass...

I dialogen “Generate Constructors from Superclass”, trykk på “OK”-knappen.

Endre den nylagde konstruktøren til å se sånn ut:

public Dragon() {
        super();
        setName("Dragon");
}

Velg fra menyen

Source -> Override/Implement Methods...

I dialogen “Override/Implement Methods”

  • Kryss av for “move()”
  • Lukk “Monster”
  • Åpne “Actor”
  • Kryss av for “strength()”
  • Kryss av for “takeMe(Actor)”

Klikk på “OK”-knappen.

Endre metoden “takeMe” til (merk: også endring av argumentnavnet fra “me” til “pickedUpThing”):

@Override
public void takeMe(Actor pickedUpThing) {
        if (null == gold) {
                gold = pickedUpThing;
                System.out.println(" " + getName() + " picked up " + pickedUpThing.getName());
        }
}

Bruk “Quick Fix” på “gold” i

gold = pickedUpThing

for å lage et felt med navn “gold” (om du gjør “Quick Fix” på den første feilen med “gold” så får feltet type Object istedenfor Actor).

Det denne metoden gjør er at om dragen ikke allerede har funnet gull, så vil den ta i mot gullet og holde på det i et felt.

Reformater fila med Ctrl-Shift-f for å fikse innrykket til nest siste klammeparantes. Lagre alle filer.

Endre metoden “strength” til:

@Override
public double strength() {
        double myStrength = 0.25;
        if (null == gold) {
                return myStrength;
        }

        return myStrength + gold.strength();
}

Denne metoden gir styrken til dragen. Uten gull så er styrken 0.25. Med gull så er styrken til dragen 0.25 pluss styrken til gullet.

Vi vil også gjøre en siste ting: vi vil at dragen skal slutte å flytte seg når den har funnet gull.

I Dragon, endre metoden “move”, til:

@Override
public void move() {
        // A dragon that has found gold stays with it.
        if (null == gold) {
                super.move();
        }
}

Tid for troll

Monster nummer to er litt av et troll.

I “Package Explorer”, høyreklikk på no.example.monstersim, og velg

New -> Class

I dialogen “New Java Class”

  • I Name: skriv
    Troll
    
  • I Superclass: skriv
    Monster
    

Klikk på “OK”-knappen.

Endre “describe”-metoden til å se sånn ut:

@Override
public String describe() {
        return "A warty and ugly troll";
}

Velg fra menyen

Source -> Generate Constructors from Superclass...

I dialogen “Generate Constructors from Superclass”, trykk på “OK”-knappen.

Endre den nylagde konstruktøren til å se sånn ut:

public Troll() {
        super();
        setName("Troll");
}

Velg fra menyen

Source -> Override/Implement Methods...

I dialogen “Override/Implement Methods”

  • Lukk “Monster”
  • Åpne “Actor”
  • Kryss av for “strength()”

Klikk på “OK”-knappen.

Endre metoden “strength” til dette:

@Override
public double strength() {
        return 0.4;
}

Gå for Gull

I “Package Explorer”, høyreklikk på no.example.monstersim, og velg

New -> Class

I dialogen “New Java Class”

  • I Name: skriv
    Gold
    
  • I Superclass: skriv
    Actor
    

Klikk på “OK”-knappen.

Endre “describe”-metoden til å se sånn ut:

@Override
public String describe() {
        return "A mound of gold";
}

Velg fra menyen

Source -> Generate Constructors from Superclass...

I dialogen “Generate Constructors from Superclass”, trykk på “OK”-knappen.

Endre den nylagde konstruktøren til å se sånn ut:

public Gold() {
        super();
        setName("Gold");
}

Velg fra menyen

Source -> Override/Implement Methods...

I dialogen “Override/Implement Methods”

  • Kryss av for “pickedUpBy()”
  • Kryss av for “strength()”

Klikk på “OK”-knappen.

Endre metoden “strength” til:

@Override
public double strength() {
        return 0.5;
}

Endre metoden “pickedUpBy” til:

@Override
public void pickedUpBy(Actor thePicker) {
        thePicker.takeMe(this);
}

Dragon kaller “pickedUpBy” på alle andre Actors i det rommet den befinner seg i. Om den kaller “pickedUpBy” på et Troll, så skjer ingenting fordi da blir den tomme metoden i Actor kalt.

Men om den kaller “pickedUpBy” på Gold, så blir metoden over kalt, og den vil da kalle “takeMe” i implementasjonen i Dragon.

Merk at ingen monstere implementerer “pickedUpBy” så de vil være upåvirket av kall til denne metoden (kallene vil gå til den tomme implementasjonen i Actor).

Tilsvarende implementerer ikke Gold “fightMe” og vil derfor være upåvirket av forsøk på å starte en slåsskamp.

Første kjøring

Nå gjenstår bare en kodebit. Gå til Simulator.java og finn metoden “setupActors” og endre den til dette:

private List<Actor> setupActors() {
        ArrayList<Actor> actors = new ArrayList<Actor>();
        actors.add(new Dragon());
        actors.add(new Gold());
        actors.add(new Troll());
        return actors;
}

Bruk “Quick Fix” på feilen i ArrayList og velg:

Import 'ArrayList' (java.util)

Lagre alle filer.

Så kan simuleringen kjøres. Bruk Ctrl-F6 til å gå til Monstersim.java. Kjør fra menyen:

Run -> Run As -> Java Application

(påfølgende kjøringer kan gjøres med Ctrl-F11)

Da vil en utskrift av en simulering komme i Console-vinduet i eclipse. Her er et eksempel på ei simulering:

Simulating a 3x3 cave:
Room at (0,2) contains:
 A dragon with green shimmering scales
Room at (1,1) contains:
 A warty and ugly troll
Room at (2,1) contains:
 A mound of gold
Simulation step: 1
 Dragon moves from Room at (0,2) to Room at (1,2)
 Troll moves from Room at (1,1) to Room at (1,2)
 Dragon sees the following actors: 
  A warty and ugly troll
 Dragon fights Troll!
 Troll sees the following actors: 
  A dragon with green shimmering scales
 Troll fights Dragon!
 Arrrrgh! Dragon dies!
Dragon is dead.  Ending simulation!

Merk: Dobbeltklikk på Console-tabben for å la Console fylle hele Eclipse-vinduet. Dobbelt-klikk på Console-tabben igjen, for å ta den ned til normal størrelse. Denne oppførselen gjelder alle vindu i Eclipse. Tastatursnarveien til denne kommandoen er Ctrl-m.

En oppsummering

Noen ting å merke seg:

  • Dette programmet ble skrevet fra toppen og nedover, fordi vi visste hvor vi skulle. Det er som regel ikke tilfelle. Som regel starter man med de objektene man vet skal være med (i dette tilfelle i første omgang Dragon, Troll og Gold, og kanskje Room), og så begynner man å lage metoder for det man vet de skal gjøre: flytte på seg, se seg om, ta opp gjenstander, slåss. Så leter man etter felles ting med gjenstandene og lager super-typer (Monster for både Dragon og Troll og Actor for typene til alle objektene som skal delta i simuleringen)
  • Mesteparten av koden er rammeverk. Det er først når man begynner på subtypene til Actor at det kommer ting som faktisk har med simuleringen å gjøre
  • Det er kanskje litt rart at gullet er en Actor, ettersom det ikke kan røre på seg? Forklaringen er at det er heller navnet Actor som ikke passer. Alle ting som skal delta i simuleringen bør ha en felles type. Men AThingThatParticipatesInSimulation virket litt langt…
  • Det er kanskje litt rart at Room ikke er en Actor, når Gold er det…? Svaret er at i en annen sammenheng og en annen simulering så kunne det vært det. Men i den modellen her så er rom posisjonen til objektet
  • Simuleringen er ikke så langt unna starten på et tekstbasert adventure-spill. Det som trengs er
    • at den ytterste løkka i simuleringsmotoren ikke går et endelig antall steg, men at den avbrytes, f.eks. når spilleren avslutter
    • å lage en Actor, som på Actor.act() vil presentere spilleren med en beskrivelse av rommet han befinner seg i, og et spørsmål om hva han vil gjøre (gå? Slåss? Plukke opp en gjenstand?)

Ting å prøve:

  • Øke størrelsen på grotta? 3×3 er litt lite. Jeg gjorde den så liten for at monstrene skulle ha større sannsynlighet for å støte på hverandre i løpet av 10 simuleringssteg, som er ca. en skjermside
  • En morsommere grotte med mer tilfeldige koblinger?
  • Trollet kan kanskje også finne ting som øker styrken? En klubbe, f.eks.?
  • Flere monstre?
  • Flere gjenstander? (isåfall blir protokollen for å finne gull mer komplisert)
  • En rikere interaksjonsprotokoll? Idag har man mulighet for å slåss (“fightMe”) og å plukke opp ting “pickedUpBy”/”takeMe”. Er det flere ting man ønsker å gjøre?
  • Mer intelligens i monstrene? Kanskje drager og troll kan lukte gull, og at dragene vil søke mot gull, mens trollene vil unnvike det (kan ikke være absolutt, men kanskje sannsynligheten for å gå i retning av gull kan økes, eller senkes?)
  • Bør “look” være en egen operasjon som kjøres av simulatoren, på linje med “move” og “act”? Det vil gjøre det enklere å lage mer intelligens i Actorene
  • Spørsmål: hvorfor kjøres “move” separat, og ikke som en del av “act”?

Debugging i eclipse

English summary: This article is in Norwegian and describes debugging of Java code in the Eclipse Java IDE.

Debugging (“avlusing”) I eclipse, betyr å stoppe et program som kjører og se hvilke verdier variabler har. Å debugge Hello.java som det var, er ikke spesielt interessant. Det er f.eks. ingen variabler å kikke på utenom “args”, som her ikke inneholder noe interessant.

Om du endrer programmet til noe sånt, blir det litt mer interessant (du kan bruke Ctrl-SPC for å ekspandere og skrive ting, eller du kan bare lime inn koden som den står i eksempelet):

package no.example.hello;

public class Hello {

        /**
         * @param args
         */
        public static void main(String[] args) {
                int numberOfTimesToPrintMessage = args.length;
                for (int i=0; i<numberOfTimesToPrintMessage; i++) {
                    System.out.println("Hello world!");
                }
        }
}

Linja med “for” betyr at programmet skal gå i ei løkke et visst antall ganger og gjøre det som står inne i løkka hver av gangene.

Om du nå prøver å kjøre programmet, så ser du at ingenting kommer ut i Console-vinduet. Det var merkelig! På tide å debugge!

Høyreklikk i margen på linja

System.out.println("Hello world!");

og velg “Toggle breakpoint”.

Velg så fra toppmenyen

Run -> Debug As -> Java Application

Programmet vil kjøre som før, og ingenting vil fortsatt skrives ut.

Sett et brekkpunkt i linja med

for (int i=0; i<numberOfTimesToPrintMessages; i++) {

og trykk på F11 for å debugge på nytt.

Du vil få opp en dialog, som heter “Confirm Perspective Switch”. Bare velg “Remember my decision”, klikk på “Yes”-knappen og fortsett.

Vinduene i eclipse vil rearrangere seg, men kildekoden til Hello.java vil fortsatt være synlig. I et vindu som heter “Variables” så vil du se at “numberOfTimesToPrintMessages” har verdien “0”. Aha!

Koden

for (int i=0; i<numberOfTimesToPrintMessages; i++) {

betyr: la variabelen i ha verdiene fra 0 og til numberOfTimesToPrintMessages – 1, og for hver ny verdi av i, utfør koden mellom “{” og “}”.

Variabelen numberOfTimesToPrintMessages er definert sånn:

int numberOfTimesToPrintMessage = args.length;

dvs. antall kommandolinjeargument som er gitt til programmet.

Trykk “F8” for å la programmet fortsette. Programmet vil avslutte uten at noe blir skrevet ut.

For å komme tilbake til oppsettet du bruke for å lage Hello.java, velg fra menyen:

Window -> Openm Perspective -> Java

I vinduet “Package Explorer”, høyreklikk på “Hello.java” og velg “Properties”.

Velg “Run/Debug Settings”, velg “Hello” og klikk på “Edit…”.

I “Edit Configuration”-dialogen, velg “Arguments”-tabben, og i “Program Arguments”, legg inn:

first second third fourth

Kjør så programmet igjen

Run -> Run

og observer at Console nå får fire utgaver av hello world.

Noen øvelser: debug og se hvordan variablene endrer seg. Du kan bruke F8 for å la programmet kjøre til neste brekkpunkt.

En ting du kan gjøre er å bruke verdiene fra argumentene i utskriftene og så endre argumentene i Properties og se hvordan det endrer kjøringen av programmet.

Installere EGit i eclipse

English summary: This article is in Norwegian and describes installation of the EGit git version control plugin, in the Eclipse Java IDE.

Et “versjonskontrollsystem” lar programmereren lagre unna noe som fungerer for å kunne gå tilbake til det om det nye man prøver på feiler.

Denne typen system kan også brukes til å dele kode mellom maskiner og mennesker. Systemet som følger med eclipse heter CVS, og har vært en tro tjener i mange år.

Men jeg, og mange med meg, bruker et nyere og smartere system som heter “git”. For å installere støtte for git i eclipse:

  • Start eclipse
  • Velg fra menyen på toppen av eclipse:
    Help -> Install New Software...
    
  • I “Install”-dialogen:
    • I “Work with: “, velg “–All Available Sites–”
    • I ruta der det står “type filter text”, skriv:
      egit
      
    • Etter litt tid vil treeviewet under ha:
      • Collaboration
        • Eclipse EGit
        • Eclipse EGit Mylyn GitHub Feature
        • Eclipse EGit Mylyn
    • Kryss av “Eclipse EGit” og klikk på “Next>”-knappen
    • Klikk på “Next>”-knappen en gang til
    • Velg “I accept the term of the license agreement” og klikk på “Finish”-knappen
  • En dialog vil vises som spør om du skal ta omstart på eclipse: ta omstart på eclipse

Etter at eclipse har startet på nytt vil git-støtten til eclipse være installert.