Skip to content

Commit 1b817ee

Browse files
committed
Merge branch 'Update-TMP-PyPNM-Dir-process'
2 parents 2ecce00 + 6f27701 commit 1b817ee

41 files changed

Lines changed: 361 additions & 96 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

CODING_AGENTS.md

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,10 +53,18 @@ Before introducing new types, validators, formats, or storage conventions:
5353
- When asked for a commit message, respond with the specified format, keep it succinct, and include all changes since the last commit message request.
5454
- Commit messages must be returned in Markdown text format (use a code block).
5555

56-
### Commit Message Format
57-
56+
## Commit Message Format
57+
- If request via chat request starts with commit-msg, then preface command ./tools/git/git-save.sh with commit-msg "<commit-msg>"
5858
- One line summary (max 50 characters)
59+
- One line Summary start: Feature: , Bugfix: , Docs: , Refactor: , Test:
5960
- Detailed description lines (max 72 characters per line); every line after the first must start with `-`
61+
- When the user asks for a commit message, provide plain text for direct paste into the terminal or UI text box.
62+
- Do not wrap commit message suggestions in quotes (`"`), backticks (`` ` ``), or code fences unless the user explicitly asks for that format.
63+
- Prefer detailed commit messages that describe the current change set clearly.
64+
- Do not default to a one-line commit message when the change set is broad; provide a title plus concise bullet points.
65+
- Avoid redundant wording and avoid repeating the exact prior commit message suggestion unless the diff is unchanged and the user explicitly asks to reuse it.
66+
- If the user asks for "in a text box", return plain text only (no markdown fence).
67+
- If the user asks for "in a markdown text box", return the commit message inside a fenced code block with `text`.
6068

6169
## Agent Constraints
6270

deploy/docker/install.sh

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ set -euo pipefail
44
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
55
COMPOSE_DIR="${SCRIPT_DIR}/compose"
66
CONFIG_DIR="${SCRIPT_DIR}/config"
7+
PYPNM_SHARED_GROUP="${PYPNM_SHARED_GROUP:-pypnm}"
78

89
create_file_if_missing() {
910
local src="$1"
@@ -21,6 +22,12 @@ create_file_if_missing() {
2122
create_file_if_missing "${COMPOSE_DIR}/.env.example" "${COMPOSE_DIR}/.env" "compose/.env"
2223
create_file_if_missing "${CONFIG_DIR}/system.json.template" "${CONFIG_DIR}/system.json" "config/system.json"
2324

25+
if [[ "$(uname -s)" == "Linux" ]]; then
26+
mkdir -p /tmp/pypnm >/dev/null 2>&1 || true
27+
chgrp "${PYPNM_SHARED_GROUP}" /tmp/pypnm >/dev/null 2>&1 || true
28+
chmod 2775 /tmp/pypnm >/dev/null 2>&1 || true
29+
fi
30+
2431
cat <<'MSG'
2532
Next steps:
2633
1. Edit deploy/docker/config/system.json with your environment details.

install.sh

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ CLEAN_MODE="0"
1515
PURGE_CACHE="0"
1616
UNINSTALL_MODE="0"
1717
GITLEAKS_VERSION="8.18.1"
18+
PYPNM_SHARED_GROUP="pypnm"
19+
GROUP_MEMBERSHIP_CHANGED="0"
1820

1921
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
2022
PROJECT_ROOT="${SCRIPT_DIR}"
@@ -658,6 +660,72 @@ run_pnm_alias_installer_if_available() {
658660
fi
659661
}
660662

663+
run_with_privilege_if_needed() {
664+
if [[ "$(id -u)" -eq 0 ]]; then
665+
"$@"
666+
return $?
667+
fi
668+
if command -v sudo >/dev/null 2>&1; then
669+
sudo "$@"
670+
return $?
671+
fi
672+
return 1
673+
}
674+
675+
ensure_pypnm_shared_group() {
676+
local target_user
677+
target_user="${SUDO_USER:-${USER:-$(id -un)}}"
678+
679+
if [[ "$(uname -s)" != "Linux" ]]; then
680+
echo "ℹ️ Skipping '${PYPNM_SHARED_GROUP}' group setup on non-Linux host."
681+
return
682+
fi
683+
684+
if getent group "${PYPNM_SHARED_GROUP}" >/dev/null 2>&1; then
685+
echo "👥 Group '${PYPNM_SHARED_GROUP}' already exists."
686+
else
687+
if run_with_privilege_if_needed groupadd "${PYPNM_SHARED_GROUP}"; then
688+
echo "👥 Created group '${PYPNM_SHARED_GROUP}'."
689+
else
690+
echo "⚠️ Unable to create group '${PYPNM_SHARED_GROUP}'."
691+
return
692+
fi
693+
fi
694+
695+
if id -nG "${target_user}" 2>/dev/null | tr ' ' '\n' | grep -qx "${PYPNM_SHARED_GROUP}"; then
696+
echo "👤 User '${target_user}' is already in group '${PYPNM_SHARED_GROUP}'."
697+
return
698+
fi
699+
700+
if run_with_privilege_if_needed usermod -aG "${PYPNM_SHARED_GROUP}" "${target_user}"; then
701+
echo "👤 Added user '${target_user}' to group '${PYPNM_SHARED_GROUP}'."
702+
GROUP_MEMBERSHIP_CHANGED="1"
703+
else
704+
echo "⚠️ Unable to add user '${target_user}' to group '${PYPNM_SHARED_GROUP}'."
705+
fi
706+
}
707+
708+
ensure_tmp_pypnm_permissions() {
709+
local tmp_root="/tmp/pypnm"
710+
711+
mkdir -p "${tmp_root}" || {
712+
echo "⚠️ Unable to create ${tmp_root}."
713+
return
714+
}
715+
716+
if chgrp "${PYPNM_SHARED_GROUP}" "${tmp_root}" >/dev/null 2>&1 || run_with_privilege_if_needed chgrp "${PYPNM_SHARED_GROUP}" "${tmp_root}" >/dev/null 2>&1; then
717+
:
718+
else
719+
echo "⚠️ Unable to set group '${PYPNM_SHARED_GROUP}' on ${tmp_root}."
720+
fi
721+
722+
if chmod 2775 "${tmp_root}" >/dev/null 2>&1 || run_with_privilege_if_needed chmod 2775 "${tmp_root}" >/dev/null 2>&1; then
723+
echo "📁 Ensured ${tmp_root} exists with group '${PYPNM_SHARED_GROUP}' and permissions 2775."
724+
else
725+
echo "⚠️ Unable to set permissions 2775 on ${tmp_root}."
726+
fi
727+
}
728+
661729
run_tmp_cleanup_cron_installer_if_available() {
662730
if [[ -x "${PROJECT_ROOT}/scripts/install-tmp-cleanup-cron.sh" ]]; then
663731
echo "🧹 Installing tmp cache cleanup cron job..."
@@ -676,6 +744,8 @@ else
676744
echo " ./tools/pnm/pnm_file_retrieval_setup.py"
677745
fi
678746

747+
ensure_pypnm_shared_group
748+
ensure_tmp_pypnm_permissions
679749
run_pnm_alias_installer_if_available
680750
run_tmp_cleanup_cron_installer_if_available
681751

@@ -686,6 +756,9 @@ fi
686756
if [[ "$PRODUCTION_MODE" == "1" ]]; then
687757
echo "👉 Production mode is restored: system settings have been reverted from backup."
688758
fi
759+
if [[ "${GROUP_MEMBERSHIP_CHANGED}" == "1" ]]; then
760+
echo "👉 Group membership changed for '${SUDO_USER:-${USER:-$(id -un)}}'. Start a new login session for group changes to take effect."
761+
fi
689762
echo "👉 Next steps:"
690763
echo " 1) source '$VENV_DIR/bin/activate'"
691764
echo " 2) (optional) ./tools/pnm/pnm_file_retrieval_setup.py"

pyproject.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,9 @@ ignore = [
197197
"B006"
198198
]
199199

200+
[tool.ruff.lint.per-file-ignores]
201+
"tests/**/*.py" = ["ANN"]
202+
200203
[tool.pyright]
201204
pythonVersion = "3.10"
202205
pythonPlatform = "Linux"

scripts/install-pypnm-docker-container.sh

Lines changed: 68 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ PYPNM_TAG="${PYPNM_TAG:-}"
1414
PYPNM_PORT="${PYPNM_PORT:-8000}"
1515
PYPNM_DEPLOY_DIR="${PYPNM_DEPLOY_DIR:-/opt/pypnm}"
1616
PYPNM_USER="${PYPNM_USER:-${USER}}"
17+
PYPNM_SHARED_GROUP="${PYPNM_SHARED_GROUP:-pypnm}"
1718
PYPNM_IMAGE="ghcr.io/PyPNMApps/pypnm:${PYPNM_TAG}"
1819
PYPNM_FALLBACK_TAG="${PYPNM_FALLBACK_TAG:-v0.9.34.0}"
1920
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
@@ -37,6 +38,18 @@ require_cmd() {
3738
command -v "$1" >/dev/null 2>&1 || { echo "Error: missing required command: $1" >&2; exit 1; }
3839
}
3940

41+
run_with_privilege_if_needed() {
42+
if [[ "$(id -u)" -eq 0 ]]; then
43+
"$@"
44+
return $?
45+
fi
46+
if command -v sudo >/dev/null 2>&1; then
47+
sudo "$@"
48+
return $?
49+
fi
50+
return 1
51+
}
52+
4053
usage() {
4154
cat <<'EOF'
4255
Usage: install-pypnm-docker-container.sh [options]
@@ -230,6 +243,57 @@ ensure_compose_ok() {
230243
exit 1
231244
}
232245

246+
ensure_pypnm_shared_group() {
247+
local users=("${PYPNM_USER}" "${SUDO_USER:-}" "${USER}")
248+
local target_user=""
249+
250+
if [[ "$(uname -s)" != "Linux" ]]; then
251+
echo "Skipping '${PYPNM_SHARED_GROUP}' group setup on non-Linux host."
252+
return
253+
fi
254+
255+
if getent group "${PYPNM_SHARED_GROUP}" >/dev/null 2>&1; then
256+
echo "Group '${PYPNM_SHARED_GROUP}' already exists."
257+
else
258+
run_with_privilege_if_needed groupadd "${PYPNM_SHARED_GROUP}" || {
259+
echo "Unable to create group '${PYPNM_SHARED_GROUP}'." >&2
260+
return
261+
}
262+
echo "Created group '${PYPNM_SHARED_GROUP}'."
263+
fi
264+
265+
for target_user in "${users[@]}"; do
266+
[[ -z "${target_user}" ]] && continue
267+
if ! id "${target_user}" >/dev/null 2>&1; then
268+
continue
269+
fi
270+
if id -nG "${target_user}" 2>/dev/null | tr ' ' '\n' | grep -qx "${PYPNM_SHARED_GROUP}"; then
271+
continue
272+
fi
273+
if run_with_privilege_if_needed usermod -aG "${PYPNM_SHARED_GROUP}" "${target_user}"; then
274+
echo "Added user '${target_user}' to group '${PYPNM_SHARED_GROUP}'."
275+
else
276+
echo "Unable to add user '${target_user}' to group '${PYPNM_SHARED_GROUP}'." >&2
277+
fi
278+
done
279+
}
280+
281+
ensure_tmp_pypnm_shared_permissions() {
282+
local tmp_root="/tmp/pypnm"
283+
mkdir -p "${tmp_root}" || {
284+
echo "Unable to create ${tmp_root}" >&2
285+
return
286+
}
287+
288+
chgrp "${PYPNM_SHARED_GROUP}" "${tmp_root}" >/dev/null 2>&1 || run_with_privilege_if_needed chgrp "${PYPNM_SHARED_GROUP}" "${tmp_root}" >/dev/null 2>&1 || {
289+
echo "Unable to set group '${PYPNM_SHARED_GROUP}' on ${tmp_root}" >&2
290+
}
291+
292+
chmod 2775 "${tmp_root}" >/dev/null 2>&1 || run_with_privilege_if_needed chmod 2775 "${tmp_root}" >/dev/null 2>&1 || {
293+
echo "Unable to set permissions 2775 on ${tmp_root}" >&2
294+
}
295+
}
296+
233297
set_env_var() {
234298
local file="$1"
235299
local key="$2"
@@ -348,7 +412,8 @@ sync_deploy_bundle() {
348412
sudo rm -rf "${PYPNM_DEPLOY_DIR}"
349413
sudo mkdir -p "${PYPNM_DEPLOY_DIR}"
350414
(cd "${source_dir}" && sudo tar -cf - .) | (cd "${PYPNM_DEPLOY_DIR}" && sudo tar -xf -)
351-
sudo chown -R "${PYPNM_USER}:${PYPNM_USER}" "${PYPNM_DEPLOY_DIR}"
415+
sudo chown -R "${PYPNM_USER}:${PYPNM_SHARED_GROUP}" "${PYPNM_DEPLOY_DIR}"
416+
sudo chmod 2775 "${PYPNM_DEPLOY_DIR}" || true
352417
}
353418

354419
initialize_bundle() {
@@ -409,6 +474,8 @@ main() {
409474
require_cmd python3
410475
ensure_docker_ok
411476
ensure_compose_ok
477+
ensure_pypnm_shared_group
478+
ensure_tmp_pypnm_shared_permissions
412479
sync_deploy_bundle
413480
initialize_bundle
414481
pull_and_start

src/pypnm/pnm/lib/pnm_artifact_store.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
from __future__ import annotations
55

6+
import grp
67
import json
78
import logging
89
import os
@@ -57,6 +58,8 @@ def __init__(self, config: PnmArtifactStorageConfig | None = None, pnm_dir: Path
5758
self._last_cleanup = 0.0
5859

5960
self._pnm_dir.mkdir(parents=True, exist_ok=True)
61+
self._tmp_root.mkdir(parents=True, exist_ok=True)
62+
self._ensure_tmp_pypnm_permissions()
6063
self._ingress_dir.mkdir(parents=True, exist_ok=True)
6164
self._materialized_dir.mkdir(parents=True, exist_ok=True)
6265

@@ -76,6 +79,8 @@ def ingress_path(self, filename: FileNameStr, transaction_id: TransactionId | No
7679
Path
7780
Filesystem path where callers can write the ingress artifact.
7881
"""
82+
self._tmp_root.mkdir(parents=True, exist_ok=True)
83+
self._ensure_tmp_pypnm_permissions()
7984
self._ingress_dir.mkdir(parents=True, exist_ok=True)
8085
return self._ingress_dir / self._normalize_ingress_name(filename)
8186

@@ -117,6 +122,34 @@ def find_ingress_by_filename(self, filename: FileNameStr) -> Path | None:
117122
return matches[0]
118123
return None
119124

125+
def _ensure_tmp_pypnm_permissions(self) -> None:
126+
"""Ensure /tmp/pypnm uses shared-group permissions when configured as cache root."""
127+
expected = Path("/tmp/pypnm")
128+
try:
129+
if self._tmp_root.resolve() != expected:
130+
return
131+
except OSError:
132+
if self._tmp_root != expected:
133+
return
134+
135+
try:
136+
pypnm_gid = grp.getgrnam("pypnm").gr_gid
137+
except KeyError:
138+
pypnm_gid = None
139+
140+
if pypnm_gid is not None:
141+
try:
142+
os.chown(self._tmp_root, -1, pypnm_gid)
143+
except OSError as exc:
144+
self.logger.warning("Unable to set group 'pypnm' on %s: %s", self._tmp_root, exc)
145+
else:
146+
self.logger.warning("Group 'pypnm' not found; continuing without chgrp for %s", self._tmp_root)
147+
148+
try:
149+
os.chmod(self._tmp_root, 0o2775)
150+
except OSError as exc:
151+
self.logger.warning("Unable to set permissions 2775 on %s: %s", self._tmp_root, exc)
152+
120153
@staticmethod
121154
def _normalize_ingress_name(filename: FileNameStr) -> str:
122155
name = Path(str(filename)).name

tests/test_cable_modem_precheck_channel_ids.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,12 @@
55

66
import pytest
77

8-
from pypnm.api.routes.common.classes.operation.cable_modem_precheck import (
9-
CableModemServicePreCheck,
10-
)
118
from pypnm.api.routes.common.classes.common_endpoint_classes.common_req_resp import (
129
TftpConfig,
1310
)
11+
from pypnm.api.routes.common.classes.operation.cable_modem_precheck import (
12+
CableModemServicePreCheck,
13+
)
1414
from pypnm.api.routes.common.service.status_codes import ServiceStatusCode
1515
from pypnm.lib.types import ChannelId, InterfaceIndex
1616

tests/test_capture_group_concurrency.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,6 @@
77
from multiprocessing.synchronize import Event
88
from pathlib import Path
99

10-
import pytest
11-
1210
from pypnm.api.routes.common.classes.file_capture.capture_group import CaptureGroup
1311
from pypnm.lib.types import GroupId, TransactionId
1412

tests/test_common_process_service.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
import logging
77
from pathlib import Path
8+
89
import pytest
910

1011
from pypnm.api.routes.common.extended.common_messaging_service import MessageResponse

tests/test_complex_array_ops.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ def pairs(*vals: float) -> list[tuple[float, float]]:
1616
"""Build (re, im) pairs from flat numbers: r1,i1,r2,i2,..."""
1717
assert len(vals) % 2 == 0
1818
it = iter(vals)
19-
return [(float(r), float(i)) for r, i in zip(it, it)]
19+
return [(float(r), float(i)) for r, i in zip(it, it, strict=False)]
2020

2121

2222
def test_init_and_len_and_repr() -> None:

0 commit comments

Comments
 (0)