You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
387 lines
11 KiB
387 lines
11 KiB
package main
|
|
|
|
import (
|
|
"archive/tar"
|
|
"compress/gzip"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"sort"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/towgo/towgo/lib/system"
|
|
g "tgk-touch/internal/global"
|
|
)
|
|
|
|
const firefoxInstallMarkerName = ".firefox-install-ok"
|
|
|
|
var forbiddenFirefoxDebPackages = map[string]struct{}{
|
|
"base-files": {},
|
|
"base-passwd": {},
|
|
"bash": {},
|
|
"coreutils": {},
|
|
"dash": {},
|
|
"debconf": {},
|
|
"dpkg": {},
|
|
"gcc-8-base": {},
|
|
"libblkid1": {},
|
|
"libc6": {},
|
|
"libgcc1": {},
|
|
"libmount1": {},
|
|
"libselinux1": {},
|
|
"libsystemd0": {},
|
|
"libudev1": {},
|
|
"login": {},
|
|
"mount": {},
|
|
"passwd": {},
|
|
"perl-base": {},
|
|
"sudo": {},
|
|
"systemd": {},
|
|
"systemd-sysv": {},
|
|
"tar": {},
|
|
"udev": {},
|
|
"util-linux": {},
|
|
}
|
|
|
|
var optionalFirefoxDebPackages = map[string]string{
|
|
"lubuntu-icon-theme": "theme package may conflict with existing adwaita-icon-theme-full and is not required by Firefox",
|
|
}
|
|
|
|
type firefoxBundleInstallPlan struct {
|
|
TotalPackageCount int
|
|
HasFirefoxPackage bool
|
|
BaseSystemPackages []string
|
|
}
|
|
|
|
func isFirefoxUsable() bool {
|
|
cmd := exec.Command("firefox", "--version")
|
|
out, err := cmd.CombinedOutput()
|
|
ok := err == nil
|
|
kioskInfof("firefox usability check ok=%v output=%q err=%v", ok, strings.TrimSpace(string(out)), err)
|
|
return ok
|
|
}
|
|
|
|
func isFirefoxInstalled() bool {
|
|
if path, err := exec.LookPath("firefox"); err == nil {
|
|
kioskDebugf("firefox install path check ok=true path=%s source=PATH", path)
|
|
return true
|
|
}
|
|
|
|
for _, path := range []string{
|
|
"/usr/bin/firefox",
|
|
"/usr/local/bin/firefox",
|
|
"/snap/bin/firefox",
|
|
} {
|
|
if isExecutableFile(path) {
|
|
kioskDebugf("firefox install path check ok=true path=%s source=fallback", path)
|
|
return true
|
|
}
|
|
}
|
|
|
|
kioskDebugf("firefox install path check ok=false")
|
|
return false
|
|
}
|
|
|
|
func isExecutableFile(path string) bool {
|
|
info, err := os.Stat(path)
|
|
return err == nil && !info.IsDir() && info.Mode()&0111 != 0
|
|
}
|
|
|
|
func installFirefox() error {
|
|
bundlePath := firefoxBundlePath()
|
|
plan, err := validateFirefoxBundle(bundlePath)
|
|
if err != nil {
|
|
kioskErrorf("firefox install refused bundle=%s err=%v", bundlePath, err)
|
|
return err
|
|
}
|
|
baseSystemPackages := strings.Join(plan.BaseSystemPackages, " ")
|
|
optionalSkipPackages := strings.Join(optionalFirefoxDebPackageNames(), " ")
|
|
kioskInfof("firefox install package filter baseSystemPackages=%q optionalSkipPackages=%q", baseSystemPackages, optionalSkipPackages)
|
|
|
|
var p struct {
|
|
DebPkgPath string `json:"DebPkgPath"`
|
|
DebPkgName string `json:"DebPkgName"`
|
|
InstallShellPath string `json:"InstallShellPath"`
|
|
}
|
|
p.DebPkgPath = g.Config().Firefox.DebPkgPath
|
|
p.DebPkgName = g.Config().Firefox.DebPkgName
|
|
p.InstallShellPath = g.Config().Firefox.InstallShellPath
|
|
kioskInfof("firefox install begin programPath=%s bundle=%s debPath=%s debName=%s installShell=%s", system.GetPathOfProgram(), bundlePath, p.DebPkgPath, p.DebPkgName, p.InstallShellPath)
|
|
|
|
command := exec.Command("sudo", "sh", "-c", fmt.Sprintf(`
|
|
echo "=== current work directory ==="
|
|
pwd
|
|
echo ""
|
|
|
|
echo "=== keep existing Firefox package ==="
|
|
echo "skip automatic dpkg purge to avoid touching base system packages"
|
|
echo ""
|
|
|
|
echo "=== clean previous offline bundle files ==="
|
|
rm -rf /opt/firefox-deb /opt/firefox-installable /opt/firefox-deb.tar.gz 2>/dev/null
|
|
echo ""
|
|
|
|
echo "=== start Firefox install ==="
|
|
echo "1. copy offline bundle to /opt..."
|
|
cp -v %s /opt/firefox-deb.tar.gz || { echo "error: copy offline bundle failed"; exit 1; }
|
|
echo ""
|
|
|
|
echo "2. extract offline bundle..."
|
|
cd /opt && tar -xzvf firefox-deb.tar.gz || { echo "error: extract offline bundle failed"; exit 1; }
|
|
echo ""
|
|
|
|
echo "3. filter Firefox deb packages..."
|
|
base_system_packages=%s
|
|
optional_skip_packages=%s
|
|
mkdir -p /opt/firefox-installable
|
|
selected_count=0
|
|
skipped_installed_count=0
|
|
skipped_optional_count=0
|
|
selected_missing_base_count=0
|
|
selected_regular_count=0
|
|
firefox_selected=0
|
|
select_deb() {
|
|
cp -v "$deb" /opt/firefox-installable/
|
|
selected_count=$((selected_count + 1))
|
|
}
|
|
package_name_from_deb() {
|
|
pkg=$(dpkg-deb -f "$1" Package 2>/dev/null || true)
|
|
if [ -n "$pkg" ]; then
|
|
echo "$pkg"
|
|
return 0
|
|
fi
|
|
file=$(basename "$1")
|
|
echo "$file" | cut -d '_' -f 1 | sed 's/[.]deb$//'
|
|
}
|
|
for deb in /opt/firefox-deb/*.deb; do
|
|
[ -e "$deb" ] || continue
|
|
file=$(basename "$deb")
|
|
pkg=$(package_name_from_deb "$deb")
|
|
if [ -z "$pkg" ]; then
|
|
echo "skip package with empty name: $file"
|
|
continue
|
|
fi
|
|
echo "package candidate: $file package=$pkg"
|
|
|
|
status=$(dpkg-query -W -f='${Status}' "$pkg" 2>/dev/null || true)
|
|
installed=0
|
|
if [ "$status" = "install ok installed" ]; then
|
|
installed=1
|
|
fi
|
|
|
|
if [ "$pkg" = "firefox" ]; then
|
|
select_deb
|
|
firefox_selected=1
|
|
continue
|
|
fi
|
|
|
|
case " $optional_skip_packages " in
|
|
*" $pkg "*)
|
|
echo "skip optional package: $file package=$pkg"
|
|
skipped_optional_count=$((skipped_optional_count + 1))
|
|
continue
|
|
;;
|
|
esac
|
|
|
|
case " $base_system_packages " in
|
|
*" $pkg "*)
|
|
if [ "$installed" -eq 1 ]; then
|
|
echo "skip installed base system package: $file"
|
|
skipped_installed_count=$((skipped_installed_count + 1))
|
|
continue
|
|
fi
|
|
echo "select missing base system package: $file"
|
|
select_deb
|
|
selected_missing_base_count=$((selected_missing_base_count + 1))
|
|
continue
|
|
;;
|
|
esac
|
|
|
|
if [ "$installed" -eq 1 ]; then
|
|
echo "skip installed package: $file"
|
|
skipped_installed_count=$((skipped_installed_count + 1))
|
|
continue
|
|
fi
|
|
|
|
select_deb
|
|
selected_regular_count=$((selected_regular_count + 1))
|
|
done
|
|
echo "selected packages: $selected_count, selected missing base: $selected_missing_base_count, selected regular: $selected_regular_count, skipped installed: $skipped_installed_count, skipped optional: $skipped_optional_count"
|
|
if [ "$selected_count" -eq 0 ]; then
|
|
echo "error: no packages selected for install"
|
|
exit 1
|
|
fi
|
|
if [ "$firefox_selected" -ne 1 ]; then
|
|
echo "error: Firefox deb package was not selected"
|
|
exit 1
|
|
fi
|
|
echo ""
|
|
|
|
echo "4. install selected Firefox packages..."
|
|
cd /opt/firefox-installable || { echo "error: enter installable package directory failed"; exit 1; }
|
|
install_status=0
|
|
sudo dpkg -i ./*.deb || install_status=$?
|
|
if [ "$install_status" -ne 0 ]; then
|
|
echo "warning: selected Firefox deb package install returned status $install_status, verify Firefox next"
|
|
fi
|
|
echo ""
|
|
|
|
echo "5. verify Firefox..."
|
|
firefox --version || { echo "error: Firefox verification failed"; exit 1; }
|
|
echo ""
|
|
|
|
echo "=== Firefox install complete ==="
|
|
`, shellQuote(bundlePath), shellQuote(baseSystemPackages), shellQuote(optionalSkipPackages)))
|
|
command.Stdout = os.Stdout
|
|
command.Stderr = os.Stderr
|
|
|
|
err = command.Run()
|
|
if err != nil {
|
|
kioskErrorf("firefox install command failed err=%v", err)
|
|
return err
|
|
}
|
|
kioskInfof("firefox install command finished successfully")
|
|
return nil
|
|
}
|
|
|
|
func firefoxInstallMarkerPath() string {
|
|
return filepath.Join(system.GetPathOfProgram(), "data", firefoxInstallMarkerName)
|
|
}
|
|
|
|
func hasFirefoxInstallMarker() bool {
|
|
path := firefoxInstallMarkerPath()
|
|
info, err := os.Stat(path)
|
|
ok := err == nil && !info.IsDir()
|
|
kioskInfof("firefox install marker check path=%s ok=%v err=%v", path, ok, err)
|
|
return ok
|
|
}
|
|
|
|
func writeFirefoxInstallMarker(reason string) error {
|
|
path := firefoxInstallMarkerPath()
|
|
if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {
|
|
kioskErrorf("create firefox install marker dir failed path=%s err=%v", filepath.Dir(path), err)
|
|
return err
|
|
}
|
|
|
|
content := fmt.Sprintf("reason=%s\ntime=%s\n", reason, time.Now().Format(time.RFC3339))
|
|
if err := os.WriteFile(path, []byte(content), 0644); err != nil {
|
|
kioskErrorf("write firefox install marker failed path=%s err=%v", path, err)
|
|
return err
|
|
}
|
|
kioskInfof("firefox install marker written path=%s reason=%s", path, reason)
|
|
return nil
|
|
}
|
|
|
|
func firefoxBundlePath() string {
|
|
return filepath.Join(system.GetPathOfProgram(), g.Config().Firefox.DebPkgPath, g.Config().Firefox.DebPkgName)
|
|
}
|
|
|
|
func validateFirefoxBundle(bundlePath string) (firefoxBundleInstallPlan, error) {
|
|
packages, err := firefoxBundlePackageNames(bundlePath)
|
|
if err != nil {
|
|
return firefoxBundleInstallPlan{}, fmt.Errorf("read firefox bundle packages: %w", err)
|
|
}
|
|
if len(packages) == 0 {
|
|
return firefoxBundleInstallPlan{}, fmt.Errorf("firefox bundle has no deb packages")
|
|
}
|
|
|
|
plan := firefoxBundleInstallPlanFromPackages(packages)
|
|
if !plan.HasFirefoxPackage {
|
|
return firefoxBundleInstallPlan{}, fmt.Errorf("firefox bundle has no firefox deb package")
|
|
}
|
|
|
|
if len(plan.BaseSystemPackages) > 0 {
|
|
kioskWarnf("firefox bundle contains base system packages; installed ones will be skipped and missing ones may be installed packages=%s", strings.Join(plan.BaseSystemPackages, ", "))
|
|
}
|
|
kioskInfof("firefox bundle validation passed bundle=%s totalPackages=%d baseSystemPackages=%d hasFirefox=%v", bundlePath, plan.TotalPackageCount, len(plan.BaseSystemPackages), plan.HasFirefoxPackage)
|
|
return plan, nil
|
|
}
|
|
|
|
func firefoxBundleInstallPlanFromPackages(packages []string) firefoxBundleInstallPlan {
|
|
plan := firefoxBundleInstallPlan{
|
|
TotalPackageCount: len(packages),
|
|
BaseSystemPackages: unsafeFirefoxDebPackages(packages),
|
|
}
|
|
for _, name := range packages {
|
|
if name == "firefox" {
|
|
plan.HasFirefoxPackage = true
|
|
}
|
|
}
|
|
return plan
|
|
}
|
|
|
|
func firefoxBundlePackageNames(bundlePath string) ([]string, error) {
|
|
file, err := os.Open(bundlePath)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer file.Close()
|
|
|
|
gzipReader, err := gzip.NewReader(file)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer gzipReader.Close()
|
|
|
|
reader := tar.NewReader(gzipReader)
|
|
var packages []string
|
|
for {
|
|
header, err := reader.Next()
|
|
if err == io.EOF {
|
|
break
|
|
}
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if header == nil || header.FileInfo().IsDir() || !strings.HasSuffix(header.Name, ".deb") {
|
|
continue
|
|
}
|
|
name := debPackageName(filepath.Base(header.Name))
|
|
if name != "" {
|
|
packages = append(packages, name)
|
|
}
|
|
}
|
|
sort.Strings(packages)
|
|
return packages, nil
|
|
}
|
|
|
|
func debPackageName(fileName string) string {
|
|
name := strings.TrimSuffix(fileName, ".deb")
|
|
if packageName, _, ok := strings.Cut(name, "_"); ok {
|
|
return packageName
|
|
}
|
|
return name
|
|
}
|
|
|
|
func unsafeFirefoxDebPackages(packages []string) []string {
|
|
seen := make(map[string]struct{})
|
|
for _, name := range packages {
|
|
if _, blocked := forbiddenFirefoxDebPackages[name]; blocked {
|
|
seen[name] = struct{}{}
|
|
}
|
|
}
|
|
|
|
blocked := make([]string, 0, len(seen))
|
|
for name := range seen {
|
|
blocked = append(blocked, name)
|
|
}
|
|
sort.Strings(blocked)
|
|
return blocked
|
|
}
|
|
|
|
func optionalFirefoxDebPackageNames() []string {
|
|
names := make([]string, 0, len(optionalFirefoxDebPackages))
|
|
for name := range optionalFirefoxDebPackages {
|
|
names = append(names, name)
|
|
}
|
|
sort.Strings(names)
|
|
return names
|
|
}
|
|
|
|
func shellQuote(value string) string {
|
|
return "'" + strings.ReplaceAll(value, "'", "'\\''") + "'"
|
|
}
|