Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 51 additions & 0 deletions apport/procutils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# Copyright (C) 2026 Canonical Ltd.
# Author: Benjamin Drung <bdrung@ubuntu.com>
#
# This program is free software; you can redistribute it and/or modify it
# under the terms of the GNU General Public License as published by the
# Free Software Foundation; either version 2 of the License, or (at your
# option) any later version. See http://www.gnu.org/copyleft/gpl.html for
# the full text of the license.

"""Functions to operate on files in /proc."""

import dataclasses


@dataclasses.dataclass
class Meminfo:
"""Data from /proc/meminfo. Values are in KiB.

Only the needed values from /proc/meminfo are provided.
This dataclass can be extended when needed.
"""

mem_total: int
mem_free: int
mem_available: int
cached: int
writeback: int


def parse_meminfo() -> Meminfo:
"""Parse /proc/meminfo and return a dictionary for the requested keys."""
remaining_keys = {"MemTotal", "MemFree", "MemAvailable", "Cached", "Writeback"}
meminfo = {}
with open("/proc/meminfo", "r", encoding="utf-8") as meminfo_file:
for line in meminfo_file:
key, remaining = line.split(":", maxsplit=1)
if key not in remaining_keys:
continue
value, unit = remaining.strip().split(maxsplit=2)
assert unit == "kB" or value == "0"
meminfo[key] = int(value)
remaining_keys.remove(key)
if not remaining_keys:
return Meminfo(
meminfo["MemTotal"],
meminfo["MemFree"],
meminfo["MemAvailable"],
meminfo["Cached"],
meminfo["Writeback"],
)
raise KeyError(f"{', '.join(sorted(remaining_keys))} not found in /proc/meminfo")
16 changes: 4 additions & 12 deletions data/apport
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ from collections.abc import Callable

import apport.fileutils
import apport.report
from apport.procutils import parse_meminfo
from apport.user_group import UserGroupID
from problem_report import CompressedFile

Expand Down Expand Up @@ -339,20 +340,11 @@ def write_user_coredump(
os.close(core_file)


def usable_ram():
def usable_ram() -> int:
"""Return how many bytes of RAM is currently available that can be
allocated without causing major thrashing."""

# abuse our excellent RFC822 parser to parse /proc/meminfo
r = apport.report.Report()
with open("/proc/meminfo", "rb") as f:
r.load(f)

memfree = int(r["MemFree"].split()[0])
cached = int(r["Cached"].split()[0])
writeback = int(r["Writeback"].split()[0])

return (memfree + cached - writeback) * 1024
meminfo = parse_meminfo()
return (meminfo.mem_free + meminfo.cached - meminfo.writeback) * 1024


def _run_with_output_limit_and_timeout(
Expand Down
7 changes: 2 additions & 5 deletions tests/system/test_apport_valgrind.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,11 @@

import pytest

from apport.procutils import parse_meminfo
from tests.helper import get_gnu_coreutils_cmd, skip_if_command_is_missing
from tests.paths import local_test_environment

with open("/proc/meminfo", encoding="utf-8") as f:
for line in f.readlines():
if line.startswith("MemTotal"):
MEM_TOTAL_MiB = int(line.split()[1]) // 1024
break
MEM_TOTAL_MiB = parse_meminfo().mem_total // 1024


@skip_if_command_is_missing("valgrind")
Expand Down
9 changes: 3 additions & 6 deletions tests/system/test_signal_crashes.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@

import apport.fileutils
import apport.report
from apport.procutils import parse_meminfo
from tests.helper import (
get_gnu_coreutils_cmd,
get_init_system,
Expand Down Expand Up @@ -159,12 +160,8 @@ def test_limit_size(self) -> None:
assert self.apport_path is not None
# determine how much data we have to pump into apport in order to make
# sure that it will refuse the core dump
r = apport.report.Report()
with open("/proc/meminfo", "rb") as f:
r.load(f)
totalmb = int(r["MemFree"].split()[0]) + int(r["Cached"].split()[0])
totalmb = int(totalmb / 1024)
del r
meminfo = parse_meminfo()
totalmb = (meminfo.mem_free + meminfo.cached) // 1024

test_proc = self.create_test_process()
try:
Expand Down
50 changes: 50 additions & 0 deletions tests/unit/test_procutils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# Copyright (C) 2026 Canonical Ltd.
# Author: Benjamin Drung <bdrung@ubuntu.com>
#
# This program is free software; you can redistribute it and/or modify it
# under the terms of the GNU General Public License as published by the
# Free Software Foundation; either version 2 of the License, or (at your
# option) any later version. See http://www.gnu.org/copyleft/gpl.html for
# the full text of the license.

"""Unit tests for the apport.procutils module."""

from unittest.mock import mock_open, patch

import pytest

from apport.procutils import Meminfo, parse_meminfo


def test_parse_meminfo() -> None:
"""Test parse_meminfo() reading all values successfully."""
open_mock = mock_open(
read_data=(
"MemTotal: 64249228 kB\n"
"MemFree: 25066812 kB\n"
"MemAvailable: 40437820 kB\n"
"Buffers: 587236 kB\n"
"Cached: 19694816 kB\n"
"Writeback: 0 kB\n"
)
)
with patch("builtins.open", open_mock):
meminfo = parse_meminfo()
assert meminfo == Meminfo(64249228, 25066812, 40437820, 19694816, 0)


def test_parse_meminfo_missing_key() -> None:
"""Test parse_meminfo() failing to read the requested keys."""
open_mock = mock_open(
read_data=(
"MemTotal: 64249228 kB\n"
"MemFree: 23434628 kB\n"
"MemAvailable: 38809072 kB\n"
"Buffers: 589372 kB\n"
"Cached: 20099444 kB\n"
)
)
with patch("builtins.open", open_mock):
with pytest.raises(KeyError) as exc_info:
parse_meminfo()
exc_info.match("Writeback not found in /proc/meminfo")
Loading