// Copyright (c) 2011 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "chrome/common/service_process_util.h"

#include "base/basictypes.h"

#if !defined(OS_MACOSX)
#include "base/at_exit.h"
#include "base/command_line.h"
#include "base/memory/scoped_ptr.h"
#include "base/process_util.h"
#include "base/string_util.h"
#include "base/test/multiprocess_test.h"
#include "base/test/test_timeouts.h"
#include "base/threading/thread.h"
#include "base/utf_string_conversions.h"
#include "chrome/common/chrome_switches.h"
#include "chrome/common/chrome_version_info.h"
#include "testing/multiprocess_func_list.h"

#if defined(OS_WIN)
#include "base/win/win_util.h"
#endif

#if defined(OS_LINUX)
#include <glib.h>
#include "chrome/common/auto_start_linux.h"
#endif

namespace {

bool g_good_shutdown = false;

void ShutdownTask(MessageLoop* loop) {
  // Quit the main message loop.
  ASSERT_FALSE(g_good_shutdown);
  g_good_shutdown = true;
  loop->PostTask(FROM_HERE, new MessageLoop::QuitTask());
}

}  // namespace

TEST(ServiceProcessUtilTest, ScopedVersionedName) {
  std::string test_str = "test";
  std::string scoped_name = GetServiceProcessScopedVersionedName(test_str);
  chrome::VersionInfo version_info;
  DCHECK(version_info.is_valid());
  EXPECT_TRUE(EndsWith(scoped_name, test_str, true));
  EXPECT_NE(std::string::npos, scoped_name.find(version_info.Version()));
}

class ServiceProcessStateTest : public base::MultiProcessTest {
 public:
  ServiceProcessStateTest();
  ~ServiceProcessStateTest();
  virtual void SetUp();
  base::MessageLoopProxy* IOMessageLoopProxy() {
    return io_thread_.message_loop_proxy();
  }
  void LaunchAndWait(const std::string& name);

 private:
  // This is used to release the ServiceProcessState singleton after each test.
  base::ShadowingAtExitManager at_exit_manager_;
  base::Thread io_thread_;
};

ServiceProcessStateTest::ServiceProcessStateTest()
    : io_thread_("ServiceProcessStateTestThread") {
}

ServiceProcessStateTest::~ServiceProcessStateTest() {
}

void ServiceProcessStateTest::SetUp() {
  base::Thread::Options options(MessageLoop::TYPE_IO, 0);
  ASSERT_TRUE(io_thread_.StartWithOptions(options));
}

void ServiceProcessStateTest::LaunchAndWait(const std::string& name) {
  base::ProcessHandle handle = SpawnChild(name, false);
  ASSERT_TRUE(handle);
  int exit_code = 0;
  ASSERT_TRUE(base::WaitForExitCode(handle, &exit_code));
  ASSERT_EQ(exit_code, 0);
}

TEST_F(ServiceProcessStateTest, Singleton) {
  ServiceProcessState state;
  ASSERT_TRUE(state.Initialize());
  LaunchAndWait("ServiceProcessStateTestSingleton");
}

TEST_F(ServiceProcessStateTest, ReadyState) {
  ASSERT_FALSE(CheckServiceProcessReady());
  ServiceProcessState state;
  ASSERT_TRUE(state.Initialize());
  ASSERT_TRUE(state.SignalReady(IOMessageLoopProxy(), NULL));
  LaunchAndWait("ServiceProcessStateTestReadyTrue");
  state.SignalStopped();
  LaunchAndWait("ServiceProcessStateTestReadyFalse");
}

TEST_F(ServiceProcessStateTest, AutoRun) {
  ServiceProcessState state;
  ASSERT_TRUE(state.AddToAutoRun());
  scoped_ptr<CommandLine> autorun_command_line;
#if defined(OS_WIN)
  std::string value_name = GetServiceProcessScopedName("_service_run");
  string16 value;
  EXPECT_TRUE(base::win::ReadCommandFromAutoRun(HKEY_CURRENT_USER,
                                                UTF8ToWide(value_name),
                                                &value));
  autorun_command_line.reset(new CommandLine(CommandLine::FromString(value)));
#elif defined(OS_LINUX)
#if defined(GOOGLE_CHROME_BUILD)
  std::string base_desktop_name = "google-chrome-service.desktop";
#else  // CHROMIUM_BUILD
  std::string base_desktop_name = "chromium-service.desktop";
#endif
  std::string exec_value;
  EXPECT_TRUE(AutoStart::GetAutostartFileValue(
      GetServiceProcessScopedName(base_desktop_name), "Exec", &exec_value));
  GError *error = NULL;
  gchar **argv = NULL;
  gint argc = 0;
  if (g_shell_parse_argv(exec_value.c_str(), &argc, &argv, &error)) {
    autorun_command_line.reset(new CommandLine(argc, argv));
    g_strfreev(argv);
  } else {
    ADD_FAILURE();
    g_error_free(error);
  }
#endif  // defined(OS_WIN)
  if (autorun_command_line.get()) {
    EXPECT_EQ(autorun_command_line->GetSwitchValueASCII(switches::kProcessType),
              std::string(switches::kServiceProcess));
  }
  ASSERT_TRUE(state.RemoveFromAutoRun());
#if defined(OS_WIN)
  EXPECT_FALSE(base::win::ReadCommandFromAutoRun(HKEY_CURRENT_USER,
                                                 UTF8ToWide(value_name),
                                                 &value));
#elif defined(OS_LINUX)
  EXPECT_FALSE(AutoStart::GetAutostartFileValue(
      GetServiceProcessScopedName(base_desktop_name), "Exec", &exec_value));
#endif  // defined(OS_WIN)
}

TEST_F(ServiceProcessStateTest, SharedMem) {
  std::string version;
  base::ProcessId pid;
#if defined(OS_WIN)
  // On Posix, named shared memory uses a file on disk. This file
  // could be lying around from previous crashes which could cause
  // GetServiceProcessPid to lie. On Windows, we use a named event so we
  // don't have this issue. Until we have a more stable shared memory
  // implementation on Posix, this check will only execute on Windows.
  ASSERT_FALSE(GetServiceProcessData(&version, &pid));
#endif  // defined(OS_WIN)
  ServiceProcessState state;
  ASSERT_TRUE(state.Initialize());
  ASSERT_TRUE(GetServiceProcessData(&version, &pid));
  ASSERT_EQ(base::GetCurrentProcId(), pid);
}

TEST_F(ServiceProcessStateTest, ForceShutdown) {
  base::ProcessHandle handle = SpawnChild("ServiceProcessStateTestShutdown",
                                          true);
  ASSERT_TRUE(handle);
  for (int i = 0; !CheckServiceProcessReady() && i < 10; ++i) {
    base::PlatformThread::Sleep(TestTimeouts::tiny_timeout_ms());
  }
  ASSERT_TRUE(CheckServiceProcessReady());
  std::string version;
  base::ProcessId pid;
  ASSERT_TRUE(GetServiceProcessData(&version, &pid));
  ASSERT_TRUE(ForceServiceProcessShutdown(version, pid));
  int exit_code = 0;
  ASSERT_TRUE(base::WaitForExitCodeWithTimeout(handle,
      &exit_code, TestTimeouts::action_max_timeout_ms()));
  base::CloseProcessHandle(handle);
  ASSERT_EQ(exit_code, 0);
}

MULTIPROCESS_TEST_MAIN(ServiceProcessStateTestSingleton) {
  ServiceProcessState state;
  EXPECT_FALSE(state.Initialize());
  return 0;
}

MULTIPROCESS_TEST_MAIN(ServiceProcessStateTestReadyTrue) {
  EXPECT_TRUE(CheckServiceProcessReady());
  return 0;
}

MULTIPROCESS_TEST_MAIN(ServiceProcessStateTestReadyFalse) {
  EXPECT_FALSE(CheckServiceProcessReady());
  return 0;
}

MULTIPROCESS_TEST_MAIN(ServiceProcessStateTestShutdown) {
  MessageLoop message_loop;
  message_loop.set_thread_name("ServiceProcessStateTestShutdownMainThread");
  base::Thread io_thread_("ServiceProcessStateTestShutdownIOThread");
  base::Thread::Options options(MessageLoop::TYPE_IO, 0);
  EXPECT_TRUE(io_thread_.StartWithOptions(options));
  ServiceProcessState state;
  EXPECT_TRUE(state.Initialize());
  EXPECT_TRUE(state.SignalReady(io_thread_.message_loop_proxy(),
                                NewRunnableFunction(&ShutdownTask,
                                                    MessageLoop::current())));
  message_loop.PostDelayedTask(FROM_HERE,
                               new MessageLoop::QuitTask(),
                               TestTimeouts::action_max_timeout_ms());
  EXPECT_FALSE(g_good_shutdown);
  message_loop.Run();
  EXPECT_TRUE(g_good_shutdown);
  return 0;
}

#else  // !OS_MACOSX

#include <CoreFoundation/CoreFoundation.h>

#include <launch.h>
#include <sys/stat.h>

#include "base/file_path.h"
#include "base/file_util.h"
#include "base/mac/mac_util.h"
#include "base/mac/scoped_cftyperef.h"
#include "base/memory/scoped_temp_dir.h"
#include "base/message_loop.h"
#include "base/stringprintf.h"
#include "base/sys_string_conversions.h"
#include "base/test/test_timeouts.h"
#include "base/threading/thread.h"
#include "chrome/common/launchd_mac.h"
#include "testing/gtest/include/gtest/gtest.h"

// TODO(dmaclach): Write this in terms of a real mock.
// http://crbug.com/76923
class MockLaunchd : public Launchd {
 public:
  MockLaunchd(const FilePath& file, MessageLoop* loop)
      : file_(file),
        message_loop_(loop),
        restart_called_(false),
        remove_called_(false),
        checkin_called_(false),
        write_called_(false),
        delete_called_(false) {
  }
  virtual ~MockLaunchd() { }

  virtual CFDictionaryRef CopyExports() OVERRIDE {
    ADD_FAILURE();
    return NULL;
  }

  virtual CFDictionaryRef CopyJobDictionary(CFStringRef label) OVERRIDE {
    ADD_FAILURE();
    return NULL;
  }

  virtual CFDictionaryRef CopyDictionaryByCheckingIn(CFErrorRef* error)
      OVERRIDE {
    checkin_called_ = true;
    CFStringRef program = CFSTR(LAUNCH_JOBKEY_PROGRAM);
    CFStringRef program_args = CFSTR(LAUNCH_JOBKEY_PROGRAMARGUMENTS);
    const void *keys[] = { program, program_args };
    base::mac::ScopedCFTypeRef<CFStringRef> path(
        base::SysUTF8ToCFStringRef(file_.value()));
    const void *array_values[] = { path.get() };
    base::mac::ScopedCFTypeRef<CFArrayRef> args(
        CFArrayCreate(kCFAllocatorDefault,
                      array_values,
                      1,
                      &kCFTypeArrayCallBacks));
    const void *values[] = { path, args };
    return CFDictionaryCreate(kCFAllocatorDefault,
                              keys,
                              values,
                              arraysize(keys),
                              &kCFTypeDictionaryKeyCallBacks,
                              &kCFTypeDictionaryValueCallBacks);
  }

  virtual bool RemoveJob(CFStringRef label, CFErrorRef* error) OVERRIDE {
    remove_called_ = true;
    message_loop_->PostTask(FROM_HERE, new MessageLoop::QuitTask);
    return true;
  }

  virtual bool RestartJob(Domain domain,
                          Type type,
                          CFStringRef name,
                          CFStringRef session_type) OVERRIDE {
    restart_called_ = true;
    message_loop_->PostTask(FROM_HERE, new MessageLoop::QuitTask);
    return true;
  }

  virtual CFMutableDictionaryRef CreatePlistFromFile(
      Domain domain,
      Type type,
      CFStringRef name) OVERRIDE {
    base::mac::ScopedCFTypeRef<CFDictionaryRef> dict(
        CopyDictionaryByCheckingIn(NULL));
    return CFDictionaryCreateMutableCopy(kCFAllocatorDefault, 0, dict);
  }

  virtual bool WritePlistToFile(Domain domain,
                                Type type,
                                CFStringRef name,
                                CFDictionaryRef dict) OVERRIDE {
    write_called_ = true;
    return true;
  }

  virtual bool DeletePlist(Domain domain,
                           Type type,
                           CFStringRef name) OVERRIDE {
    delete_called_ = true;
    return true;
  }

  bool restart_called() const { return restart_called_; }
  bool remove_called() const { return remove_called_; }
  bool checkin_called() const { return checkin_called_; }
  bool write_called() const { return write_called_; }
  bool delete_called() const { return delete_called_; }

 private:
  FilePath file_;
  MessageLoop* message_loop_;
  bool restart_called_;
  bool remove_called_;
  bool checkin_called_;
  bool write_called_;
  bool delete_called_;
};

class ServiceProcessStateFileManipulationTest : public ::testing::Test {
 protected:
  ServiceProcessStateFileManipulationTest()
      : io_thread_("ServiceProcessStateFileManipulationTest_IO") {
  }
  virtual ~ServiceProcessStateFileManipulationTest() { }

  virtual void SetUp() {
    base::Thread::Options options;
    options.message_loop_type = MessageLoop::TYPE_IO;
    ASSERT_TRUE(io_thread_.StartWithOptions(options));
    ASSERT_TRUE(temp_dir_.CreateUniqueTempDir());
    ASSERT_TRUE(MakeABundle(GetTempDirPath(),
                            "Test",
                            &bundle_path_,
                            &executable_path_));
    mock_launchd_.reset(new MockLaunchd(executable_path_, &loop_));
    scoped_launchd_instance_.reset(
        new Launchd::ScopedInstance(mock_launchd_.get()));
    ASSERT_TRUE(service_process_state_.Initialize());
    ASSERT_TRUE(service_process_state_.SignalReady(
        io_thread_.message_loop_proxy(),
        NULL));
    loop_.PostDelayedTask(FROM_HERE,
                          new MessageLoop::QuitTask,
                          TestTimeouts::action_max_timeout_ms());
  }

  bool MakeABundle(const FilePath& dst,
                   const std::string& name,
                   FilePath* bundle_root,
                   FilePath* executable) {
    *bundle_root = dst.Append(name + std::string(".app"));
    FilePath contents = bundle_root->AppendASCII("Contents");
    FilePath mac_os = contents.AppendASCII("MacOS");
    *executable = mac_os.Append(name);
    FilePath info_plist = contents.Append("Info.plist");

    if (!file_util::CreateDirectory(mac_os)) {
      return false;
    }
    const char *data = "#! testbundle\n";
    int len = strlen(data);
    if (file_util::WriteFile(*executable, data, len) != len) {
      return false;
    }
    if (chmod(executable->value().c_str(), 0555) != 0) {
      return false;
    }

    const char* info_plist_format =
      "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
      "<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n"
      "<plist version=\"1.0\">\n"
      "<dict>\n"
      "  <key>CFBundleDevelopmentRegion</key>\n"
      "  <string>English</string>\n"
      "  <key>CFBundleIdentifier</key>\n"
      "  <string>com.test.%s</string>\n"
      "  <key>CFBundleInfoDictionaryVersion</key>\n"
      "  <string>6.0</string>\n"
      "  <key>CFBundleExecutable</key>\n"
      "  <string>%s</string>\n"
      "  <key>CFBundleVersion</key>\n"
      "  <string>1</string>\n"
      "</dict>\n"
      "</plist>\n";
    std::string info_plist_data = base::StringPrintf(info_plist_format,
                                                     name.c_str(),
                                                     name.c_str());
    len = info_plist_data.length();
    if (file_util::WriteFile(info_plist, info_plist_data.c_str(), len) != len) {
      return false;
    }
    const UInt8* bundle_root_path =
        reinterpret_cast<const UInt8*>(bundle_root->value().c_str());
    base::mac::ScopedCFTypeRef<CFURLRef> url(
      CFURLCreateFromFileSystemRepresentation(kCFAllocatorDefault,
                                              bundle_root_path,
                                              bundle_root->value().length(),
                                              true));
    base::mac::ScopedCFTypeRef<CFBundleRef> bundle(
        CFBundleCreate(kCFAllocatorDefault, url));
    return bundle.get();
  }

  const MockLaunchd* mock_launchd() const { return mock_launchd_.get(); }
  const FilePath& executable_path() const { return executable_path_; }
  const FilePath& bundle_path() const { return bundle_path_; }
  const FilePath& GetTempDirPath() const { return temp_dir_.path(); }

  base::MessageLoopProxy* GetIOMessageLoopProxy() {
    return io_thread_.message_loop_proxy().get();
  }
  void Run() { loop_.Run(); }

 private:
  ScopedTempDir temp_dir_;
  MessageLoopForUI loop_;
  base::Thread io_thread_;
  FilePath executable_path_, bundle_path_;
  scoped_ptr<MockLaunchd> mock_launchd_;
  scoped_ptr<Launchd::ScopedInstance> scoped_launchd_instance_;
  ServiceProcessState service_process_state_;
};

void DeleteFunc(const FilePath& file) {
  EXPECT_TRUE(file_util::Delete(file, true));
}

void MoveFunc(const FilePath& from, const FilePath& to) {
  EXPECT_TRUE(file_util::Move(from, to));
}

void ChangeAttr(const FilePath& from, int mode) {
  EXPECT_EQ(chmod(from.value().c_str(), mode), 0);
}

class ScopedAttributesRestorer {
 public:
  ScopedAttributesRestorer(const FilePath& path, int mode)
      : path_(path), mode_(mode) {
  }
  ~ScopedAttributesRestorer() {
    ChangeAttr(path_, mode_);
  }
 private:
  FilePath path_;
  int mode_;
};

void TrashFunc(const FilePath& src) {
  FSRef path_ref;
  FSRef new_path_ref;
  EXPECT_TRUE(base::mac::FSRefFromPath(src.value(), &path_ref));
  OSStatus status = FSMoveObjectToTrashSync(&path_ref,
                                            &new_path_ref,
                                            kFSFileOperationDefaultOptions);
  EXPECT_EQ(status, noErr)  << "FSMoveObjectToTrashSync " << status;
}

TEST_F(ServiceProcessStateFileManipulationTest, DeleteFile) {
  GetIOMessageLoopProxy()->PostTask(
      FROM_HERE,
      NewRunnableFunction(&DeleteFunc, executable_path()));
  Run();
  ASSERT_TRUE(mock_launchd()->remove_called());
  ASSERT_TRUE(mock_launchd()->delete_called());
}

TEST_F(ServiceProcessStateFileManipulationTest, DeleteBundle) {
  GetIOMessageLoopProxy()->PostTask(
      FROM_HERE,
      NewRunnableFunction(&DeleteFunc, bundle_path()));
  Run();
  ASSERT_TRUE(mock_launchd()->remove_called());
  ASSERT_TRUE(mock_launchd()->delete_called());
}

TEST_F(ServiceProcessStateFileManipulationTest, MoveBundle) {
  FilePath new_loc = GetTempDirPath().AppendASCII("MoveBundle");
  GetIOMessageLoopProxy()->PostTask(
      FROM_HERE,
      NewRunnableFunction(&MoveFunc, bundle_path(), new_loc));
  Run();
  ASSERT_TRUE(mock_launchd()->restart_called());
  ASSERT_TRUE(mock_launchd()->write_called());
}

TEST_F(ServiceProcessStateFileManipulationTest, MoveFile) {
  FilePath new_loc = GetTempDirPath().AppendASCII("MoveFile");
  GetIOMessageLoopProxy()->PostTask(
      FROM_HERE,
      NewRunnableFunction(&MoveFunc, executable_path(), new_loc));
  Run();
  ASSERT_TRUE(mock_launchd()->remove_called());
  ASSERT_TRUE(mock_launchd()->delete_called());
}

TEST_F(ServiceProcessStateFileManipulationTest, TrashBundle) {
  FSRef bundle_ref;
  ASSERT_TRUE(base::mac::FSRefFromPath(bundle_path().value(), &bundle_ref));
  GetIOMessageLoopProxy()->PostTask(
      FROM_HERE,
      NewRunnableFunction(&TrashFunc, bundle_path()));
  Run();
  ASSERT_TRUE(mock_launchd()->remove_called());
  ASSERT_TRUE(mock_launchd()->delete_called());
  std::string path(base::mac::PathFromFSRef(bundle_ref));
  FilePath file_path(path);
  ASSERT_TRUE(file_util::Delete(file_path, true));
}

TEST_F(ServiceProcessStateFileManipulationTest, ChangeAttr) {
  ScopedAttributesRestorer restorer(bundle_path(), 0777);
  GetIOMessageLoopProxy()->PostTask(
      FROM_HERE,
      NewRunnableFunction(&ChangeAttr, bundle_path(), 0222));
  Run();
  ASSERT_TRUE(mock_launchd()->remove_called());
  ASSERT_TRUE(mock_launchd()->delete_called());
}

#endif  // !OS_MACOSX