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
+22
View File
@@ -0,0 +1,22 @@
# QuickShare for Linux
## Native application logs
The QuickShare GUI writes its native Abseil diagnostics to:
```text
$XDG_STATE_HOME/quickshare/logs/quickshare.log
```
If `XDG_STATE_HOME` is not set to an absolute path, the directory defaults to
`$HOME/.local/state/quickshare/logs`.
Records are stored exactly as Abseil emits them. The active log rotates at
10 MiB, with `quickshare.log.1` through `quickshare.log.4` retaining the four
previous files. The directory and its files are accessible only to the current
user.
After file logging initializes, native Abseil diagnostics are not duplicated
to stderr. If the file cannot be initialized or written, diagnostics fall back
to stderr. Qt and QML messages keep their existing behavior and are not written
to these files.
+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
@@ -182,7 +182,7 @@ class LinuxDeviceInfo final : public nearby::api::DeviceInfo {
return BuildPathFromBase("/tmp", {"Google Nearby"});
}
FilePath GetLogPath() const override {
return GetLocalAppDataPath(FilePath("logs"));
return GetQuickShareLogPath();
}
bool IsScreenLocked() const override { return false; }
void RegisterScreenLockedListener(
+13
View File
@@ -50,6 +50,19 @@ FilePath BuildPathFromBase(const std::string& base,
return FilePath(path.string());
}
FilePath GetQuickShareLogPath() {
const char* state_home = std::getenv("XDG_STATE_HOME");
std::filesystem::path base_path;
if (state_home != nullptr && *state_home != '\0' &&
std::filesystem::path(state_home).is_absolute()) {
base_path = state_home;
} else {
base_path =
std::filesystem::path(GetHomeDirectory()) / ".local" / "state";
}
return FilePath((base_path / "quickshare" / "logs").string());
}
std::optional<std::string> GetLanguageCode() {
const char* lang = std::getenv("LANG");
if (lang == nullptr || *lang == '\0') {
+1
View File
@@ -29,6 +29,7 @@ std::string GetEnvOrDefault(const char* key, std::string fallback);
std::string GetHomeDirectory();
FilePath BuildPathFromBase(const std::string& base,
std::initializer_list<std::string> components);
FilePath GetQuickShareLogPath();
std::optional<std::string> GetLanguageCode();
bool HasNonLoopbackInterface();