The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
# ====================================================================
#    Licensed to the Apache Software Foundation (ASF) under one
#    or more contributor license agreements.  See the NOTICE file
#    distributed with this work for additional information
#    regarding copyright ownership.  The ASF licenses this file
#    to you under the Apache License, Version 2.0 (the
#    "License"); you may not use this file except in compliance
#    with the License.  You may obtain a copy of the License at
#
#      http://www.apache.org/licenses/LICENSE-2.0
#
#    Unless required by applicable law or agreed to in writing,
#    software distributed under the License is distributed on an
#    "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
#    KIND, either express or implied.  See the License for the
#    specific language governing permissions and limitations
#    under the License.
# ====================================================================

require 'etc'
require 'fileutils'

module SvnTestUtil
  module Windows
    module Svnserve
      def service_name
        "test-svn-server--port-#{@svnserve_port}"
      end

      class << self
        def escape_value(value)
          escaped_value = value.gsub(/"/, '\\"') # "
          "\"#{escaped_value}\""
        end
      end

      def service_control(command, args={})
        args = args.collect do |key, value|
          "#{key}= #{Svnserve.escape_value(value)}"
        end.join(" ")
        result = `sc #{command} #{service_name} #{args}`
        if result.match(/FAILED/)
          raise "Failed to #{command} #{service_name}: #{args}"
        end
        /^\s*STATE\s*:\s\d+\s*(.*?)\s*$/ =~ result
        $1
      end

      def grant_everyone_full_access(dir)
        dir = dir.tr(File::SEPARATOR, File::ALT_SEPARATOR)
        `cacls #{Svnserve.escape_value(dir)} /T /E /P Everyone:F`
      end

      def service_exists?
        begin
          service_control("query")
          true
        rescue
          false
        end
      end

      def service_stopped?
        "STOPPED" == service_control("query") rescue true
      end

      def setup_svnserve
        @svnserve_port = @svnserve_ports.last
        @repos_svnserve_uri = "svn://#{@svnserve_host}:#{@svnserve_port}"
        grant_everyone_full_access(@full_repos_path)

        @@service_created ||= begin
          @@service_created = true
          service_control('stop') unless service_stopped?
          service_control('delete') if service_exists?

          svnserve_dir = File.expand_path("svnserve")
          FileUtils.mkdir_p(svnserve_dir)
          at_exit do
            service_control('stop') unless service_stopped?
            service_control('delete') if service_exists?
            FileUtils.rm_rf(svnserve_dir)
          end
          trap("INT") do
            service_control('stop') unless service_stopped?
            service_control('delete') if service_exists?
            FileUtils.rm_rf(svnserve_dir)
          end

          config = SetupEnvironment.gen_make_opts
          apr_version_include = Pathname.new(config["--with-apr"])  +
              'include' + 'apr_version.h'
          %r'^\s*#define\s+APR_MAJOR_VERSION\s+(\d+)' =~ apr_version_include.read
          apr_major_version = $1 == '0' ? '' : "-#{$1}"

          cwd = Dir.getwd
          targets = %W(svnserve.exe libsvn_subr-1.dll libsvn_repos-1.dll
                       libsvn_fs-1.dll libsvn_delta-1.dll
                       libaprutil#{apr_major_version}.dll
                       libapr#{apr_major_version}.dll
                       libapriconv#{apr_major_version}.dll
                       libdb??.dll libdb??d.dll)
          ENV["PATH"].split(";").each do |path|

            # Change the cwd to path, but ignore non-existent paths.
            begin
              Dir.chdir(path)
            rescue Errno::ENOENT
              next
            end

            found_targets = []
            targets.each do |target|
              matching_paths = Dir.glob(target)
              matching_paths.each do |target_path|
                target_path = File.join(path.tr('\\', '/'), target_path)
                if File.exists?(target_path)
                  found_targets << target
                  retried = 0
                  begin
                    FileUtils.cp(target_path, svnserve_dir)
                  rescue Errno::EACCES
                    # On Windows the tests frequently fail spuriously with a
                    # 'Errno::EACCES: Permission denied - svnserve.exe' error.
                    # Sleeping for a few seconds avoids this.
                    if retried > 5
                      # Give up!
                      raise
                    else
                      # Wait a sec...
                      sleep(1)
                      retried += 1
                      retry
                    end
                  end
                end
              end
            end
            targets -= found_targets
            break if targets.empty?
          end
          Dir.chdir(cwd)
          # Remove optional targets instead of raising below.  If they are really
          # needed, svnserve won't start anyway.
          targets -= %W[libapriconv#{apr_major_version}.dll]
          # Ditto these four, since svnserve.exe might be a static build.
          targets -= %W[libsvn_subr-1.dll]
          targets -= %W[libsvn_repos-1.dll]
          targets -= %W[libsvn_fs-1.dll]
          targets -= %W[libsvn_delta-1.dll]

          unless targets.empty?
            raise "can't find libraries to work svnserve: #{targets.join(' ')}"
          end

          grant_everyone_full_access(svnserve_dir)

          svnserve_path = File.join(svnserve_dir, "svnserve.exe")
          svnserve_path = svnserve_path.tr(File::SEPARATOR,
                                           File::ALT_SEPARATOR)
          svnserve_path = Svnserve.escape_value(svnserve_path)

          root = @full_repos_path.tr(File::SEPARATOR, File::ALT_SEPARATOR)

          args = ["--service", "--root", Svnserve.escape_value(root),
                  "--listen-host", @svnserve_host,
                  "--listen-port", @svnserve_port]
          user = ENV["USERNAME"] || Etc.getlogin
          service_control('create',
                          [["binPath", "#{svnserve_path} #{args.join(' ')}"],
                           ["DisplayName", service_name],
                           ["type", "own"]])
        end
        service_control('start')
        true
      end

      def teardown_svnserve
        service_control('stop') unless service_stopped?
      end

      def add_pre_revprop_change_hook
        File.open("#{@repos.pre_revprop_change_hook}.cmd", "w") do |hook|
          hook.print <<-HOOK
set REPOS=%1
set REV=%2
set USER=%3
set PROPNAME=%4
if "%PROPNAME%" == "#{Svn::Core::PROP_REVISION_LOG}" if "%USER%" == "#{@author}" exit 0
exit 1
          HOOK
        end
      end
    end

    module SetupEnvironment
      def setup_test_environment(top_dir, base_dir, ext_dir)
        @@top_dir = top_dir

        build_type = ENV["BUILD_TYPE"] || "Release"

        FileUtils.mkdir_p(ext_dir)

        relative_base_dir =
          base_dir.sub(/^#{Regexp.escape(top_dir + File::SEPARATOR)}/, '')
        build_base_dir = File.join(top_dir, build_type, relative_base_dir)

        dll_dir = File.expand_path(build_base_dir)
        subversion_dir = File.join(build_base_dir, "..", "..", "..")
        subversion_dir = File.expand_path(subversion_dir)

        util_name = "util"
        build_conf = File.join(top_dir, "build.conf")
        File.open(File.join(ext_dir, "#{util_name}.rb" ), 'w') do |util|
          setup_dll_wrapper_util(dll_dir, util)
          add_depended_dll_path_to_dll_wrapper_util(top_dir, build_type, util)
          add_svn_dll_path_to_dll_wrapper_util(build_conf, subversion_dir, util)
          setup_dll_wrappers(build_conf, ext_dir, dll_dir, util_name) do |lib|
            svn_lib_dir = File.join(subversion_dir, "libsvn_#{lib}")
            util.puts("add_path.call(#{svn_lib_dir.dump})")
          end

          svnserve_dir = File.join(subversion_dir, "svnserve")
          util.puts("add_path.call(#{svnserve_dir.dump})")
        end
      end

      def gen_make_opts
        @gen_make_opts ||= begin
          lines = []
          gen_make_opts = File.join(@@top_dir, "gen-make.opts")
          lines =
            File.read(gen_make_opts).lines.to_a if File.exists?(gen_make_opts)
          config = Hash.new do |hash, key|
            if /^--with-(.*)$/ =~ key
              hash[key] = File.join(@@top_dir, $1)
            end
          end

          lines.each do |line|
            name, value = line.chomp.split(/\s*=\s*/, 2)
            if value
              config[name] = Pathname.new(value).absolute? ?
                value :
                File.join(@@top_dir, value)
            end
          end
          config
        end
      end
      module_function :gen_make_opts

      private
      def setup_dll_wrapper_util(dll_dir, util)
        libsvn_swig_ruby_dll_dir = File.join(dll_dir, "libsvn_swig_ruby")

        util.puts(<<-EOC)
paths = ENV["PATH"].split(';')
add_path = Proc.new do |path|
  win_path = path.tr(File::SEPARATOR, File::ALT_SEPARATOR)
  unless paths.include?(win_path)
    ENV["PATH"] = "\#{win_path};\#{ENV['PATH']}"
  end
end

add_path.call(#{dll_dir.dump})
add_path.call(#{libsvn_swig_ruby_dll_dir.dump})
EOC
      end

      def add_depended_dll_path_to_dll_wrapper_util(top_dir, build_type, util)
        [
         ["apr", build_type],
         ["apr-util", build_type],
         ["apr-iconv", build_type],
         ["berkeley-db", "bin"],
         ["libintl", "bin"],
         ["sasl", "lib"],
        ].each do |lib, sub_dir|
          lib_dir = Pathname.new(gen_make_opts["--with-#{lib}"])
          dll_dir = lib_dir + sub_dir
          dll_dir = dll_dir.expand_path
          util.puts("add_path.call(#{dll_dir.to_s.dump})")
        end
      end

      def add_svn_dll_path_to_dll_wrapper_util(build_conf, subversion_dir, util)
        File.open(build_conf) do |f|
          f.each do |line|
            if /^\[(libsvn_.+)\]\s*$/ =~ line
              lib_name = $1
              lib_dir = File.join(subversion_dir, lib_name)
              util.puts("add_path.call(#{lib_dir.dump})")
            end
          end
        end
      end

      def setup_dll_wrappers(build_conf, ext_dir, dll_dir, util_name)
        File.open(build_conf) do |f|
          f.each do |line|
            if /^\[swig_(.+)\]\s*$/ =~ line
              lib_name = $1
              File.open(File.join(ext_dir, "#{lib_name}.rb" ), 'w') do |rb|
                rb.puts(<<-EOC)
require File.join(File.dirname(__FILE__), #{util_name.dump})
require File.join(#{dll_dir.dump}, File.basename(__FILE__, '.rb')) + '.so'
EOC
              end

              yield(lib_name)
            end
          end
        end
      end
    end
  end
end