感謝のプログラミング 10000時間

たどり着いた結果(さき)は、感謝でした。

RedmineをEC2にインストールしてハマったこと。nokogiriとかbundle、passengerでエラー。

基本は以下のサイトに従うが、ところどころハマりポイントがあるので後世のためにメモを残す。

http://qiita.com/nakanishi-m/items/73ecefb5706381bf3c32
http://blog.redmine.jp/articles/3_3/install/centos/

ハマりポイント「sudo bundle install」を実行すると「sudo: bundle: command not found」が出る。

Check if the PATH has the same values both with and without sudo. Apparently it cannot find bundle just because it is not listed in PATH

PATHがsudoとsudo以外で同じか確認しなさい、とのことです。

確認方法は以下の通り。

echo 'echo $PATH' | sh
/usr/local/bin:/bin:/usr/bin:/usr/local/sbin:/usr/sbin:/sbin:/opt/aws/bin:/home/ec2-user/.local/bin:/home/ec2-user/bin

sudoのPATHは以下のコマンドで確認します。

$ echo 'echo $PATH' | sudo sh
/sbin:/bin:/usr/sbin:/usr/bin

それで、解決策の一つは、「~/.bashrc」に

alias sudo='sudo env PATH=$PATH'

を追記すること。

$ cat ~/.bashrc
# .bashrc

# Source global definitions
if [ -f /etc/bashrc ]; then
        . /etc/bashrc
fi

# User specific aliases and functions
alias sudo='sudo env PATH=$PATH'

上記を追記した上で、

source ~/.bashrc

で読み込みます。

すると、bundleコマンドが実行できました。

https://stackoverflow.com/questions/22456998/why-is-sudo-bundle-command-not-found

ハマリポイント2 bundle install中にnokogiriでエラー

An error occurred while installing nokogiri (1.8.0), and Bundler cannot continue.
Make sure that `gem install nokogiri -v '1.8.0'` succeeds before bundling.

警告に従って、

gem install nokogiri -v '1.8.0'

を実行すると以下のエラーが。

Nokogiriはいつもエラーの原因になってクソだということはわかっているが、こいつなんとかならないのかな。。

IMPORTANT NOTICE:

Building Nokogiri with a packaged version of libxml2-2.9.4
with the following patches applied:
        - 0001-Fix-comparison-with-root-node-in-xmlXPathCmpNodes.patch
        - 0002-Fix-XPointer-paths-beginning-with-range-to.patch
        - 0003-Disallow-namespace-nodes-in-XPointer-ranges.patch

Team Nokogiri will keep on doing their best to provide security
updates in a timely manner, but if this is a concern for you and want
to use the system library instead; abort this installation process and
reinstall nokogiri as follows:

    gem install nokogiri -- --use-system-libraries
        [--with-xml2-config=/path/to/xml2-config]
        [--with-xslt-config=/path/to/xslt-config]

If you are using Bundler, tell it to use the option:

    bundle config build.nokogiri --use-system-libraries
    bundle install

とりあえず警告どおりに以下を実行。

$ sudo bundle config build.nokogiri --use-system-libraries


bundleを実行すると以下のエラーが。

pkg-config could not be used to find libxml-2.0
Please install either `pkg-config` or the pkg-config gem per

    gem install pkg-config -v "~> 1.1.7"

pkg-config could not be used to find libxslt
Please install either `pkg-config` or the pkg-config gem per

    gem install pkg-config -v "~> 1.1.7"

pkg-config could not be used to find libexslt
Please install either `pkg-config` or the pkg-config gem per

    gem install pkg-config -v "~> 1.1.7"

このまま頑張ってもnokogiriを入れることはできなそうなので、以下のQiitaの記事を参考に別のアプローチを検討する。
http://qiita.com/nekakoshi/items/98bcdf0137e55a0b2395

すると、以下のようにyumでlibxml2を入れることで、nokogiriのインストールが解消した!

sudo yum install libxml2-devel libxslt-devel

以下のコマンドは成功する。

sudo  gem install nokogiri -- --use-system-libraries=true --with-xml2-include=/usr/include/libxml2/

ここまでやったらついに、

sudo bundle install --without development test postgresql sqlite --path vendor/bundle

が成功!!

redmineを見ようとしたら「You don't have permission to access /redmine/ on this server.」でアクセスできない

ブラウザで以下のようなメッセージが出ている。

Forbidden

You don't have permission to access /redmine/ on this server.

まずは/var/lib/redmine/log/production.logを確認する。
起動していない場合はPassengerに問題がある。
Passenger問題については後述。

chownでapacheに設定しているかも確認しておくこと。

redmineを見ようとしたらディレクトリのリストが見えてしまう

これもPassengerの設定が正しくできていないため。

なぜだ?と思って色々調べていたら、インストールコマンドで実行するとエラーが出ていた。

passenger-install-apache2-module --auto

結局メモリが足りていなかったことが判明。

_HEADER="<hash_map>" -DHASH_MAP_CLASS="hash_map" -DHASH_FUN_H="<hash_fun.h>" -c src/agent/Core/CoreMain.cpp
virtual memory exhausted: Cannot allocate memory
rake aborted!

やっぱり起動できていない。

$ passenger-status
ERROR: Phusion Passenger doesn't seem to be running. If you are sure that it is running, then the causes of this problem could be:

http://www.redmine.org/boards/2/topics/43395


こちらの記事ではスワップ領域を増やすやり方を採用していたけれど、僕は素直にEC2のインスタンスタイプを変更した。
http://qiita.com/abetd/items/43a06e7376ad7bedca6d

あとはPassengerの設定をちゃんとapacheのconfに反映させることかな。

手順をコピペしないで、

passenger-install-apache2-module --snippet

で出てきた値を使うこと。

LoadModule passenger_module /usr/local/share/ruby/gems/2.3/gems/passenger-5.1.5/buildout/apache2/mod_passenger.so
<IfModule mod_passenger.c>
  PassengerRoot /usr/local/share/ruby/gems/2.3/gems/passenger-5.1.5
  PassengerDefaultRuby /usr/bin/ruby2.3
</IfModule>


あとは、初期設定はこちら。
http://redmine.jp/tech_note/first-step/admin/

Jettyで超簡単にWebSocketサーバを作るサンプル

Jettyというサーブレットコンテナを使って、java -jar XXXX.jarで実行できるような軽いサーブレットを立ち上げ、
そこでWebSocket接続を待ち受けるサービスを立ち上げたいと思います。

今回のサンプルのディレクトリ構成は以下の通りです。

f:id:sho322:20170622224746j:plain


eclipseでMavenプロジェクトを作り、以下のようにpom.xmlを編集してください。

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>

  <groupId>sample</groupId>
  <artifactId>websocket</artifactId>
  <version>0.0.1-SNAPSHOT</version>
  <packaging>jar</packaging>

  <name>websocket</name>
  <url>http://maven.apache.org</url>

  <properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <jetty.version>9.2.11.v20150529</jetty.version>
  </properties>
  <build>
      <plugins>
          <plugin>
              <groupId>org.eclipse.jetty</groupId>
              <artifactId>jetty-maven-plugin</artifactId>
              <version>${jetty.version}</version>
          </plugin>
      </plugins>
  </build>
  <dependencies>
    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>3.8.1</version>
      <scope>test</scope>
    </dependency>
    <!--Jetty dependencies start here -->
    <dependency>
        <groupId>org.eclipse.jetty</groupId>
        <artifactId>jetty-server</artifactId>
        <version>${jetty.version}</version>
    </dependency>

    <dependency>
        <groupId>org.eclipse.jetty</groupId>
        <artifactId>jetty-servlet</artifactId>
        <version>${jetty.version}</version>
    </dependency>

	<!-- https://mvnrepository.com/artifact/org.eclipse.jetty/jetty-webapp -->
	<dependency>
	    <groupId>org.eclipse.jetty</groupId>
	    <artifactId>jetty-webapp</artifactId>
	    <version>${jetty.version}</version>
	</dependency>

    <!--Jetty Websocket API server side dependency -->
    <dependency>
        <groupId>org.eclipse.jetty.websocket</groupId>
        <artifactId>websocket-server</artifactId>
        <version>${jetty.version}</version>
    </dependency>

  </dependencies>
</project>

Jettyを立ち上げるためのスタートポイントを作ります。
ここにはmainメソッドがあります。

JettyLauncherはGitBucketのソースを参考にしました。
https://github.com/gitbucket/gitbucket

package sample.websocket;

import java.net.InetSocketAddress;
import java.net.URL;
import java.security.ProtectionDomain;

import org.eclipse.jetty.server.ConnectionFactory;
import org.eclipse.jetty.server.Connector;
import org.eclipse.jetty.server.Handler;
import org.eclipse.jetty.server.HttpConnectionFactory;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.handler.StatisticsHandler;
import org.eclipse.jetty.webapp.WebAppContext;

public class JettyLauncher {
    public static void main(String[] args) throws Exception {
        System.out.println("Hello Jetty!");
        System.out.println("Jetty is running -> localhost:8888");

        String host = null;
        int port = 8888;
        String contextPath = "/";
        boolean forceHttps = false;
        InetSocketAddress address = null;

        if (host != null) {
        	address = new InetSocketAddress(host, port);
        } else {
        	address = new InetSocketAddress(port);
        }

        Server server = new Server(address);

        for (Connector connector : server.getConnectors()) {
            for (ConnectionFactory factory : connector.getConnectionFactories()) {
                if (factory instanceof HttpConnectionFactory) {
                    ((HttpConnectionFactory) factory).getHttpConfiguration().setSendServerVersion(false);
                }
            }
        }
        WebAppContext context = new WebAppContext();
        ProtectionDomain domain = JettyLauncher.class.getProtectionDomain();
        URL location = domain.getCodeSource().getLocation();

        context.setContextPath(contextPath);
        context.setDescriptor(location.toExternalForm() + "/WEB-INF/web.xml");
        context.setServer(server);
        context.setWar(location.toExternalForm());
        if (forceHttps) {
            context.setInitParameter("org.scalatra.ForceHttps", "true");
        }

        Handler handler = addStatisticsHandler(context);

        server.setHandler(handler);
        server.setStopAtShutdown(true);
        server.setStopTimeout(7000);
        server.start();
        server.join();
    }

    private static Handler addStatisticsHandler(Handler handler) {

        final StatisticsHandler statisticsHandler = new StatisticsHandler();
        statisticsHandler.setHandler(handler);
        return statisticsHandler;
    }
}

上記でコンテクストパスを設定しているため、/WEB-INF/web.xmlを以下のように記載します。

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://java.sun.com/xml/ns/javaee"
      xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
      xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
      version="3.0">

  <servlet>
    <servlet-name>WebSocketServlet</servlet-name>
    <servlet-class>sample.websocket.servlet.ToUpperWebSocketServlet</servlet-class>
  </servlet>

  <servlet-mapping>
    <servlet-name>WebSocketServlet</servlet-name>
    <url-pattern>/*</url-pattern>
  </servlet-mapping>

</web-app>

次に、WebSocketを通信を行うサービスを作ります。

package sample.websocket.service;

import java.io.IOException;

import org.eclipse.jetty.websocket.api.Session;
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketClose;
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketConnect;
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketMessage;
import org.eclipse.jetty.websocket.api.annotations.WebSocket;

@WebSocket
public class ToUpperWebSocketService {

	@OnWebSocketMessage
	public void onText(Session session, String message) throws IOException {
		System.out.println("Message Received:" + message);
		if (session.isOpen()) {
			String response = message.toUpperCase();
			session.getRemote().sendString(response);
		}
	}

	@OnWebSocketConnect
	public void onConnect(Session session) throws IOException {
		System.out.println(session.getRemoteAddress().getHostString() + "connected!");
	}

	@OnWebSocketClose
	public void onClose(Session session, int status, String reason) {
		System.out.println(session.getRemoteAddress().getHostString() + "closed!");
	}
}

受け取った文字列を大文字にして返します。

そして、WebSocketのリクエストを受け取るサーブレットを作ります。
このサーブレットはweb.xmlに定義します。

package sample.websocket.servlet;

import javax.servlet.annotation.WebServlet;

import org.eclipse.jetty.websocket.servlet.WebSocketServlet;
import org.eclipse.jetty.websocket.servlet.WebSocketServletFactory;

import sample.websocket.service.ToUpperWebSocketService;

@WebServlet(urlPatterns="/toUpper")
public class ToUpperWebSocketServlet extends WebSocketServlet{
	@Override
	public void configure(WebSocketServletFactory factory) {
		System.out.println("register ToUpperWebSocket class to WebSocketFactory...");
		factory.register(ToUpperWebSocketService.class);

	}
}


これでサーバサイドは完成です。
次にクライアント側のソースコードを作りましょう。

以下のサイトを参考にしています。
https://examples.javacodegeeks.com/enterprise-java/jetty/jetty-websocket-example/

<html>
<body>
	<div>
		<input type="text" id="input" />
	</div>
	<div>
		<input type="button" id="connectBtn" value="CONNECT"
			onclick="connect()" /> <input type="button" id="sendBtn"
			value="SEND" onclick="send()" disabled="true" />
	</div>
	<div id="output">
		<p>Output</p>
	</div>
</body>

<script type="text/javascript">
	var webSocket;
	var output = document.getElementById("output");
	var connectBtn = document.getElementById("connectBtn");
	var sendBtn = document.getElementById("sendBtn");

	function connect() {
		// open the connection if one does not exist
		if (webSocket !== undefined
				&& webSocket.readyState !== WebSocket.CLOSED) {
			return;
		}
		// Create a websocket
		webSocket = new WebSocket("ws://localhost:8888/toUpper");

		webSocket.onopen = function(event) {
			updateOutput("Connected!");
			connectBtn.disabled = true;
			sendBtn.disabled = false;

		};

		webSocket.onmessage = function(event) {
			updateOutput(event.data);
		};

		webSocket.onclose = function(event) {
			updateOutput("Connection Closed");
			connectBtn.disabled = false;
			sendBtn.disabled = true;
		};
	}

	function send() {
		var text = document.getElementById("input").value;
		webSocket.send(text);
	}

	function closeSocket() {
		webSocket.close();
	}

	function updateOutput(text) {
		output.innerHTML += "<br/>" + text;
	}
</script>
</html>

eclipse上からjavaを実行し、サーバを起動します。
それから、sample.htmlのようなファイルに上記のhtmlを保存し、それをChromeで開くと、以下のような画面が表示されます。
f:id:sho322:20170622224758j:plain

CONNECTしてからSENDを押すと、大文字になった文字が返ってきます。

まずここまでで、超かんたんなWebSocketのサンプルができました。
これを使って、今度はサーバからのプッシュ通信を実現してみましょう。

STSでSpring BootでWebアプリケーションを作ってみる。


Spring BootではSpring Initializerという雛形生成Webサービスが用意されています。
これを利用して簡単なWebアプリを作ってみます。

http://start.spring.io/

「Search for dependencies」に「Web」と入力します。

「Generate Project」をクリックすると、demo.zipがダウンロードされます。

Eclipseを立ち上げ、
New > Import > Maven >Existing Maven Projectsを選択して[Next]

Root Directory: にさっきのdemoを解凍したものを選択します。
Add projects to working setにチェックを入れてfinishでOKです。

ちなみにですが、これらの作業はSTSでやりましょう。
http://macappstore.org/sts/

普通のEclipseだとサーバを動かすのが面倒だったりしますが、STSだとdemoのDemoApplicationで普通にJavaを動かすだけで、とりあえずtomcatが動いてページが表示されます。

STSをMacにインストールするのは、

brew cask install sts

でOK。

インストールが終わったら、Command+Spaceで出てきたSpotlightに「STS」と入力。
Spring Tool Suiteが起動します。

DemoApplicationをちょっと書き換えるだけで、Hello World画面を表示することができます。

package com.example;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@SpringBootApplication
@RestController
public class DemoApplication {

	@RequestMapping("/")
	String hell() {
		return "Hello! Spring Boot!";
	}
	
	public static void main(String[] args) {
		SpringApplication.run(DemoApplication.class, args);
	}
}

この修正をした上で、Run Javaを実行。

すると、localhost:8080

Hello! Spring Boot!


という文言が表示されます。