added native file logging

This commit is contained in:
Lasan Mahaliyana
2026-07-20 19:06:26 +05:30
parent 3b67895106
commit 6beb25cd44
9 changed files with 634 additions and 1 deletions
+34
View File
@@ -1,4 +1,5 @@
load("@rules_cc//cc:cc_library.bzl", "cc_library")
load("@rules_cc//cc:cc_test.bzl", "cc_test")
load("@rules_qt//:qt.bzl", "qt_cc_binary", "qt_cc_library", "qt_resource_via_qrc")
load("@rules_shell//shell:sh_binary.bzl", "sh_binary")
@@ -67,6 +68,38 @@ qt_cc_library(
],
)
cc_library(
name = "native_file_logging",
srcs = ["native_file_logging.cc"],
hdrs = ["native_file_logging.h"],
deps = [
"//sharing/linux:linux_sharing_platform",
"@com_google_absl//absl/base:log_severity",
"@com_google_absl//absl/log:globals",
"@com_google_absl//absl/log:initialize",
"@com_google_absl//absl/log:log_entry",
"@com_google_absl//absl/log:log_sink",
"@com_google_absl//absl/log:log_sink_registry",
"@com_google_absl//absl/strings:string_view",
],
)
cc_test(
name = "native_file_logging_test",
size = "small",
srcs = ["native_file_logging_test.cc"],
deps = [
":native_file_logging",
"//sharing/linux:linux_sharing_platform",
"@com_google_absl//absl/log",
"@com_google_absl//absl/log:globals",
"@com_google_absl//absl/log:initialize",
"@com_google_absl//absl/log:log_sink",
"@com_google_absl//absl/log:log_sink_registry",
"@com_google_googletest//:gtest_main",
],
)
qt_cc_binary(
name = "app",
srcs = ["main.cc"],
@@ -76,6 +109,7 @@ qt_cc_binary(
deps = [
":app_resources",
":backend",
":native_file_logging",
"@rules_qt//:qt_core",
"@rules_qt//:qt_gui",
"@rules_qt//:qt_hdrs",
+3
View File
@@ -18,6 +18,7 @@
#include <QQmlContext>
#include "backend.h"
#include "qobject.h"
#include "sharing/linux/app/native_file_logging.h"
namespace {
@@ -113,6 +114,8 @@ void InstallTerminationCleanup(QApplication& app) {
} // namespace
int main(int argc, char* argv[]) {
std::unique_ptr<nearby::sharing::linux::NativeFileLogging> native_logging =
nearby::sharing::linux::NativeFileLogging::Initialize();
QApplication app(argc, argv);
app.setApplicationName(QStringLiteral("QuickShare"));
app.setDesktopFileName(QStringLiteral("quickshare"));
+213
View File
@@ -0,0 +1,213 @@
// Copyright 2026 Google LLC
//
// Licensed 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
//
// https://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.
#include "sharing/linux/app/native_file_logging.h"
#include <errno.h>
#include <fcntl.h>
#include <sys/file.h>
#include <sys/stat.h>
#include <unistd.h>
#include <cstdio>
#include <system_error>
#include <utility>
#include "absl/log/globals.h"
#include "absl/log/initialize.h"
#include "absl/log/log_sink_registry.h"
#include "absl/strings/string_view.h"
#include "sharing/linux/platform/platform_util.h"
namespace nearby::sharing::linux {
namespace {
constexpr char kLogFileName[] = "quickshare.log";
constexpr char kLockFileName[] = ".quickshare.log.lock";
bool WriteAll(int fd, const char* data, size_t size) {
while (size > 0) {
const ssize_t written = write(fd, data, size);
if (written < 0) {
if (errno == EINTR) {
continue;
}
return false;
}
data += written;
size -= static_cast<size_t>(written);
}
return true;
}
void WriteToStderr(absl::string_view message) {
static_cast<void>(WriteAll(STDERR_FILENO, message.data(), message.size()));
}
} // namespace
std::unique_ptr<NativeFileLogSink> NativeFileLogSink::Create(
std::filesystem::path log_directory, size_t max_file_size, int file_count) {
if (max_file_size == 0 || file_count < 1) {
return nullptr;
}
std::error_code error;
std::filesystem::create_directories(log_directory, error);
if (error) {
return nullptr;
}
std::filesystem::permissions(log_directory, std::filesystem::perms::owner_all,
std::filesystem::perm_options::replace, error);
if (error) {
return nullptr;
}
const std::filesystem::path lock_path = log_directory / kLockFileName;
const int lock_fd =
open(lock_path.c_str(), O_CREAT | O_RDWR | O_CLOEXEC | O_NOFOLLOW, 0600);
if (lock_fd < 0) {
return nullptr;
}
if (fchmod(lock_fd, 0600) != 0) {
close(lock_fd);
return nullptr;
}
const std::filesystem::path log_path = log_directory / kLogFileName;
const int log_fd =
open(log_path.c_str(),
O_CREAT | O_WRONLY | O_APPEND | O_CLOEXEC | O_NOFOLLOW, 0600);
if (log_fd < 0 || fchmod(log_fd, 0600) != 0) {
if (log_fd >= 0) {
close(log_fd);
}
close(lock_fd);
return nullptr;
}
close(log_fd);
return std::unique_ptr<NativeFileLogSink>(new NativeFileLogSink(
std::move(log_directory), lock_fd, max_file_size, file_count));
}
NativeFileLogSink::NativeFileLogSink(std::filesystem::path log_directory,
int lock_fd, size_t max_file_size,
int file_count)
: log_path_(std::move(log_directory) / kLogFileName),
lock_fd_(lock_fd),
max_file_size_(max_file_size),
file_count_(file_count) {}
NativeFileLogSink::~NativeFileLogSink() {
close(lock_fd_);
}
void NativeFileLogSink::Send(const absl::LogEntry& entry) {
const absl::string_view message =
entry.text_message_with_prefix_and_newline();
const bool flush = entry.log_severity() >= absl::LogSeverity::kError;
std::lock_guard<std::mutex> lock(mutex_);
if (flock(lock_fd_, LOCK_EX) != 0) {
WriteFallback(message.data(), message.size());
return;
}
const bool success = RotateIfNeeded(message.size()) &&
WriteRecord(message.data(), message.size(), flush);
static_cast<void>(flock(lock_fd_, LOCK_UN));
if (!success) {
WriteFallback(message.data(), message.size());
}
}
bool NativeFileLogSink::RotateIfNeeded(size_t incoming_size) {
struct stat status{};
if (stat(log_path_.c_str(), &status) != 0) {
return errno == ENOENT;
}
const size_t current_size =
status.st_size > 0 ? static_cast<size_t>(status.st_size) : 0;
if (current_size == 0 || (incoming_size <= max_file_size_ &&
current_size <= max_file_size_ - incoming_size)) {
return true;
}
for (int index = file_count_ - 1; index >= 1; --index) {
const std::filesystem::path destination =
log_path_.string() + "." + std::to_string(index);
const std::filesystem::path source =
index == 1 ? log_path_
: std::filesystem::path(log_path_.string() + "." +
std::to_string(index - 1));
if (rename(source.c_str(), destination.c_str()) != 0 && errno != ENOENT) {
return false;
}
}
return true;
}
bool NativeFileLogSink::WriteRecord(const char* data, size_t size, bool flush) {
const int fd =
open(log_path_.c_str(),
O_CREAT | O_WRONLY | O_APPEND | O_CLOEXEC | O_NOFOLLOW, 0600);
if (fd < 0) {
return false;
}
bool success = fchmod(fd, 0600) == 0 && WriteAll(fd, data, size);
if (success && flush) {
success = fsync(fd) == 0;
}
if (close(fd) != 0) {
success = false;
}
return success;
}
void NativeFileLogSink::WriteFallback(const char* data, size_t size) {
WriteToStderr(absl::string_view(data, size));
}
std::unique_ptr<NativeFileLogging> NativeFileLogging::Initialize() {
std::unique_ptr<NativeFileLogSink> sink =
NativeFileLogSink::Create(internal::GetQuickShareLogPath().ToString());
absl::InitializeLog();
if (sink == nullptr) {
WriteToStderr(
"QuickShare: unable to initialize file logging; using "
"stderr.\n");
return nullptr;
}
absl::AddLogSink(sink.get());
const absl::LogSeverityAtLeast previous_threshold = absl::StderrThreshold();
absl::SetStderrThreshold(absl::LogSeverityAtLeast::kInfinity);
return std::unique_ptr<NativeFileLogging>(
new NativeFileLogging(std::move(sink), previous_threshold));
}
NativeFileLogging::NativeFileLogging(
std::unique_ptr<NativeFileLogSink> sink,
absl::LogSeverityAtLeast previous_stderr_threshold)
: sink_(std::move(sink)),
previous_stderr_threshold_(previous_stderr_threshold) {}
NativeFileLogging::~NativeFileLogging() {
absl::SetStderrThreshold(previous_stderr_threshold_);
absl::RemoveLogSink(sink_.get());
}
} // namespace nearby::sharing::linux
+82
View File
@@ -0,0 +1,82 @@
// Copyright 2026 Google LLC
//
// Licensed 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
//
// https://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.
#ifndef SHARING_LINUX_APP_NATIVE_FILE_LOGGING_H_
#define SHARING_LINUX_APP_NATIVE_FILE_LOGGING_H_
#include <cstddef>
#include <filesystem>
#include <memory>
#include <mutex>
#include "absl/base/log_severity.h"
#include "absl/log/log_entry.h"
#include "absl/log/log_sink.h"
namespace nearby::sharing::linux {
class NativeFileLogSink final : public absl::LogSink {
public:
static constexpr size_t kDefaultMaxFileSize = 10 * 1024 * 1024;
static constexpr int kDefaultFileCount = 5;
static std::unique_ptr<NativeFileLogSink> Create(
std::filesystem::path log_directory,
size_t max_file_size = kDefaultMaxFileSize,
int file_count = kDefaultFileCount);
NativeFileLogSink(const NativeFileLogSink&) = delete;
NativeFileLogSink& operator=(const NativeFileLogSink&) = delete;
~NativeFileLogSink() override;
void Send(const absl::LogEntry& entry) override;
const std::filesystem::path& log_path() const { return log_path_; }
private:
NativeFileLogSink(std::filesystem::path log_directory, int lock_fd,
size_t max_file_size, int file_count);
bool RotateIfNeeded(size_t incoming_size);
bool WriteRecord(const char* data, size_t size, bool flush);
void WriteFallback(const char* data, size_t size);
std::filesystem::path log_path_;
int lock_fd_;
size_t max_file_size_;
int file_count_;
std::mutex mutex_;
};
// Owns the process-wide Abseil sink registration. Initialize() must be called
// exactly once, before any application components start logging.
class NativeFileLogging final {
public:
static std::unique_ptr<NativeFileLogging> Initialize();
NativeFileLogging(const NativeFileLogging&) = delete;
NativeFileLogging& operator=(const NativeFileLogging&) = delete;
~NativeFileLogging();
private:
NativeFileLogging(std::unique_ptr<NativeFileLogSink> sink,
absl::LogSeverityAtLeast previous_stderr_threshold);
std::unique_ptr<NativeFileLogSink> sink_;
absl::LogSeverityAtLeast previous_stderr_threshold_;
};
} // namespace nearby::sharing::linux
#endif // SHARING_LINUX_APP_NATIVE_FILE_LOGGING_H_
@@ -0,0 +1,265 @@
// Copyright 2026 Google LLC
//
// Licensed 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
//
// https://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.
#include "sharing/linux/app/native_file_logging.h"
#include <sys/stat.h>
#include <unistd.h>
#include <cstdlib>
#include <filesystem>
#include <fstream>
#include <mutex>
#include <optional>
#include <sstream>
#include <string>
#include <thread>
#include <utility>
#include <vector>
#include "absl/log/globals.h"
#include "absl/log/initialize.h"
#include "absl/log/log.h"
#include "absl/log/log_sink.h"
#include "absl/log/log_sink_registry.h"
#include "gtest/gtest.h"
#include "sharing/linux/platform/platform_util.h"
namespace nearby::sharing::linux {
namespace {
class TemporaryDirectory {
public:
TemporaryDirectory() {
std::string path =
(std::filesystem::temp_directory_path() /
("quickshare-logging-test-" + std::to_string(getpid()) + "-XXXXXX"))
.string();
path.push_back('\0');
char* created = mkdtemp(path.data());
EXPECT_NE(created, nullptr);
if (created != nullptr) {
path_ = created;
}
}
~TemporaryDirectory() {
std::error_code error;
std::filesystem::remove_all(path_, error);
}
const std::filesystem::path& path() const { return path_; }
private:
std::filesystem::path path_;
};
class ScopedEnvironmentVariable {
public:
ScopedEnvironmentVariable(const char* name, std::optional<std::string> value)
: name_(name) {
const char* old_value = std::getenv(name);
if (old_value != nullptr) {
old_value_ = old_value;
}
if (value.has_value()) {
setenv(name, value->c_str(), 1);
} else {
unsetenv(name);
}
}
~ScopedEnvironmentVariable() {
if (old_value_.has_value()) {
setenv(name_.c_str(), old_value_->c_str(), 1);
} else {
unsetenv(name_.c_str());
}
}
private:
std::string name_;
std::optional<std::string> old_value_;
};
class CaptureSink final : public absl::LogSink {
public:
void Send(const absl::LogEntry& entry) override {
std::lock_guard<std::mutex> lock(mutex_);
records_.emplace_back(entry.text_message_with_prefix_and_newline());
}
std::vector<std::string> records() {
std::lock_guard<std::mutex> lock(mutex_);
return records_;
}
private:
std::mutex mutex_;
std::vector<std::string> records_;
};
void EnsureAbslLoggingInitialized() {
static const bool initialized = [] {
absl::InitializeLog();
return true;
}();
static_cast<void>(initialized);
}
std::string ReadFile(const std::filesystem::path& path) {
std::ifstream stream(path, std::ios::binary);
std::ostringstream contents;
contents << stream.rdbuf();
return contents.str();
}
int PermissionBits(const std::filesystem::path& path) {
struct stat status{};
EXPECT_EQ(stat(path.c_str(), &status), 0);
return status.st_mode & 0777;
}
TEST(NativeFileLogSinkTest, PreservesOriginalAbseilRecord) {
EnsureAbslLoggingInitialized();
TemporaryDirectory temporary_directory;
auto sink = NativeFileLogSink::Create(temporary_directory.path());
ASSERT_NE(sink, nullptr);
CaptureSink capture;
absl::ScopedStderrThreshold suppress_stderr(
absl::LogSeverityAtLeast::kInfinity);
absl::AddLogSink(sink.get());
absl::AddLogSink(&capture);
LOG(INFO) << "native-record-sentinel";
absl::RemoveLogSink(&capture);
absl::RemoveLogSink(sink.get());
const std::vector<std::string> records = capture.records();
ASSERT_EQ(records.size(), size_t{1});
EXPECT_EQ(ReadFile(sink->log_path()), records.front());
}
TEST(NativeFileLogSinkTest, RotatesAndRetainsFiveFiles) {
EnsureAbslLoggingInitialized();
TemporaryDirectory temporary_directory;
auto sink = NativeFileLogSink::Create(temporary_directory.path(), 256, 5);
ASSERT_NE(sink, nullptr);
absl::ScopedStderrThreshold suppress_stderr(
absl::LogSeverityAtLeast::kInfinity);
absl::AddLogSink(sink.get());
for (int index = 0; index < 8; ++index) {
LOG(INFO) << "rotation-record-" << index << "-" << std::string(180, 'x');
}
absl::RemoveLogSink(sink.get());
EXPECT_TRUE(std::filesystem::exists(sink->log_path()));
for (int index = 1; index <= 4; ++index) {
EXPECT_TRUE(std::filesystem::exists(sink->log_path().string() + "." +
std::to_string(index)));
}
EXPECT_FALSE(std::filesystem::exists(sink->log_path().string() + ".5"));
}
TEST(NativeFileLogSinkTest, SerializesConcurrentRecords) {
EnsureAbslLoggingInitialized();
TemporaryDirectory temporary_directory;
auto sink = NativeFileLogSink::Create(temporary_directory.path());
ASSERT_NE(sink, nullptr);
absl::ScopedStderrThreshold suppress_stderr(
absl::LogSeverityAtLeast::kInfinity);
absl::AddLogSink(sink.get());
std::vector<std::thread> threads;
for (int thread = 0; thread < 4; ++thread) {
threads.emplace_back([thread] {
for (int record = 0; record < 20; ++record) {
LOG(INFO) << "concurrent-record-" << thread << "-" << record;
}
});
}
for (std::thread& thread : threads) {
thread.join();
}
absl::RemoveLogSink(sink.get());
const std::string contents = ReadFile(sink->log_path());
size_t record_count = 0;
size_t position = 0;
while ((position = contents.find("concurrent-record-", position)) !=
std::string::npos) {
++record_count;
position += 18;
}
EXPECT_EQ(record_count, size_t{80});
}
TEST(NativeFileLogSinkTest, UsesPrivatePermissions) {
TemporaryDirectory temporary_directory;
const std::filesystem::path log_directory =
temporary_directory.path() / "nested" / "logs";
auto sink = NativeFileLogSink::Create(log_directory);
ASSERT_NE(sink, nullptr);
EXPECT_EQ(PermissionBits(log_directory), 0700);
EXPECT_EQ(PermissionBits(sink->log_path()), 0600);
EXPECT_EQ(PermissionBits(log_directory / ".quickshare.log.lock"), 0600);
}
TEST(NativeFileLogSinkTest, RejectsUnavailableDirectory) {
TemporaryDirectory temporary_directory;
const std::filesystem::path regular_file =
temporary_directory.path() / "not-a-directory";
{
std::ofstream stream(regular_file);
stream << "content";
}
EXPECT_EQ(NativeFileLogSink::Create(regular_file), nullptr);
}
TEST(LogPathTest, UsesAbsoluteXdgStateHome) {
TemporaryDirectory temporary_directory;
ScopedEnvironmentVariable state_home("XDG_STATE_HOME",
temporary_directory.path().string());
EXPECT_EQ(internal::GetQuickShareLogPath().ToString(),
(temporary_directory.path() / "quickshare" / "logs").string());
}
TEST(LogPathTest, FallsBackToHomeForRelativeXdgStateHome) {
TemporaryDirectory temporary_directory;
ScopedEnvironmentVariable home("HOME", temporary_directory.path().string());
ScopedEnvironmentVariable state_home("XDG_STATE_HOME", "relative/path");
EXPECT_EQ(
internal::GetQuickShareLogPath().ToString(),
(temporary_directory.path() / ".local" / "state" / "quickshare" / "logs")
.string());
}
TEST(LogPathTest, FallsBackToHomeWhenXdgStateHomeIsMissing) {
TemporaryDirectory temporary_directory;
ScopedEnvironmentVariable home("HOME", temporary_directory.path().string());
ScopedEnvironmentVariable state_home("XDG_STATE_HOME", std::nullopt);
EXPECT_EQ(
internal::GetQuickShareLogPath().ToString(),
(temporary_directory.path() / ".local" / "state" / "quickshare" / "logs")
.string());
}
} // namespace
} // namespace nearby::sharing::linux