Skip to content

Commit

Permalink
implement statfs (#144)
Browse files Browse the repository at this point in the history
  • Loading branch information
azuline authored Dec 31, 2024
1 parent 42f9051 commit 1b3c5fc
Show file tree
Hide file tree
Showing 13 changed files with 90 additions and 66 deletions.
8 changes: 7 additions & 1 deletion .github/workflows/build.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,17 @@ name: build
on: push
jobs:
build:
runs-on: ubuntu-latest
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, macos-latest]
env:
ACTIONS_ALLOW_UNSECURE_COMMANDS: true
steps:
- uses: actions/checkout@v3
- name: Install macFUSE
if: matrix.os == 'macos-latest'
run: brew install --cask macfuse
- uses: cachix/install-nix-action@v20
- uses: cachix/cachix-action@v12
with:
Expand Down
10 changes: 9 additions & 1 deletion conftest.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import dataclasses
import hashlib
import logging
import multiprocessing
import shutil
import sqlite3
import time
Expand All @@ -10,7 +11,7 @@
import pytest
from click.testing import CliRunner
from rose.cache import CACHE_SCHEMA_PATH, process_string_for_fts, update_cache
from rose.common import VERSION
from rose.common import VERSION, initialize_logging
from rose.config import Config, VirtualFSConfig
from rose.templates import PathTemplateConfig

Expand All @@ -25,8 +26,15 @@
TEST_TAGGER = TESTDATA / "Tagger"


@pytest.fixture(autouse=True)
def multiprocessing_set_start_method() -> None:
# Force fork on MacOS, spawn is too unperformant.
multiprocessing.set_start_method("fork", force=True)


@pytest.fixture(autouse=True)
def debug_logging() -> None:
initialize_logging()
logging.getLogger().setLevel(logging.DEBUG)


Expand Down
3 changes: 3 additions & 0 deletions flake.nix
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,9 @@
];
})
];
propagatedBuildInputs = [
(pkgs.lib.optional pkgs.stdenv.isDarwin pkgs.macfuse-stubs)
];
};
packages = rec {
rose-py = pkgs.callPackage ./rose-py { inherit version python-pin py-deps; };
Expand Down
3 changes: 0 additions & 3 deletions rose-cli/rose_cli/__init__.py

This file was deleted.

3 changes: 2 additions & 1 deletion rose-cli/rose_cli/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,12 @@

import click
from rose import RoseExpectedError

from rose.common import initialize_logging
from rose_cli.cli import CliExpectedError, cli


def main() -> None:
initialize_logging()
try:
cli()
except (RoseExpectedError, CliExpectedError) as e:
Expand Down
9 changes: 2 additions & 7 deletions rose-cli/rose_cli/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,6 @@
toggle_release_new,
update_cache,
)

from rose_cli.dump import (
dump_all_artists,
dump_all_collages,
Expand Down Expand Up @@ -104,11 +103,8 @@ class Context:
def cli(cc: click.Context, verbose: bool, config: Path | None = None) -> None:
"""A music manager with a virtual filesystem."""

cc.obj = Context(
config=Config.parse(config_path_override=config),
)
if verbose:
logging.getLogger().setLevel(logging.DEBUG)
cc.obj = Context(config=Config.parse(config_path_override=config))
logging.getLogger().setLevel(logging.DEBUG if verbose else logging.INFO)
maybe_invalidate_cache_database(cc.obj.config)


Expand Down Expand Up @@ -209,7 +205,6 @@ def mount(ctx: Context, foreground: bool) -> None:
mount_virtualfs(ctx.config, debug=debug)
finally:
p.join(timeout=1)
return


@fs.command()
Expand Down
3 changes: 1 addition & 2 deletions rose-cli/rose_cli/cli_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,6 @@
import pytest
from click.testing import CliRunner
from rose import AudioTags, Config
from rose_vfs.virtualfs_test import start_virtual_fs

from rose_cli.cli import (
Context,
InvalidReleaseArgError,
Expand All @@ -19,6 +17,7 @@
unwatch,
watch,
)
from rose_vfs.virtualfs_test import start_virtual_fs


@pytest.mark.usefixtures("seeded_cache")
Expand Down
62 changes: 33 additions & 29 deletions rose-py/rose/cache.py
Original file line number Diff line number Diff line change
Expand Up @@ -928,23 +928,25 @@ def _update_cache_for_releases_executor(

if release_dirty:
logger.debug(f"Scheduling upsert for dirty release in database: {release.source_path}")
upd_release_args.append([
release.id,
str(release.source_path),
str(release.cover_image_path) if release.cover_image_path else None,
release.added_at,
release.datafile_mtime,
release.releasetitle,
release.releasetype,
str(release.releasedate) if release.releasedate else None,
str(release.originaldate) if release.originaldate else None,
str(release.compositiondate) if release.compositiondate else None,
release.edition,
release.catalognumber,
release.disctotal,
release.new,
sha256_dataclass(release),
])
upd_release_args.append(
[
release.id,
str(release.source_path),
str(release.cover_image_path) if release.cover_image_path else None,
release.added_at,
release.datafile_mtime,
release.releasetitle,
release.releasetype,
str(release.releasedate) if release.releasedate else None,
str(release.originaldate) if release.originaldate else None,
str(release.compositiondate) if release.compositiondate else None,
release.edition,
release.catalognumber,
release.disctotal,
release.new,
sha256_dataclass(release),
]
)
upd_release_ids.append(release.id)
for pos, genre in enumerate(release.genres):
upd_release_genre_args.append([release.id, genre, pos])
Expand All @@ -965,18 +967,20 @@ def _update_cache_for_releases_executor(
if track.id not in track_ids_to_insert:
continue
logger.debug(f"Scheduling upsert for dirty track in database: {track.source_path}")
upd_track_args.append([
track.id,
str(track.source_path),
track.source_mtime,
track.tracktitle,
track.release.id,
track.tracknumber,
track.tracktotal,
track.discnumber,
track.duration_seconds,
sha256_dataclass(track),
])
upd_track_args.append(
[
track.id,
str(track.source_path),
track.source_mtime,
track.tracktitle,
track.release.id,
track.tracknumber,
track.tracktotal,
track.discnumber,
track.duration_seconds,
sha256_dataclass(track),
]
)
upd_track_ids.append(track.id)
pos = 0
for role, artists in track.trackartists.items():
Expand Down
13 changes: 4 additions & 9 deletions rose-py/rose/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -153,20 +153,15 @@ def _rec_sha256_dataclass(hasher: Any, value: Any) -> None:
hasher.update(str(value).encode())


__logging_initialized = False
__logging_initialized: set[str | None] = set()


def initialize_logging(logger_name: str) -> None:
global __logging_initialized
if __logging_initialized:
def initialize_logging(logger_name: str | None = None) -> None:
if logger_name in __logging_initialized:
return
__logging_initialized = True
__logging_initialized.add(logger_name)

logger = logging.getLogger(logger_name)
logger.setLevel(logging.INFO)

if "pytest" in sys.modules: # pragma: no cover
logger.setLevel(logging.DEBUG)

# appdirs by default has Unix log to $XDG_CACHE_HOME, but I'd rather write logs to $XDG_STATE_HOME.
log_home = Path(appdirs.user_state_dir("rose"))
Expand Down
4 changes: 0 additions & 4 deletions rose-vfs/rose_vfs/__init__.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,6 @@
from rose import initialize_logging

from rose_vfs.virtualfs import mount_virtualfs, unmount_virtualfs

__all__ = [
"mount_virtualfs",
"unmount_virtualfs",
]

initialize_logging(__name__)
27 changes: 23 additions & 4 deletions rose-vfs/rose_vfs/virtualfs.py
Original file line number Diff line number Diff line change
Expand Up @@ -462,15 +462,15 @@ def list_release_paths(
# Generate a position if we're in a collage.
position = None
if release_parent.collage:
position = f"{str(idx+1).zfill(prefix_pad_size)}"
position = f"{str(idx + 1).zfill(prefix_pad_size)}"

# Generate the virtual name.
time_start = time.time()
cachekey = (release_parent, template, release.metahash, position)
try:
vname = self._release_template_eval_cache[cachekey]
logger.debug(
f"VNAMES: Reused cached virtual dirname {vname} for release {logtext} in {time.time()-time_start} seconds"
f"VNAMES: Reused cached virtual dirname {vname} for release {logtext} in {time.time() - time_start} seconds"
)
except KeyError:
context = PathContext(
Expand Down Expand Up @@ -505,7 +505,7 @@ def list_release_paths(
vname = sanitize_dirname(self._config, vname, False)
self._release_template_eval_cache[cachekey] = vname
logger.debug(
f"VNAMES: Generated virtual dirname {vname} for release {logtext} in {time.time()-time_start} seconds"
f"VNAMES: Generated virtual dirname {vname} for release {logtext} in {time.time() - time_start} seconds"
)

# Handle name collisions by appending a unique discriminator to the end.
Expand Down Expand Up @@ -594,7 +594,7 @@ def list_track_paths(
# Generate a position if we're in a playlist.
position = None
if track_parent.playlist:
position = f"{str(idx+1).zfill(prefix_pad_size)}"
position = f"{str(idx + 1).zfill(prefix_pad_size)}"
# Generate the virtual filename.
time_start = time.time()
cachekey = (track_parent, template, track.metahash, position)
Expand Down Expand Up @@ -1778,6 +1778,12 @@ def open(self, inode: int, flags: int, _: Any) -> int:
self.ghost_existing_files[str(spath)] = True
return self.fhandler.dev_null

# We also black hole all "._*" files on MacOS, which contain extended attribute data which
# we don't support.
if vpath.file and vpath.file.startswith("._"):
self.ghost_existing_files[str(spath)] = True
return self.fhandler.dev_null

try:
fh = self.rose.open(vpath, flags)
except OSError as e:
Expand Down Expand Up @@ -1980,6 +1986,19 @@ def removexattr(self, inode: int, name: bytes, _: Any) -> None:
logger.debug(f"FUSE: Received removexattr for {inode=} {name=}")
raise llfuse.FUSEError(llfuse.ENOATTR)

def statfs(self, _: Any) -> llfuse.StatvfsData:
return llfuse.StatvfsData(
f_bsize=4096, # Filesystem block size (4KB is common)
f_frsize=4096, # Fragment size (usually same as block size)
f_blocks=1024 * 1024 * 16, # Total data blocks in the filesystem (16GB in this example)
f_bfree=1024 * 1024 * 16, # Free blocks available to non-superuser (16GB free)
f_bavail=1024 * 1024 * 16, # Free blocks available to superuser (same as above here)
f_files=1024 * 128, # Total number of file nodes (arbitrary value)
f_ffree=1024 * 64, # Free file nodes (arbitrary value)
f_favail=1024 * 64, # Free file nodes for superuser
f_namemax=255, # Maximum filename length (255 is typical)
)


def mount_virtualfs(c: Config, debug: bool = False) -> None:
options = set(llfuse.default_options)
Expand Down
7 changes: 6 additions & 1 deletion rose-vfs/rose_vfs/virtualfs_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,12 @@ def start_virtual_fs(c: Config) -> Iterator[None]:
p = Process(target=mount_virtualfs, args=[c, True])
try:
p.start()
time.sleep(0.15)
# Takes >1 second to mount with MacFUSE, ~100ms on Linux.
start = time.time()
while not list(c.vfs.mount_dir.iterdir()):
diff = time.time() - start
assert diff < 2, "timed out waiting for vfs to mount"
time.sleep(0.05)
yield
unmount_virtualfs(c)
p.join(timeout=1)
Expand Down
4 changes: 0 additions & 4 deletions rose-watch/rose_watch/__init__.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,5 @@
from rose import initialize_logging

from rose_watch.watcher import start_watchdog

__all__ = [
"start_watchdog",
]

initialize_logging(__name__)

0 comments on commit 1b3c5fc

Please sign in to comment.