|
7 | 7 | """ |
8 | 8 |
|
9 | 9 | import logging |
| 10 | +from functools import partial |
10 | 11 | from pathlib import Path |
11 | | -from typing import Dict, Any |
12 | 12 |
|
13 | 13 | # Import file detection |
14 | 14 | from blkcache.file import detect |
15 | 15 | from blkcache.file.device import DEFAULT_SECTOR_SIZE |
16 | 16 |
|
17 | 17 | log = logging.getLogger(__name__) |
18 | 18 |
|
19 | | - |
20 | | -class HandleManager: |
21 | | - """Manages file handles and their lifecycle for nbdkit backend.""" |
22 | | - |
23 | | - def __init__(self): |
24 | | - self._handles: Dict[int, Any] = {} # handle_id -> File instance |
25 | | - self._next_handle = 1 |
26 | | - self._files = [] # Keep references to context managers |
27 | | - |
28 | | - def open_file(self, path: Path, mode: str) -> int: |
29 | | - """Open a file and return a handle ID.""" |
30 | | - file_cls = detect(path) |
31 | | - file_instance = file_cls(path, mode) |
32 | | - |
33 | | - # Enter the context manager and keep reference |
34 | | - opened_file = file_instance.__enter__() |
35 | | - self._files.append((file_instance, opened_file)) |
36 | | - |
37 | | - # Assign handle and store |
38 | | - handle = self._next_handle |
39 | | - self._handles[handle] = opened_file |
40 | | - self._next_handle += 1 |
41 | | - |
42 | | - log.debug("Opened file %s as handle %d", path, handle) |
43 | | - return handle |
44 | | - |
45 | | - def get_file(self, handle: int): |
46 | | - """Get the file instance for a handle.""" |
47 | | - if handle not in self._handles: |
48 | | - raise ValueError(f"Invalid handle: {handle}") |
49 | | - return self._handles[handle] |
50 | | - |
51 | | - def close_file(self, handle: int) -> None: |
52 | | - """Close a specific file handle.""" |
53 | | - if handle in self._handles: |
54 | | - file_instance = self._handles[handle] |
55 | | - # Find and close the corresponding context manager |
56 | | - for i, (ctx_mgr, opened_file) in enumerate(self._files): |
57 | | - if opened_file is file_instance: |
58 | | - try: |
59 | | - ctx_mgr.__exit__(None, None, None) |
60 | | - except Exception as e: |
61 | | - log.warning("Error closing file handle %d: %s", handle, e) |
62 | | - self._files.pop(i) |
63 | | - break |
64 | | - |
65 | | - del self._handles[handle] |
66 | | - log.debug("Closed handle %d", handle) |
67 | | - |
68 | | - def close_all(self) -> None: |
69 | | - """Close all open files.""" |
70 | | - for ctx_mgr, _ in self._files: |
71 | | - try: |
72 | | - ctx_mgr.__exit__(None, None, None) |
73 | | - except Exception as e: |
74 | | - log.warning("Error during cleanup: %s", e) |
75 | | - |
76 | | - self._handles.clear() |
77 | | - self._files.clear() |
78 | | - log.debug("Closed all handles") |
79 | | - |
80 | | - |
81 | 19 | # Global state |
82 | 20 | DEV: Path | None = None |
83 | 21 | CACHE: Path | None = None |
84 | 22 | SECTOR_SIZE = DEFAULT_SECTOR_SIZE |
85 | 23 | METADATA = {} |
86 | 24 |
|
87 | | -# Handle manager instance |
88 | | -HANDLE_MANAGER = HandleManager() |
| 25 | +# Simple dispatch table: handle -> (file_instance, context_generator) |
| 26 | +TABLE = {} |
| 27 | + |
| 28 | + |
| 29 | +def lookup(attr: str, handle: int, table=TABLE): |
| 30 | + """Generic attribute lookup for dispatch.""" |
| 31 | + obj, _ = table[handle] |
| 32 | + return getattr(obj, attr) |
89 | 33 |
|
90 | 34 |
|
91 | | -# These functions are now handled by the BlockCache class |
| 35 | +def open_file_context(path: Path, mode: str): |
| 36 | + """Generator to manage file lifecycle.""" |
| 37 | + file_cls = detect(path) |
| 38 | + file_instance = file_cls(path, mode) |
| 39 | + with file_instance as f: |
| 40 | + yield f |
92 | 41 |
|
93 | 42 |
|
94 | 43 | def config(key: str, val: str) -> None: |
@@ -126,72 +75,45 @@ def config_complete() -> None: |
126 | 75 | def open(_readonly: bool) -> int: |
127 | 76 | """Opens device and returns handle ID.""" |
128 | 77 | mode = "rb" if _readonly else "r+b" |
129 | | - return HANDLE_MANAGER.open_file(DEV, mode) |
| 78 | + handle = len(TABLE) + 1 |
| 79 | + ctx = open_file_context(DEV, mode) |
| 80 | + obj = next(ctx) |
| 81 | + TABLE[handle] = (obj, ctx) |
| 82 | + log.debug("Opened file %s as handle %d", DEV, handle) |
| 83 | + return handle |
130 | 84 |
|
131 | 85 |
|
132 | 86 | def get_size(h: int) -> int: |
133 | 87 | """Get file size.""" |
134 | | - file_instance = HANDLE_MANAGER.get_file(h) |
135 | | - return file_instance.size() |
| 88 | + obj, _ = TABLE[h] |
| 89 | + return obj.size() |
136 | 90 |
|
137 | 91 |
|
138 | 92 | def pread(h: int, count: int, offset: int) -> bytes: |
139 | 93 | """Read data at offset.""" |
140 | | - file_instance = HANDLE_MANAGER.get_file(h) |
141 | | - return file_instance.pread(count, offset) |
| 94 | + obj, _ = TABLE[h] |
| 95 | + return obj.pread(count, offset) |
142 | 96 |
|
143 | 97 |
|
144 | 98 | def close(h: int) -> None: |
145 | 99 | """Close file handle.""" |
146 | 100 | log.debug("Backend close() called for handle %d", h) |
147 | | - HANDLE_MANAGER.close_file(h) |
| 101 | + if h in TABLE: |
| 102 | + obj, ctx = TABLE[h] |
| 103 | + try: |
| 104 | + next(ctx) # Advance generator to trigger cleanup |
| 105 | + except StopIteration: |
| 106 | + pass |
| 107 | + del TABLE[h] |
148 | 108 | log.debug("Backend close() completed") |
149 | 109 |
|
150 | 110 |
|
151 | | -# Optional capability functions - use duck typing |
152 | | -def can_write(h: int) -> bool: |
153 | | - """Check if file supports writing.""" |
154 | | - file_instance = HANDLE_MANAGER.get_file(h) |
155 | | - return "w" in file_instance.mode or "a" in file_instance.mode or "+" in file_instance.mode |
156 | | - |
157 | | - |
158 | | -def can_flush(h: int) -> bool: |
159 | | - """Check if file supports flushing.""" |
160 | | - file_instance = HANDLE_MANAGER.get_file(h) |
161 | | - return hasattr(file_instance, "flush") |
162 | | - |
163 | | - |
164 | | -def can_trim(h: int) -> bool: |
165 | | - """Check if file supports trim operations.""" |
166 | | - file_instance = HANDLE_MANAGER.get_file(h) |
167 | | - return hasattr(file_instance, "trim") |
168 | | - |
169 | | - |
170 | | -def can_zero(h: int) -> bool: |
171 | | - """Check if file supports zero operations.""" |
172 | | - file_instance = HANDLE_MANAGER.get_file(h) |
173 | | - return hasattr(file_instance, "zero") |
174 | | - |
175 | | - |
176 | | -def can_fast_zero(h: int) -> bool: |
177 | | - """Check if file supports fast zero operations.""" |
178 | | - file_instance = HANDLE_MANAGER.get_file(h) |
179 | | - return hasattr(file_instance, "fast_zero") |
180 | | - |
181 | | - |
182 | | -def can_extents(h: int) -> bool: |
183 | | - """Check if file supports extent operations.""" |
184 | | - file_instance = HANDLE_MANAGER.get_file(h) |
185 | | - return hasattr(file_instance, "extents") |
186 | | - |
187 | | - |
188 | | -def is_rotational(h: int) -> bool: |
189 | | - """Check if underlying storage is rotational.""" |
190 | | - file_instance = HANDLE_MANAGER.get_file(h) |
191 | | - return hasattr(file_instance, "is_rotational") and file_instance.is_rotational() |
192 | | - |
193 | | - |
194 | | -def can_multi_conn(h: int) -> bool: |
195 | | - """Check if file supports multiple connections.""" |
196 | | - # For now, return False for safety |
197 | | - return False |
| 111 | +# Optional capability functions - use partial dispatch |
| 112 | +can_write = partial(lookup, "can_write") |
| 113 | +can_flush = partial(lookup, "can_flush") |
| 114 | +can_trim = partial(lookup, "can_trim") |
| 115 | +can_zero = partial(lookup, "can_zero") |
| 116 | +can_fast_zero = partial(lookup, "can_fast_zero") |
| 117 | +can_extents = partial(lookup, "can_extents") |
| 118 | +is_rotational = partial(lookup, "is_rotational") |
| 119 | +can_multi_conn = partial(lookup, "can_multi_conn") |
0 commit comments