Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Pull request for benjimons-develop branch. #3078

Open
wants to merge 2 commits into
base: benjimons-develop
Choose a base branch
from
Open
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
126 changes: 80 additions & 46 deletions glances/plugins/tailer/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
# -*- coding: utf-8 -*-
#
# This file is part of Glances.
#
Expand Down Expand Up @@ -56,7 +57,6 @@
# Plugin class
# -----------------------------------------------------------------------------


class PluginModel(GlancesPluginModel):
"""Tailer plugin main class.

Expand Down Expand Up @@ -137,6 +137,7 @@ def _build_file_stat(self, filename, num_lines):

if not os.path.isfile(filename):
logger.debug(f"File not found: {filename}")
result["last_lines"] = ["", "File Not Found"]
return result

try:
Expand All @@ -147,11 +148,9 @@ def _build_file_stat(self, filename, num_lines):
# File size
result["file_size"] = os.path.getsize(filename)

# Count lines, read last N lines
# Count lines, read last N lines (efficiently for large files)
line_count, last_lines = self._tail_file(filename, num_lines)
result["line_count"] = line_count
# Store the last lines as a single string or as a list.
# For display convenience, we might store them as a list of strings.
result["last_lines"] = last_lines

except Exception as e:
Expand All @@ -160,39 +159,78 @@ def _build_file_stat(self, filename, num_lines):
return result

def _tail_file(self, filename, num_lines):
"""Return (total_line_count, list_of_last_N_lines)."""
"""
Return (total_line_count, list_of_last_N_lines) for a potentially huge file.

1) Count total lines by reading the file in chunks (no huge memory usage).
2) Retrieve the last N lines by reading from the end in chunks.
"""

# 1) Count total lines in a streaming fashion
chunk_size = 8192
total_line_count = 0

with open(filename, 'rb') as f:
# If the file is huge, you might want a more efficient way to read
# the last N lines rather than reading the entire file.
# For simplicity, read all lines:
content = f.read().splitlines()
total_lines = len(content)
# Extract the last num_lines lines
last_lines = content[-num_lines:] if total_lines >= num_lines else content
# Decode to str (assuming UTF-8) for each line
last_lines_decoded = [line.decode('utf-8', errors='replace') for line in last_lines]

return total_lines, last_lines_decoded
while True:
chunk = f.read(chunk_size)
if not chunk:
break
# Each \r\n sequence contains a \n, so counting b'\n' is OK
total_line_count += chunk.count(b'\n')

# If file isn't empty and doesn't end with a newline, that last partial line counts
file_size = os.path.getsize(filename)
if file_size > 0:
with open(filename, 'rb') as f:
# Seek to last byte
f.seek(-1, os.SEEK_END)
if f.read(1) not in (b'\n', b'\r'):
total_line_count += 1

# 2) Retrieve last N lines from the end
lines_reversed = []
newlines_found = 0

def update_views(self):
"""Update stats views (optional).
with open(filename, 'rb') as f:
# Start from end of file
f.seek(0, os.SEEK_END)
position = f.tell()

If you need to set decorations (alerts or color formatting),
you can do it here.
"""
super().update_views()
while position > 0 and newlines_found <= num_lines:
read_size = min(chunk_size, position)
position -= read_size
f.seek(position)

chunk = f.read(read_size)
reversed_chunk = chunk[::-1]

for b in reversed_chunk:
if b == 10: # b'\n'
newlines_found += 1
if newlines_found > num_lines:
break
lines_reversed.append(b)

if newlines_found > num_lines:
break

# lines_reversed now includes the bytes for at least N lines in reverse order
lines_reversed.reverse()

# Example: if file_size is above a threshold, we could color it in TUI
for stat_dict in self.get_raw():
fsize = stat_dict.get("file_size", 0)
# Example: decorate if file > 1GB
if fsize > 1024**3:
self.views[stat_dict[self.get_key()]]["file_size"]["decoration"] = self.get_alert(
fsize, header='bigfile'
)
# Decode to text and split lines. splitlines() handles \r, \n, \r\n, etc.
last_data = bytes(lines_reversed).decode('utf-8', errors='replace')
all_last_lines = last_data.splitlines()

last_n_lines = all_last_lines[-num_lines:] if len(all_last_lines) > num_lines else all_last_lines

return total_line_count, last_n_lines

def update_views(self):
"""Update stats views (optional)."""
super().update_views()

def msg_curse(self, args=None, max_width: Optional[int] = None) -> list[str]:
"""Return the dict (list of lines) to display in the TUI."""
"""Return the list of lines to display in the TUI."""
ret = []

# If no stats or disabled, return empty
Expand All @@ -210,27 +248,23 @@ def msg_curse(self, args=None, max_width: Optional[int] = None) -> list[str]:
last_modified = stat.get("last_modified", "")
last_lines = stat.get("last_lines", [])

# New line for each file
ret.append(self.curse_new_line())

# 1) Filename
msg_filename = f"File: {filename}"
ret.append(self.curse_add_line(msg_filename))

# 2) File size + last modified time
# (1) File info
msg_meta = (
f"Size: {self.auto_unit(file_size)}, " f"Last Modified: {last_modified}, " f"Total Lines: {line_count}"
f"File: {filename}, "
f"Size: {self.auto_unit(file_size)}, "
f"Last Modified: {last_modified}, "
f"Total Lines: {line_count}"
)
ret.append(self.curse_new_line())
ret.append(self.curse_add_line(msg_meta))

# 3) Last N lines
ret.append(self.curse_new_line())
ret.append(self.curse_add_line("Last lines:"))
# (2) Last N lines
first_nonblank = True
for line in last_lines:
ret.append(self.curse_new_line())
if first_nonblank:
ret.append(self.curse_new_line())
first_nonblank = False
ret.append(self.curse_add_line(f" {line}"))

ret.append(self.curse_new_line())
ret.append(self.curse_new_line())

return ret