Skip to content

Instantly share code, notes, and snippets.

@moosavimaleki
Last active October 14, 2025 09:32
Show Gist options
  • Save moosavimaleki/bc2cb36a7c09ff5a6a4d27fa7e723914 to your computer and use it in GitHub Desktop.
Save moosavimaleki/bc2cb36a7c09ff5a6a4d27fa7e723914 to your computer and use it in GitHub Desktop.
test.md
xfreerdp3 /v:127.0.0.1:3389 /u:MyWindowsUser /p:MyWindowsPassword /cert:ignore +clipboard /dynamic-resolution
python /home/h-mousavi/.local/bin/linoffice/gui/linoffice.py
~/.local/bin/linoffice/venv/bin/podman-compose --file ~/.local/bin/linoffice/config/compose.yaml up
xfreerdp3 /v:127.0.0.1:3389 /u:MyWindowsUser /p:MyWindowsPassword /cert:ignore +clipboard /dynamic-resolution /drive:linux_home,/home/h-mousavi/
------------------------
shell
----------------------
#!/bin/bash
# نام کانتینر ویندوز شما
CONTAINER_NAME="LinOffice"
RDP_SERVER="127.0.0.1:3389"
TIMEOUT=120 # حداکثر زمان انتظار برای بالا آمدن ویندوز به ثانیه
CHECK_INTERVAL=3 # فاصله زمانی بین هر تلاش برای اتصال به RDP به ثانیه
# چک کردن اینکه آیا کانتینر در حال اجرا است یا نه
if ! podman ps --filter "name=$CONTAINER_NAME" --quiet | grep -q .; then
# اگر کانتینر در حال اجرا نیست، ویندوز را راه‌اندازی می‌کنیم
echo "Windows is not running. Starting Windows with podman-compose..."
~/.local/bin/linoffice/venv/bin/podman-compose --file ~/.local/bin/linoffice/config/compose.yaml up -d
echo "Waiting for Windows to fully start..."
# تلاش برای اتصال به ویندوز با استفاده از xfreerdp3 هر ۳ ثانیه
SECONDS=0
while ! xfreerdp3 /v:$RDP_SERVER /u:MyWindowsUser /p:MyWindowsPassword /cert:ignore +clipboard /dynamic-resolution /drive:linux_home,/home/h-mousavi/; do
echo "RDP connection failed, retrying..."
sleep $CHECK_INTERVAL
SECONDS=$((SECONDS + CHECK_INTERVAL))
# اگر از زمان مشخص شده بیشتر منتظر ماندیم، پایان می‌دهیم
if [ $SECONDS -ge $TIMEOUT ]; then
echo "Timeout reached. Unable to connect to RDP after $TIMEOUT seconds."
exit 1
fi
done
else
# اگر کانتینر در حال اجرا است، مستقیم به اتصال می‌پردازیم
echo "Windows is already running. Attempting to connect..."
# تلاش برای اتصال به ویندوز با استفاده از xfreerdp3
xfreerdp3 /v:$RDP_SERVER /u:MyWindowsUser /p:MyWindowsPassword /cert:ignore +clipboard /dynamic-resolution /drive:linux_home,/home/h-mousavi/
fi
# پایان اسکریپت
echo "Finished the script execution."
ممنون از جزئیات. از لاگ‌ها معلومه اصلاً «درخواستی» به ویندوز نمی‌رسد (وقتی روشن/خاموش بودن ویندوز هیچ فرقی در خروجی FreeRDP ندارد و در Event Viewer هم چیزی ثبت نمی‌شود، یعنی کانکشن قبل از رسیدن به RDP-Tcp قطع می‌شود). در داکر/پادمَن این معمولاً به یکی از این دو علت است:
1. **hostfwdِ QEMU به IP اشتباه داخل ویندوز می‌رود** (پیش‌فرض ایمیج به 20.20.20.21 فوروارد می‌کند؛ اگر کارت شبکهٔ ویندوز IP دیگری گرفته باشد، همهٔ کانکشن‌ها بی‌اثر می‌شوند).
2. **NAT rootless (slirp4netns)** روی 3389 رفتار عجیبی دارد و کانکشن را reset می‌کند.
برو سریع طبق این چک‌لیست جلو؛ هر کدام حل شد، همان را نگه‌دار:
---
## قدم 1) IP داخل ویندوز را ثابت کن روی 20.20.20.21
داخل ویندوز (PowerShell ادمین):
```powershell
$if = Get-NetIPInterface -AddressFamily IPv4 |
Where-Object {$_.InterfaceAlias -like "Ethernet*"} |
Sort-Object ifIndex | Select-Object -First 1
# حذف DHCP فعلی (اگر وجود دارد)
Get-NetIPAddress -InterfaceIndex $if.InterfaceIndex -AddressFamily IPv4 -ErrorAction SilentlyContinue |
Remove-NetIPAddress -Confirm:$false -ErrorAction SilentlyContinue
# ستاتیک: 20.20.20.21/24 و دروازه 20.20.20.1
New-NetIPAddress -InterfaceIndex $if.InterfaceIndex -IPAddress 20.20.20.21 -PrefixLength 24 -DefaultGateway 20.20.20.1
Set-DnsClientServerAddress -InterfaceIndex $if.InterfaceIndex -ServerAddresses 1.1.1.1,8.8.8.8
Restart-Service TermService -Force
```
سپس دوباره با **TLS** تست کن (RDP-Security قدیمی را کنار بگذار):
```bash
rm -f ~/.config/freerdp/known_hosts* ~/.freerdp/known_hosts*
xfreerdp3 /v:127.0.0.1:3389 /u:MyWindowsUser /p:MyWindowsPassword /sec:tls /cert:ignore +clipboard /dynamic-resolution
```
> اگر این مرحله جواب بده، مشکل دقیقاً IP بوده (hostfwd → 20.20.20.21).
---
## قدم 2) NAT پادمَن را سالم کن (اگر قدم 1 جواب نداد)
فایل compose را این‌طور ساده کن:
* **UDP را فعلاً حذف کن** (فقط TCP):
```yaml
ports:
- "3390:3389/tcp" # پورت میزبان را هم عوض کن که مطمئن شو چیز دیگری مزاحم 3389 نیست
# - "3389:3389/udp" # موقتاً حذف
```
* شبکه را از slirp به **pasta** تغییر بده (پایدارتر برای RDP):
```yaml
services:
windows:
# ...
networks:
- default
networks:
default:
driver: pasta
```
سپس:
```bash
podman stop LinOffice && podman rm LinOffice
~/.local/bin/linoffice/venv/bin/podman-compose --file ~/.local/bin/linoffice/config/compose.yaml up -d
podman port LinOffice
```
و تست روی پورت جدید:
```bash
xfreerdp3 /v:127.0.0.1:3390 /u:MyWindowsUser /p:MyWindowsPassword /sec:tls /cert:ignore +clipboard /dynamic-resolution
```
---
## قدم 3) دو تست خیلی سریع برای تشخیص مسیر
1. ببین اصلاً روی میزبان، لیسنر وجود دارد:
```bash
nc -vz 127.0.0.1 3389 # یا 3390 اگر عوض کردی
```
اگر «succeeded» گفت ولی باز هم RDP reset شد و همچنان در ویندوز لاگی نمی‌افتد، یعنی Reset قبل از رسیدن به ویندوز رخ می‌دهد (مشکل NAT/hostfwd).
2. لاگ بوت کانتینر را با دقت چک کن تا **خط فرمان QEMU** را ببینی (باید شامل `hostfwd=tcp::3389-20.20.20.21:3389` باشد):
```bash
podman logs --tail=200 LinOffice | sed -n '1,200p'
```
اگر مقصد hostfwd چیز دیگری بود یا چندتا hostfwd تکراری دیدی، همان علت است.
---
## چند نکتهٔ تکمیلی
* وقتی TLS/NLA را تغییر می‌دهی، **TermService را ریستارت** کردی (درست است)؛ اما اگر تغییری می‌دهی و باز هم هیچ لاگی در `RdpCoreTS` و `LocalSessionManager` نمی‌افتد، مطمئن باش مشکل از ویندوز نیست.
* اگر هنوز گیر است، یک‌بار فقط **TCP روی پورت دیگر** امتحان کن (مثلاً 3390) و روی کلاینت هم `/sec:tls` بزن — خیلی از resetهای FreeRDP وقتی UDP باز است رخ می‌دهند.
* اگر خواستی همه‌چیز را از پایه تمیز کنی: کانتینر را پاک کن، **ولوم `linoffice_data` را هم پاک کن** و از نو بالا بیاور (هشدار: ویندوز داخل کانتینر پاک می‌شود). این کار مطمئن می‌کند اسکریپت‌های QEMU با تنظیمات پیش‌فرض سالم بالا بیایند.
---
### اگر بخواهی، خروجی این ۳ مورد را بده تا دقیق نقطهٔ ایراد را بگویم:
* `ipconfig` داخل ویندوز (IPv4 کارت شبکه)
* `podman logs LinOffice | head -n 80` (برای دیدن hostfwd)
* نتیجهٔ `xfreerdp3 ... /sec:tls` روی پورت جدید 3390
با این سه تا، معمولاً درجا روشن می‌شود که IP داخل ویندوز با hostfwd هم‌خوان نیست یا NAT پادمَن داستان دارد.
version: "3.8"
volumes:
data:
external: true
name: linoffice_data
services:
windows:
image: ghcr.io/dockur/windows:4.34
container_name: LinOffice
privileged: true
environment:
VERSION: "11"
RAM_SIZE: 4G
CPU_CORES: "4"
DISK_SIZE: 64G
USERNAME: MyWindowsUser
PASSWORD: MyWindowsPassword
ARGUMENTS: "-cpu host,arch_capabilities=off"
NETWORK: "user" # پیش‌فرض همین است؛ صراحتاً می‌گذاریم
# USER_PORTS: "3389" # ← نذار تا دوبار نشه
devices:
- /dev/kvm
- /dev/net/tun
cap_add:
- NET_ADMIN
ports:
- "8006:8006"
- "3389:3389/tcp" # ← فقط TCP
volumes:
- "data:/storage:Z"
- "./oem:/oem:Z"
- "/home/h-mousavi/Downloads:/shared:Z"
restart: on-failure
stop_grace_period: 120s
#!/bin/bash
# LinOffice Setup Script
CONTAINER_NAME="LinOffice" # should match the name in the compose.yaml
CONTAINER_EXISTS=0 # 0 = Does not exist (default), 1 = exists
# Absolute filepaths
USER_APPLICATIONS_DIR="${HOME}/.local/share/applications"
APPDATA_PATH="${HOME}/.local/share/linoffice"
# Ensure APPDATA_PATH exists before using it
mkdir -p "$APPDATA_PATH"
SUCCESS_FILE="${APPDATA_PATH}/success"
PROGRESS_FILE="${APPDATA_PATH}/setup_progress.log"
OUTPUT_LOG="${APPDATA_PATH}/setup_output.log"
# Relative filepaths
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
LINOFFICE_DIR="$SCRIPT_DIR"
LINOFFICE="$(realpath "${SCRIPT_DIR}/linoffice.sh")"
COMPOSE_FILE="$(realpath "${SCRIPT_DIR}/config/compose.yaml")"
LINOFFICE_CONF="$(realpath "${SCRIPT_DIR}/config/linoffice.conf")"
OEM_DIR="$(realpath "${SCRIPT_DIR}/config/oem")"
LOCALE_REG_SCRIPT="$(realpath "${SCRIPT_DIR}/config/locale_reg.sh")"
LOCALE_LANG_SCRIPT="$(realpath "${SCRIPT_DIR}/config/locale_lang.sh")"
REGIONAL_REG="$(realpath "${SCRIPT_DIR}/config/oem/registry/regional_settings.reg")"
LOGFILE="${APPDATA_PATH}/windows_install.log"
APPS_DIR="$(realpath "${SCRIPT_DIR}/apps")"
DESKTOP_DIR="$(realpath "${APPS_DIR}/desktop")"
# Available and working freerdp commands
EXISTS_XFREERDP=false
EXISTS_XFREERDP3=false
EXISTS_FLATPAK_FREERDP=false
FREERDP_COMMAND="" # will be checked in the script whether it's xfreerdp, xfreerdp3, or the Flatpak version
FREERDP_SEC_RDP=false
FREERDP_NETWORK_LAN=false
FREERDP_NSC=false
FREERDP_XWAYLAND=false
# Progress tracking states
PROGRESS_REQUIREMENTS="requirements_completed"
PROGRESS_CONTAINER="container_created"
PROGRESS_OFFICE="office_installed"
PROGRESS_DESKTOP="desktop_files_installed"
# Command line arguments
DESKTOP_ONLY=false
FIRSTRUN=false
INSTALL_OFFICE_ONLY=false
HEALTHCHECK=false
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
USE_VENV=0
COMPOSE_COMMAND="podman-compose"
# Redirect all output to the log file
exec > >(stdbuf -oL -eL sed -u 's/\x1b\[[0-9;]*m//g' | tee -a "$OUTPUT_LOG") 2>&1
# Functions to print colored output
print_error() {
echo -e "${RED}ERROR:${NC} $1" >&2
}
print_success() {
echo -e "${GREEN}SUCCESS:${NC} $1"
}
print_info() {
echo -e "${YELLOW}INFO:${NC} $1"
}
print_step() {
echo -e "\n${GREEN}Step $1:${NC} $2"
}
print_progress() {
echo -e "${GREEN}Progress:${NC} $1"
}
# Name: 'use_venv'
# Role: Activate virtual environment if available
use_venv() {
local venv_dir="$HOME/.local/bin/linoffice/venv"
local activate_script="$venv_dir/bin/activate"
print_info "Checking for virtual environment at: $venv_dir"
if [[ -f "$activate_script" ]]; then
print_info "Virtual environment found at $venv_dir"
source "$activate_script"
VENV_PATH="$venv_dir"
USE_VENV=1
PYTHON_PATH="$venv_dir/bin/python3"
print_info "Virtual environment Python: $PYTHON_PATH"
USER_SITE_PATH=$($PYTHON_PATH -m site | grep USER_SITE | awk -F"'" '{print $2}')
PODMAN_COMPOSE_BIN="/home/h-mousavi/.local/bin/linoffice/venv/lib/python3.12/site-packages/podman_compose.py"
if [[ -f "$PODMAN_COMPOSE_BIN" ]]; then
print_info "Using podman-compose from virtual environment: $PODMAN_COMPOSE_BIN"
COMPOSE_COMMAND="$PYTHON_PATH $PODMAN_COMPOSE_BIN"
return 0
else
print_info "podman-compose not found in virtual environment user packages at: $PODMAN_COMPOSE_BIN"
print_info "Checking if virtual environment can access system podman-compose..."
# Check if venv can access system podman-compose (due to --system-site-packages)
if command -v podman-compose &> /dev/null; then
print_info "Virtual environment can access system podman-compose"
COMPOSE_COMMAND="podman-compose"
return 0
else
print_info "podman-compose not available in virtual environment or system"
print_info "Will check for system podman-compose instead"
# Don't return here - let the system check handle it
USE_VENV=0 # Reset to system mode since venv doesn't have podman-compose
return 1
fi
fi
else
print_info "Virtual environment not found at $venv_dir, using system Python"
return 1
fi
}
use_venv
# Function to display usage information
print_usage() {
print_info "Usage: $0 [--desktop] [--firstrun] [--installoffice] [--healthcheck]"
print_info "Options:"
print_info " (no flag) Run the installation script from the beginning"
print_info " --desktop Only recreate the desktop files (.desktop launchers)"
print_info " --firstrun Force RDP and Office installation checks"
print_info " --installoffice Only run the Office installation script script (in case the Windows installation has finished but Office is not installed)"
print_info " --healthcheck Check that the system requirements are met and dependencies are installed and the container is healthy"
exit 1
}
# Parse command line arguments
while [[ $# -gt 0 ]]; do
case $1 in
--desktop)
DESKTOP_ONLY=true
shift
;;
--firstrun)
FIRSTRUN=true
shift
;;
--installoffice)
INSTALL_OFFICE_ONLY=true
shift
;;
--healthcheck)
HEALTHCHECK=true
shift
;;
--help)
print_usage
;;
*)
print_error "Unknown option: $1"
print_usage
;;
esac
done
# Function to exit with error
exit_with_error() {
print_error "$1"
exit 1
}
# Progress tracking functions
function init_progress_file() {
mkdir -p "$(dirname "$PROGRESS_FILE")"
touch "$PROGRESS_FILE"
}
function mark_progress() {
local step=$1
echo "$step" >> "$PROGRESS_FILE"
}
function check_progress() {
local step=$1
if [ -f "$PROGRESS_FILE" ] && grep -q "^$step$" "$PROGRESS_FILE"; then
return 0
else
return 1
fi
}
function clear_progress() {
if [ -f "$PROGRESS_FILE" ]; then
rm "$PROGRESS_FILE"
fi
}
function check_linoffice_container() {
print_info "Checking if LinOffice container exists already"
if podman container exists "$CONTAINER_NAME"; then
print_info "Container exists already."
CONTAINER_EXISTS=1
else
print_info "Container does not yet exist."
CONTAINER_EXISTS=0
fi
}
# Function to detect and set the FreeRDP command
function detect_freerdp_command() {
# Determine availability of all FreeRDP variants and set EXISTS_* flags
EXISTS_XFREERDP=false
EXISTS_XFREERDP3=false
EXISTS_FLATPAK_FREERDP=false
if command -v xfreerdp3 &>/dev/null; then
EXISTS_XFREERDP3=true
fi
if command -v xfreerdp &>/dev/null; then
EXISTS_XFREERDP=true
fi
if command -v flatpak &>/dev/null; then
if flatpak list --columns=application | grep -q "^com.freerdp.FreeRDP$"; then
EXISTS_FLATPAK_FREERDP=true
fi
fi
# Set FREERDP_COMMAND to the first available in priority order: xfreerdp3 > flatpak > xfreerdp
if [ "$EXISTS_XFREERDP3" = true ]; then
FREERDP_COMMAND="xfreerdp3"
elif [ "$EXISTS_FLATPAK_FREERDP" = true ]; then
FREERDP_COMMAND="flatpak run --command=xfreerdp com.freerdp.FreeRDP"
elif [ "$EXISTS_XFREERDP" = true ]; then
FREERDP_COMMAND="xfreerdp"
else
FREERDP_COMMAND="" # Not found
fi
}
# Function to check if all requirements are met to run the Windows VM in Podman
function check_requirements() {
# Exit on any error
set -e
print_info "Starting LinOffice setup script..."
print_step "1" "Checking requirements"
# Check minimum RAM (8 GB)
print_info "Checking minimum RAM"
local ram_line=$((LINENO + 1))
REQUIRED_RAM=7 # 8 GB shows up as 7.6 GiB so best to just set the threshold to 7 in this script
AVAILABLE_RAM="$(free -b | awk '/^Mem:/{print int($2/1024/1024/1024)}')"
if [ "$AVAILABLE_RAM" -lt "$REQUIRED_RAM" ]; then
exit_with_error "Insufficient RAM. Required: ${REQUIRED_RAM}GB, Available: ${AVAILABLE_RAM}GB. \
Please upgrade your system memory to at least ${REQUIRED_RAM}GB.
The Windows VM needs 4 GB of RAM. If you still want to continue with the installation, for example if you are using zswap, you can change the minimum RAM required by editing line $ram_line in $SCRIPT_DIR/setup.sh and then run the setup again."
fi
print_success "Sufficient RAM detected: ${AVAILABLE_RAM}GB"
# Check minimum free storage (64 GB)
check_linoffice_container
if [ "$CONTAINER_EXISTS" -eq 1 ]; then
print_info "Container exists already. Skipping check if sufficient free storage is available."
else
print_info "Checking minimum free storage"
REQUIRED_STORAGE=64
AVAILABLE_STORAGE=$(df -B1G --output=avail /home | tail -n 1 | awk '{print $1}')
if [ "$AVAILABLE_STORAGE" -lt "$REQUIRED_STORAGE" ]; then
exit_with_error "Insufficient free storage. Required: ${REQUIRED_STORAGE}GB, Available: ${AVAILABLE_STORAGE}GB \
Please free up disk space or use a different storage device."
fi
print_success "Sufficient free storage detected: ${AVAILABLE_STORAGE}GB"
fi
# Check if computer supports virtualization
print_info "Checking virtualization support"
if ! command -v lscpu &> /dev/null; then
exit_with_error "lscpu command not found. Please install util-linux package."
fi
# Check for virtualization support
if lscpu | grep -qiE 'virtualization|vmx|svm'; then
echo "Virtualization is supported."
else
exit_with_error "CPU virtualization not supported or not enabled.
HOW TO FIX:
1. Reboot your computer and enter BIOS/UEFI settings (usually F2, F12, Del, or Esc during boot)
2. Look for virtualization settings:
- Intel: Enable 'Intel VT-x' or 'Intel Virtualization Technology'
- AMD: Enable 'AMD-V' or 'SVM Mode'
3. Save settings and reboot
4. If you can't find these options, your CPU may not support virtualization"
fi
# Additional check for KVM support
if [ ! -e /dev/kvm ]; then
exit_with_error "KVM device not available. Virtualization may not be enabled in BIOS.
HOW TO FIX:
1. Ensure virtualization is enabled in BIOS (see previous instructions)
2. Install KVM kernel modules: sudo modprobe kvm
3. For Intel CPUs: sudo modprobe kvm_intel
4. For AMD CPUs: sudo modprobe kvm_amd
5. Reboot if necessary"
fi
print_success "Virtualization support detected: $VIRT_SUPPORT"
# Check if podman is installed
print_info "Checking if podman is installed"
if ! command -v podman &> /dev/null; then
exit_with_error "podman is not installed.
HOW TO FIX:
Ubuntu/Debian: sudo apt update && sudo apt install podman
Fedora/RHEL: sudo dnf install podman
OpenSUSE: sudo zypper install podman
Arch Linux: sudo pacman -S podman
openSUSE: sudo zypper install podman
Or visit: https://podman.io/getting-started/installation"
fi
if ! podman info >/dev/null 2>&1; then
exit_with_error "Podman is not configured correctly or you lack sufficient permissions. Run 'podman info' to diagnose the issue."
fi
PODMAN_VERSION=$(podman --version)
print_success "podman is installed: $PODMAN_VERSION"
# Check if podman-compose is installed
print_info "Checking if podman-compose is installed"
print_info "Python environment: $(if [[ "$USE_VENV" -eq 1 ]]; then echo "Virtual environment at $VENV_PATH"; else echo "System Python"; fi)"
# Determine which Python to use for dependency checks
if [[ "$USE_VENV" -eq 1 ]]; then
PYTHON_CMD="$PYTHON_PATH"
PYTHON_ENV="virtual environment"
else
PYTHON_CMD="python3"
PYTHON_ENV="system"
fi
if [[ "$USE_VENV" -eq 0 ]]; then
# Use system podman-compose, not the one in ~/.local/bin which might be broken
if [[ -x "/usr/bin/podman-compose" ]]; then
COMPOSE_COMMAND="/usr/bin/podman-compose"
print_success "Using system podman-compose: /usr/bin/podman-compose"
elif command -v podman-compose &> /dev/null; then
COMPOSE_COMMAND="podman-compose"
print_success "Using podman-compose from PATH: $(command -v podman-compose)"
else
exit_with_error "podman-compose is not installed.
HOW TO FIX:
Option 1 - Using pip: pip3 install podman-compose
Option 2 - Using package manager:
Ubuntu/Debian: sudo apt install podman-compose
Fedora: sudo dnf install podman-compose
OpenSUSE: sudo zypper install podman-compose
Arch Linux: sudo pacman -S podman-compose
Or visit: https://github.com/containers/podman-compose"
fi
# Check if python-dotenv is installed (dependency of podman-compose)
if ! command -v $PYTHON_CMD &> /dev/null; then
exit_with_error "$PYTHON_CMD command not found. Please install Python 3."
fi
# Show which Python is being used for debugging
PYTHON_FULL_PATH=$(command -v $PYTHON_CMD)
print_info "Using $PYTHON_ENV Python: $PYTHON_FULL_PATH"
if $PYTHON_CMD -c "import dotenv" >/dev/null 2>&1; then
print_success "python-dotenv is installed in $PYTHON_ENV environment."
else
# Provide more detailed error information
print_error "python-dotenv is not installed or not accessible to $PYTHON_FULL_PATH"
print_info "Python version: $($PYTHON_CMD --version 2>&1)"
print_info "Python path: $PYTHON_FULL_PATH"
print_info "Available packages: $($PYTHON_CMD -m pip list 2>/dev/null | grep -i dotenv || echo 'No dotenv packages found')"
exit_with_error "python-dotenv is not installed in $PYTHON_ENV environment.
HOW TO FIX:
Using pip: $PYTHON_CMD -m pip install python-dotenv
If you don't have pip, you can install it with your package manager.
Ubuntu/Debian: sudo apt install python-dotenv
Fedora: sudo dnf install python-dotenv
OpenSUSE: sudo zypper install python-python-dotenv
Arch Linux: sudo pacman -S python-dotenv
If the package is installed but not detected, try:
- Check if you have multiple Python versions: which python3
- Install for the specific Python: $PYTHON_FULL_PATH -m pip install python-dotenv"
fi
else
# When using virtual environment, check if podman-compose is installed in venv
if [[ -f "$PODMAN_COMPOSE_BIN" ]]; then
print_success "podman-compose is installed in virtual environment."
else
exit_with_error "podman-compose is not installed in virtual environment.
HOW TO FIX:
The virtual environment needs podman-compose installed.
Run: $PYTHON_PATH -m pip install podman-compose"
fi
# Check if python-dotenv is installed in virtual environment (dependency of podman-compose)
if $PYTHON_CMD -c "import dotenv" >/dev/null 2>&1; then
print_success "python-dotenv is installed in virtual environment."
else
print_error "python-dotenv is not installed in virtual environment."
print_info "Python version: $($PYTHON_CMD --version 2>&1)"
print_info "Python path: $PYTHON_CMD"
print_info "Available packages: $($PYTHON_CMD -m pip list 2>/dev/null | grep -i dotenv || echo 'No dotenv packages found')"
print_info "User site packages: $($PYTHON_CMD -m site --user-site 2>/dev/null || echo 'Unknown')"
# Check if it's available in user packages (installed with --user flag)
USER_SITE_PACKAGES=$($PYTHON_CMD -m site --user-site 2>/dev/null)
if [[ -n "$USER_SITE_PACKAGES" ]] && [[ -d "$USER_SITE_PACKAGES" ]] && find "$USER_SITE_PACKAGES" -name "*dotenv*" -type d 2>/dev/null | grep -q .; then
print_info "python-dotenv found in user site packages but not importable"
print_info "This might be due to virtual environment configuration issues"
fi
exit_with_error "python-dotenv is not installed in virtual environment.
HOW TO FIX:
The virtual environment needs python-dotenv installed.
Run: $PYTHON_CMD -m pip install python-dotenv
If packages were installed with --user flag by quickstart.sh, try:
$PYTHON_CMD -m pip install --user python-dotenv"
fi
fi
COMPOSE_VERSION=$($COMPOSE_COMMAND --version)
print_success "podman-compose is installed: $COMPOSE_VERSION"
# Check if FreeRDP is available
print_info "Checking if FreeRDP is available"
detect_freerdp_command
local FREERDP_MAJOR_VERSION=""
if [ -n "$FREERDP_COMMAND" ]; then
if [ "$FREERDP_COMMAND" = "xfreerdp" ]; then
FREERDP_MAJOR_VERSION=$(xfreerdp --version | head -n 1 | grep -o -m 1 '\b[0-9]\S*' | head -n 1 | cut -d'.' -f1)
elif [ "$FREERDP_COMMAND" = "xfreerdp3" ]; then
FREERDP_MAJOR_VERSION=$(xfreerdp3 --version | head -n 1 | grep -o -m 1 '\b[0-9]\S*' | head -n 1 | cut -d'.' -f1)
elif [ "$FREERDP_COMMAND" = "flatpak run --command=xfreerdp com.freerdp.FreeRDP" ]; then
FREERDP_MAJOR_VERSION=$(flatpak list --columns=application,version | grep "^com.freerdp.FreeRDP" | awk '{print $2}' | cut -d'.' -f1)
# Check if Flatpak has access to /home
if ! flatpak info --show-permissions com.freerdp.FreeRDP | grep -q "filesystems=.*home"; then
exit_with_error "Flatpak FreeRDP does not have access to /home directory.
HOW TO FIX:
1. Close any running FreeRDP instances
2. Run this command to grant access:
flatpak override --user --filesystem=home com.freerdp.FreeRDP
3. Run this setup script again"
fi
fi
if [[ ! $FREERDP_MAJOR_VERSION =~ ^[0-9]+$ ]] || ((FREERDP_MAJOR_VERSION < 3)); then
exit_with_error "FreeRDP version 3 or greater is required. Detected version: $FREERDP_MAJOR_VERSION"
fi
else
exit_with_error "FreeRDP is not installed
HOW TO FIX:
Option 1 - Using Flatpak and Flathub: flatpak install com.freerdp.FreeRDP
Option 2 - Using package manager:
Ubuntu/Debian: sudo apt install freerdp3-x11
Fedora: sudo dnf install freerdp
OpenSUSE: sudo zypper install freerdp
Arch Linux: sudo pacman -S freerdp"
fi
if ! $FREERDP_COMMAND --version >/dev/null 2>&1; then
exit_with_error "FreeRDP command '$FREERDP_COMMAND' is not functional. Please ensure FreeRDP is correctly installed and configured."
fi
print_success "FreeRDP found. Using FreeRDP command '${FREERDP_COMMAND}'."
# Check if iptables modules are loaded
print_info "Checking iptables kernel modules"
if ! lsmod | grep -q ip_tables || ! lsmod | grep -q iptable_nat; then
print_info "WARNING: iptables kernel modules not loaded. Sharing the /home folder with the Windows VM will not work unless connected via RDP. HOW TO FIX:
Run the following command:
echo -e 'ip_tables\niptable_nat' | sudo tee /etc/modules-load.d/iptables.conf
Then reboot your system."
fi
print_success "iptables modules are loaded"
# Check if most important LinOffice files exist
print_info "Checking for essential setup files"
if [ ! -d "$OEM_DIR" ]; then
exit_with_error "OEM files not found
Please ensure the config/oem directory exists"
fi
# Check OEM directory permissions
if [ ! -r "$OEM_DIR" ] || [ ! -x "$OEM_DIR" ] || ! find "$OEM_DIR" -type f -readable | head -1 >/dev/null 2>&1; then
exit_with_error "Insufficient permissions to access OEM directory: $OEM_DIR
HOW TO FIX:
1. Check directory permissions: ls -ld $OEM_DIR
2. Fix permissions: chmod -R u+rwX $OEM_DIR
3. If using SELinux/AppArmor, you may need to adjust security contexts"
fi
# Check if compose.yaml exists
if [ ! -f "$COMPOSE_FILE.default" ]; then
exit_with_error "Compose file not found: $COMPOSE_FILE.default
Please ensure the file exists in the config directory."
fi
# Check if LinOffice script exists
if [ ! -f "$LINOFFICE_CONF.default" ]; then
exit_with_error "LinOffice configuration file not found: $LINOFFICE_CONF.default
Please ensure the file exists in the config directory."
fi
if [ ! -f "$LINOFFICE" ]; then
exit_with_error "File not found: $LINOFFICE"
fi
print_success "Files found."
# Make scripts executable
print_info "Making scripts executable"
if [ ! -f "$LINOFFICE" ]; then
exit_with_error "File not found: $LINOFFICE
Please ensure the script is in the same directory as this setup script."
fi
if [ ! -f "$LOCALE_REG_SCRIPT" ]; then
exit_with_error "File not found: $LOCALE_REG_SCRIPT
Please ensure the config directory and locale_reg.sh script exist."
fi
if [ ! -f "$LOCALE_LANG_SCRIPT" ]; then
exit_with_error "File not found: $LOCALE_LANG_SCRIPT
Please ensure the config directory and local_compose.sh script exist."
fi
chmod +x "$LINOFFICE" || exit_with_error "Failed to make $LINOFFICE executable"
chmod +x "$LOCALE_REG_SCRIPT" || exit_with_error "Failed to make $LOCALE_REG_SCRIPT executable"
chmod +x "$LOCALE_LANG_SCRIPT" || exit_with_error "Failed to make $LOCALE_LANG_SCRIPT executable"
print_success "Made scripts executable"
# Check for various potential Podman problems
# Check subUID/subGID mappings as some users had problems here
print_info "Checking subUID/subGID mappings"
if ! grep -q "^$(whoami):" /etc/subuid || ! grep -q "^$(whoami):" /etc/subgid; then
exit_with_error "Missing subUID/subGID mappings for the user.
HOW TO FIX:
1. Run: sudo usermod --add-subuids 100000-165535 --add-subgids 100000-165535 $(whoami)
2. Refresh Podman configuration: podman system migrate
3. Verify mappings in /etc/subuid and /etc/subgid"
fi
print_success "subUID/subGID mappings verified."
# Check Podman storage configuration and whether overlay storage driver is used, as some users had problems here
print_info "Checking Podman storage configuration"
driver=$(podman info --format '{{.Store.GraphDriverName}}' 2>/dev/null)
error_msg="Podman is not using overlay storage driver or there's a configuration issue.
HOW TO FIX:
mkdir -p ~/.config/containers
cat > ~/.config/containers/storage.conf <<EOF
[storage]
driver = \"overlay\"
runroot = \"/run/user/\$(id -u)/containers\"
graphroot = \"\$HOME/.local/share/containers/storage\"
EOF
podman system migrate
Then verify with:
podman info --format '{{.Store.GraphDriverName}}'
Expected: overlay"
if ! echo "$driver" | grep -qE "overlay|btrfs"; then
if [ -z "$driver" ]; then
print_info "Podman storage driver is not set. Setting up with overlay storage driver"
mkdir -p ~/.config/containers
cat > ~/.config/containers/storage.conf <<EOF
[storage]
driver = "overlay"
runroot = "/run/user/\$(id -u)/containers"
graphroot = "\$HOME/.local/share/containers/storage"
EOF
# Check running containers before migrate
running_containers=$(podman ps --format '{{.Names}}' 2>/dev/null)
if [ -n "$running_containers" ] && ! [[ "$running_containers" == "LinOffice" ]]; then
# Multiple or non-LinOffice: Prompt user
echo "PROMPT:PODMAN_MIGRATE"
while true; do
read -p "WARNING: podman system migrate will stop ALL running containers and reconfigure storage. This may disrupt other workloads. Continue? (y/n): " -n 1 -r
echo
if [[ $REPLY =~ ^[Yy]$ ]]; then
break
elif [[ $REPLY =~ ^[Nn]$ ]] || [ -z "$REPLY" ]; then
exit_with_error "Setup aborted by user. Please run the script again when ready to migrate."
fi
done
fi
podman system migrate
new_driver=$(podman info --format '{{.Store.GraphDriverName}}' 2>/dev/null)
if ! echo "$new_driver" | grep -qE "overlay|btrfs"; then
exit_with_error "$error_msg"
fi
else
exit_with_error "$error_msg"
fi
fi
# Determine if running rootless or rootful
if podman info --format '{{.Host.Security.Rootless}}' | grep -q true; then
IS_ROOTLESS=true
STORAGE_DIR="$HOME/.local/share/containers/storage"
else
IS_ROOTLESS=false
STORAGE_DIR="/var/lib/containers/storage"
fi
print_info "Podman running in $( $IS_ROOTLESS && echo 'rootless' || echo 'rootful' ) mode"
# Set network directory paths based on rootless status first
if [ "$IS_ROOTLESS" = true ]; then
NETAVARK_DIR="$HOME/.local/share/containers/networks"
CNI_DIR="$HOME/.config/cni/net.d"
else
NETAVARK_DIR="/var/lib/containers/networks"
CNI_DIR="/etc/cni/net.d"
fi
# Now select the correct directory based on the network backend
if [ "$NETWORK_BACKEND" = "netavark" ]; then
NETWORK_DIR="$NETAVARK_DIR"
else
# Default to CNI backend
NETWORK_DIR="$CNI_DIR"
fi
print_info "Podman network directory: $NETWORK_DIR"
# Check if storage directory is accessible
if [ ! -d "$STORAGE_DIR" ] || [ ! -w "$STORAGE_DIR" ]; then
exit_with_error "Podman storage directory inaccessible: $STORAGE_DIR
1. Check if directory exists: ls -ld \"$STORAGE_DIR\"
2. If it exists, fix permissions: $( $IS_ROOTLESS && echo "chmod -R u+rwX \"$STORAGE_DIR\"" || echo "sudo chmod -R u+rwX \"$STORAGE_DIR\"" )
3. If it does not exist, initialize Podman: podman info"
fi
print_success "Podman storage directory verified: $STORAGE_DIR"
# Check which networking backend is in use
print_info "Checking Podman networking is working"
NETWORK_BACKEND=$(podman info --format '{{.Host.NetworkBackend}}' 2>/dev/null)
if [ -z "$NETWORK_BACKEND" ]; then
exit_with_error "Failed to detect Podman's network backend. Make sure Podman is correctly installed and accessible to your user. Run 'podman info' to diagnose."
fi
print_info "Podman is using network backend: $NETWORK_BACKEND"
# Test network creation for all backends
TEST_NET_NAME="linoffice_net_test_$(date +%s)"
print_info "Testing network creation with backend: $NETWORK_BACKEND"
if ! podman network create "$TEST_NET_NAME" >/dev/null 2>&1; then
exit_with_error "Failed to create test network '$TEST_NET_NAME'.
HOW TO FIX:
1. Check Podman logs: journalctl -u podman
2. $( $IS_ROOTLESS && echo 'Ensure user has sufficient permissions.' || echo 'Run as root or check sudo permissions.' )
3. Reinstall network backend:
- For netavark: $( $IS_ROOTLESS && echo 'podman system reset && podman info' || echo 'sudo dnf reinstall netavark || sudo apt install netavark' )
- For CNI: Ensure CNI plugins are installed (e.g., sudo dnf install containernetworking-plugins)
4. Verify SELinux/AppArmor settings if enabled."
fi
print_success "Test network '$TEST_NET_NAME' created successfully."
# Check that network directory exists
if [ ! -d "$NETWORK_DIR" ]; then
print_info "WARNING: Network directory does not exist: $NETWORK_DIR
This might lead to errors."
fi
if [ ! -w "$NETWORK_DIR" ]; then
print_info "WARNING: Network directory not writable: $NETWORK_DIR
This might lead to errors."
fi
# Clean up test network
if podman network exists "$TEST_NET_NAME" >/dev/null 2>&1; then
podman network rm "$TEST_NET_NAME" >/dev/null 2>&1 || print_info "Note: Failed to remove test network '$TEST_NET_NAME', you may remove it manually."
fi
print_success "Podman networking check completed."
# Test basic container creation to catch storage issues early
print_info "Testing basic container functionality..."
if ! timeout 60 podman run --rm alpine:latest echo "test" >/dev/null 2>&1; then
exit_with_error "Basic container test failed. This could indicate storage driver issues.
HOW TO FIX:
1. Check Podman logs: journalctl --user -u podman
2. Try: podman system reset (WARNING: removes all containers/images)
3. Ensure /run/containers and storage directories have correct permissions
4. Check if your filesystem supports overlay mounts"
fi
print_success "Podman test container created and removed successfully."
# Check connectivity to microsoft.com
print_info "Checking connectivity to Microsoft"
if ! curl -s --head --request GET --max-time 10 -L https://www.microsoft.com | grep -q "200"; then
# Alternative method: curl to a reliable fallback endpoint
if ! curl -s --head --request GET --max-time 10 -L https://www.office.com | grep -q "200"; then
exit_with_error "Unable to connect to microsoft.com.
HOW TO FIX:
1. Check your internet connection
2. Verify DNS settings: Ensure you can resolve microsoft.com (try: nslookup microsoft.com)
3. Check firewall settings: Ensure outbound connections to microsoft.com are allowed
4. Try again or contact your network administrator"
else
print_success "Successfully connected to Microsoft"
fi
else
print_success "Successfully connected to Microsoft"
fi
# Run locale scripts
print_step "2" "Detecting region and language settings"
print_info "Running locale configuration scripts"
print_info "Executing: $LOCALE_REG_SCRIPT"
if ! "$LOCALE_REG_SCRIPT"; then
exit_with_error "Failed to execute $LOCALE_REG_SCRIPT (exit code: $?)"
fi
print_info "Executing: $LOCALE_LANG_SCRIPT"
if ! "$LOCALE_LANG_SCRIPT"; then
exit_with_error "Failed to execute $LOCALE_LANG_SCRIPT (exit code: $?)"
fi
print_success "Locale script executed successfully"
# Check if newly created regional.reg exists
print_info "Checking for regional_settings.reg file"
if [ ! -f "$REGIONAL_REG" ]; then
exit_with_error "Required file not found: $REGIONAL_REG
Please ensure the config/oem/registry directory exists and contains regional_settings.reg"
fi
print_success "Found regional_settings.reg file"
}
function setup_logfile() {
# Check if the logfile already exists, if yes rename old one with its last modified date and start with a fresh logfile
mkdir -p "$(dirname "$LOGFILE")"
echo "Logfile: $LOGFILE"
if [ -e "$LOGFILE" ]; then
MODIFIED_DATE=$(stat -c %y "$LOGFILE" | sed 's/[: ]/_/g' | cut -d '.' -f 1)
mv "$LOGFILE" "${LOGFILE%.log}_$MODIFIED_DATE.log"
fi
}
function create_container() {
print_step "3" "Setting up the LinOffice container"
local bootcount=0
local required_boots=4 # Accept 4 as the minimum, but allow for 5 if it happens (see comment below)
# The Windows/Office install process may show 4 or 5 reboots depending on version and script details.
# Sometimes the reboot between install.bat and InstallOffice.ps1 is not a full UEFI reboot, so only 4 are seen.
# We proceed if we see at least 4 reboots, and print a note if more are detected.
# this is how many times the Windows VM needs to boot to be ready
# the string to look for is "BdsDxe: starting Boot0004"
# 3 reboots will be logged during initial Windows until you can see the desktop for the first time
# 1 reboot at the end of install.bat (this is the one that is not always logged for some reason)
# 1 reboot at the end of the InstallOffice.ps1
local result=1 # 0 = success, 1 = failure (assume failure by default)
local download_started=false
local download_finished=false
local install_started=false
local timeout_counter=0
local max_timeout=3600 # 60 minutes maximum wait time between podman-compose log output
local last_activity_time=$(date +%s)
local windows_version=""
# Start podman-compose in the background with unbuffered output and strip ANSI codes
print_info "Starting podman-compose in detached mode..."
# If the compose file doesn't exist yet, initialize it from the default template
if [ ! -f "$COMPOSE_FILE" ]; then
if [ -f "$COMPOSE_FILE.default" ]; then
print_info "Creating $COMPOSE_FILE from default template"
cp "$COMPOSE_FILE.default" "$COMPOSE_FILE" || exit_with_error "Failed to copy $COMPOSE_FILE.default to $COMPOSE_FILE"
else
exit_with_error "Compose file missing: $COMPOSE_FILE and $COMPOSE_FILE.default not found"
fi
fi
if ! $COMPOSE_COMMAND --file "$COMPOSE_FILE" up -d >>"$LOGFILE" 2>&1; then
exit_with_error "Failed to start containers. Check $LOGFILE for details."
fi
# Check if container was actually created
sleep 5
if ! podman ps -a --filter "name=$CONTAINER_NAME" --format "{{.Names}}" | grep -q "^$CONTAINER_NAME$"; then
exit_with_error "Container $CONTAINER_NAME was not created successfully. Check $LOGFILE for detailed error messages."
fi
print_info "Tailing logs from container: $CONTAINER_NAME"
podman logs -f --timestamps "$CONTAINER_NAME" 2>&1 | \
stdbuf -oL -eL sed -u 's/\x1b\[[0-9;]*m//g' >> "$LOGFILE" &
log_pid=$!
print_info "Monitoring container setup progress..."
# Monitor the logfile for progress
while true; do
local current_time=$(date +%s)
if [ $((current_time - last_activity_time)) -gt $max_timeout ]; then
result=1
exit_with_error "Container setup timed out after $((max_timeout/60)) minutes."
fi
# Read the logfile if it exists
if [ -f "$LOGFILE" ]; then
# Get file size to detect new activity
local current_size=$(stat -c%s "$LOGFILE" 2>/dev/null || echo "0")
local previous_size=${previous_size:-0}
if [ "$current_size" -gt "$previous_size" ]; then
last_activity_time=$current_time
previous_size=$current_size
fi
# Check for download progress
if ! $download_started && grep -q "Downloading Windows" "$LOGFILE"; then
print_step "4" "Starting Windows download (about 5 GB). This will take a while depending on your Internet speed."
download_started=true
last_activity_time=$current_time
windows_version=$(grep "Downloading Windows" "$LOGFILE" | tail -1 | grep -oE '10|11')
fi
# Output download progress at each percent
if $download_started && ! $download_finished; then
# Parse wget progress line: percent and speed
last_percent=${last_percent:--1}
progress_line=$(grep -E "%" "$LOGFILE" | grep -E "[0-9.]+[MK]" | tail -1)
pct=$(echo "$progress_line" | grep -oE "[ ]{1,3}[0-9]{1,3}%" | tail -1 | tr -d ' %')
speed=$(echo "$progress_line" | grep -oE "[0-9.]+[MK]" | tail -1)
if [[ "$pct" =~ ^[0-9]+$ ]] && [ "$pct" -gt "$last_percent" ] && [ "$pct" -le 100 ]; then
if [ -n "$windows_version" ]; then
print_progress "Downloading Windows ${windows_version}: ${pct}% | Speed: ${speed}B/s"
else
print_progress "Downloading Windows: ${pct}% | Speed: ${speed}B/s"
fi
last_percent=$pct
fi
fi
# Check for download completion
if $download_started && ! $download_finished && grep -q "100%" "$LOGFILE"; then
print_success "Windows download finished"
download_finished=true
last_activity_time=$current_time
# Monitor for either "Windows started" or "Shutdown completed" after download
print_info "Waiting for Windows to start after download..."
local monitor_timeout=300 # 5 minutes
local monitor_elapsed=0
local monitor_interval=5
while [ $monitor_elapsed -lt $monitor_timeout ]; do
if grep -q "Windows started" "$LOGFILE"; then
print_step "5" "Installing Windows. This will take a while."
install_started=true
last_activity_time=$(date +%s)
break
fi
if grep -q "Shutdown completed" "$LOGFILE"; then
exit_with_error "Windows installation failed: Detected shutdown before Windows started. Check $LOGFILE for details and see https://github.com/dockur/windows/issues for troubleshooting."
fi
sleep $monitor_interval
monitor_elapsed=$((monitor_elapsed + monitor_interval))
done
if [ $monitor_elapsed -ge $monitor_timeout ]; then
exit_with_error "Timeout waiting for Windows to start after download. Check $LOGFILE for details."
fi
fi
# Check for error conditions
if grep -iq "error\|failed\|cannot\|timeout" "$LOGFILE" | tail -10 | grep -q "FATAL\|ERROR"; then
print_error "Error detected in container logs. Check $LOGFILE for details."
# Don't exit immediately, but log the concern
fi
# Check for boot progress
local current_boots=0
current_boots=$(grep -c "BdsDxe: starting Boot0004" "$LOGFILE" 2>/dev/null) || current_boots=0
if [ "$current_boots" -gt "$bootcount" ]; then
bootcount=$current_boots
print_success "Reboot $bootcount of $required_boots completed"
if [ "$bootcount" -eq 3 ]; then
print_success "Windows installation finished"
print_step "6" "Downloading and installing Office (about 3 GB). This will take a while."
fi
if [ "$bootcount" -gt 4 ]; then
print_info "More than 4 reboots detected ($bootcount). This is expected in some cases. Proceeding."
fi
last_activity_time=$current_time
if [ "$bootcount" -ge "$required_boots" ]; then
result=0
break
fi
fi
fi
# Sleep briefly to avoid high CPU usage
sleep 5
done
# Stop the background log tailing process
if kill -0 "$log_pid" 2>/dev/null; then
kill "$log_pid" 2>/dev/null || true
fi
# Then check success/failure
if [ "$result" -eq 0 ]; then
sleep 5
if ! podman ps -q --filter "name=$CONTAINER_NAME" | grep -q .; then
exit_with_error "Container setup completed but container is not running. Check $LOGFILE for details."
else
print_success "Container setup completed successfully"
return 0
fi
else
exit_with_error "Container setup failed. Check $LOGFILE for details or visit 127.0.0.1:8006 in your web browser."
fi
}
function verify_container_health() {
print_info "Verifying container health..."
# Ensure container exists, otherwise create it
if ! podman container exists "$CONTAINER_NAME" 2>/dev/null; then
print_info "Container does not exist. Creating it now with podman-compose up -d..."
if ! $COMPOSE_COMMAND --file "$COMPOSE_FILE" up -d; then
print_error "Failed to create container via compose up -d"
return 1
fi
print_info "Waiting for container to boot..."
sleep 20
fi
# Check if container is running, otherwise start it
if ! podman ps -q --filter "name=$CONTAINER_NAME" | grep -q .; then
print_info "Container is not running. Attempting to start it..."
if ! $COMPOSE_COMMAND --file "$COMPOSE_FILE" start; then
print_error "Failed to start container"
print_info "Container may be in an improper state. Try these commands to fix it:
1. podman rm -f LinOffice
2. $COMPOSE_COMMAND --file config/compose.yaml up -d"
return 1
fi
print_info "Waiting for container to boot..."
sleep 20
fi
# Check container logs for any obvious errors
local container_logs=$(podman logs --tail 50 "$CONTAINER_NAME" 2>/dev/null || echo "")
if echo "$container_logs" | grep -iq "error\|failed\|fatal"; then
print_error "Container logs show potential issues"
print_info "If the container is in an improper state, try these commands to fix it:
1. podman rm -f LinOffice
2. $COMPOSE_COMMAND --file config/compose.yaml up -d"
return 1
fi
return 0
}
# Update the lines FREERDP_CONFIG="", RDP_FLAGS="", and XWAYLAND="" in linoffice.conf
function update_config_file() {
local conf_file="$LINOFFICE_CONF"
if [ ! -f "$LINOFFICE_CONF" ]; then
exit_with_error "LinOffice configuration file not found: $LINOFFICE_CONF
Please ensure the file exists in the config directory."
fi
print_info "Updating config file"
# Update XWAYLAND line
sed -i "/^XWAYLAND=/ s/.*/XWAYLAND=\"${FREERDP_XWAYLAND}\"/" "$conf_file"
# Handle RDP_FLAGS: get current value, modify based on conditions, then update line
local rdp_flags
if grep -q "^RDP_FLAGS=" "$conf_file"; then
rdp_flags=$(grep "^RDP_FLAGS=" "$conf_file" | cut -d= -f2- | sed 's/^"//; s/"$//')
else
rdp_flags=""
fi
# Remove flags if corresponding var is false
if [[ "$FREERDP_NSC" != "true" ]]; then
rdp_flags=$(echo "$rdp_flags" | sed 's| /gfx:off /nsc||g; s|^/gfx:off /nsc ||; s| /gfx:off /nsc$||')
fi
if [[ "$FREERDP_NETWORK_LAN" != "true" ]]; then
rdp_flags=$(echo "$rdp_flags" | sed 's| /network:lan||g; s|^/network:lan ||; s| /network:lan$||')
fi
# if [[ "$FREERDP_SEC_RDP" != "true" ]]; then
# rdp_flags=$(echo "$rdp_flags" | sed 's| /sec:rdp||g; s|^/sec:rdp ||; s| /sec:rdp$||')
# fi
# Add flags if corresponding var is true (trim and add if not already present, but per spec, just add)
if [[ "$FREERDP_NSC" == "true" ]]; then
if ! echo "$rdp_flags" | grep -q "/gfx:off /nsc"; then
rdp_flags="$rdp_flags /gfx:off /nsc"
fi
fi
if [[ "$FREERDP_NETWORK_LAN" == "true" ]]; then
if ! echo "$rdp_flags" | grep -q "/network:lan"; then
rdp_flags="$rdp_flags /network:lan"
fi
fi
# if [[ "$FREERDP_SEC_RDP" == "true" ]]; then
# if ! echo "$rdp_flags" | grep -q "/sec:rdp"; then
# rdp_flags="$rdp_flags /sec:rdp"
# fi
# fi
# Trim leading/trailing spaces and update the line
rdp_flags=$(echo "$rdp_flags" | sed 's|^ ||; s| $||')
if [[ -n "$rdp_flags" ]]; then
# Escape sed replacement specials in flags
local rdp_flags_escaped
rdp_flags_escaped=$(printf '%s' "$rdp_flags" | sed 's/[&|]/\\&/g')
sed -i "/^RDP_FLAGS=/ s|.*|RDP_FLAGS=\"$rdp_flags_escaped\"|" "$conf_file"
else
sed -i "/^RDP_FLAGS=/ s|.*|RDP_FLAGS=\"\"|" "$conf_file"
fi
# Update FREERDP_COMMAND line (escape sed replacement specials)
local freerdp_command_escaped
freerdp_command_escaped=$(printf '%s' "$FREERDP_COMMAND" | sed 's/[&|]/\\&/g')
sed -i "/^FREERDP_COMMAND=/ s|\"[^\"]*\"|\"${freerdp_command_escaped}\"|" "$conf_file"
}
function check_available() {
if [ -z "$FREERDP_COMMAND" ]; then
detect_freerdp_command
fi
print_step "7" "Checking if everything is set up correctly"
print_info "Checking if RDP server is available"
# Prepare candidate list based on availability
local candidates=()
if [ "$EXISTS_XFREERDP3" = true ]; then candidates+=("xfreerdp3"); fi
if [ "$EXISTS_FLATPAK_FREERDP" = true ]; then candidates+=("flatpak run --command=xfreerdp com.freerdp.FreeRDP"); fi
if [ "$EXISTS_XFREERDP" = true ]; then candidates+=("xfreerdp"); fi
# Helper to run one attempt and detect success
_run_attempt() {
local cmd_str="$1"; shift
local extra_flags=("$@")
local output=""
local use_xwayland=false
local arg_flags=()
for f in "${extra_flags[@]}"; do
case "$f" in
XWAYLAND)
use_xwayland=true
;;
*)
arg_flags+=("$f")
;;
esac
done
# Build command arguments
local cmd_args=(
/cert:ignore
/u:MyWindowsUser
/p:MyWindowsPassword
/v:127.0.0.1
/port:3389
"${arg_flags[@]}"
/app:program:cmd.exe,cmd:'/c tsdiscon'
)
# Execute command based on type
if [[ "$cmd_str" == flatpak* ]]; then
local full_cmd="$cmd_str"
for arg in "${cmd_args[@]}"; do
full_cmd="$full_cmd $arg"
done
print_info "trying command: timeout 30 bash -c '$full_cmd'"
if [ "$use_xwayland" = true ]; then
output=$(timeout 30 bash -c "WAYLAND_DISPLAY= $full_cmd" 2>&1)
else
output=$(timeout 30 bash -c "$full_cmd" 2>&1)
fi
else
print_info "trying command: timeout 30 $cmd_str ${cmd_args[*]}"
if [ "$use_xwayland" = true ]; then
output=$(timeout 30 env WAYLAND_DISPLAY= "$cmd_str" "${cmd_args[@]}" 2>&1)
else
output=$(timeout 30 "$cmd_str" "${cmd_args[@]}" 2>&1)
fi
fi
# Log everything, print ERROR lines only to terminal
echo "$output" >>"$LOGFILE"
local error_lines
error_lines=$(echo "$output" | grep -F "ERROR" || true)
if [ -n "$error_lines" ]; then echo "$error_lines"; fi
# Success when user logoff detected
if echo "$output" | grep -q "ERRINFO_LOGOFF_BY_USER"; then
print_success "RDP server is available (user logoff detected)"
print_info "Used command: $cmd_str ${cmd_args[*]}"
return 0
fi
return 1
}
# Helper to run full test sequence
_run_test_sequence() {
local c
# 1) Minimal command with detected FREERDP_COMMAND
if [ -n "$FREERDP_COMMAND" ]; then
if _run_attempt "$FREERDP_COMMAND"; then
return 0
fi
fi
# 2) Minimal command with all available candidates
for c in "${candidates[@]}"; do
if _run_attempt "$c"; then
FREERDP_COMMAND="$c"
update_config_file
return 0
fi
done
# Utility: are we on Wayland?
local on_wayland=false
if [ "$XDG_SESSION_TYPE" = "wayland" ] || [ -n "$WAYLAND_DISPLAY" ]; then
on_wayland=true
fi
# 3) Xwayland variant (Wayland sessions only)
if [ "$on_wayland" = true ]; then
for c in "${candidates[@]}"; do
if _run_attempt "$c" "XWAYLAND"; then
FREERDP_COMMAND="$c"
FREERDP_XWAYLAND=true
update_config_file
return 0
fi
done
fi
# 4) /sec:rdp variant
# for c in "${candidates[@]}"; do
# if _run_attempt "$c" "/sec:rdp"; then
# FREERDP_COMMAND="$c"
# update_config_file
# return 0
# fi
# done
# 5) /network:lan variant
for c in "${candidates[@]}"; do
if _run_attempt "$c" "/network:lan"; then
FREERDP_COMMAND="$c"
FREERDP_NETWORK_LAN=true
update_config_file
return 0
fi
done
# 6) /nsc variant
for c in "${candidates[@]}"; do
if _run_attempt "$c" "/gfx:off /nsc"; then
FREERDP_COMMAND="$c"
FREERDP_NSC=true
update_config_file
return 0
fi
done
# 7) All together (xwayland, /sec:rdp, /network:lan, /gfx:off /nsc)
if [ "$on_wayland" = true ]; then
for c in "${candidates[@]}"; do
if _run_attempt "$c" "XWAYLAND" "/network:lan" "/nsc"; then
FREERDP_COMMAND="$c"
FREERDP_XWAYLAND=true
FREERDP_NETWORK_LAN=true
FREERDP_NSC=true
update_config_file
return 0
fi
done
else
for c in "${candidates[@]}"; do
if _run_attempt "$c" "/network:lan" "/nsc"; then
FREERDP_COMMAND="$c"
FREERDP_NETWORK_LAN=true
FREERDP_NSC=true
update_config_file
return 0
fi
done
fi
return 1
}
# Ensure container is up before testing
if ! verify_container_health; then
print_error "Container is not healthy; cannot perform RDP checks."
return 1
fi
# Run initial test sequence
if _run_test_sequence; then
update_config_file
return 0
fi
# 8) Reboot container and retry
print_info "Rebooting Windows VM container and retrying checks..."
"$COMPOSE_COMMAND" --file "$COMPOSE_FILE" restart >>"$LOGFILE" 2>&1 || true
sleep 10
if ! verify_container_health; then
print_error "Problem with container after reboot."
else
if _run_test_sequence; then
update_config_file
return 0
fi
fi
# 9) Prompt user to log out via VNC and retry once
print_error "Unable to connect via RDP automatically."
echo
print_info "What to do now:"
print_info "1) Open your browser to 127.0.0.1:8006"
print_info "2) Login with password: MyWindowsPassword"
print_info "3) In Windows, sign out the user (do not shutdown)."
echo
echo "PROMPT:VNC_SIGN_OUT_AND_RETRY"
local answer
read -r -p "Try again now? [Y/n]: " answer
answer=${answer:-Y}
if [[ "$answer" =~ ^[Yy]$ ]]; then
if _run_test_sequence; then
update_config_file
return 0
fi
fi
return 1
}
function check_success() {
if [ -z "$FREERDP_COMMAND" ]; then
detect_freerdp_command
fi
print_info "Checking if Office is installed"
local freerdp_pid=""
local elapsed_time=0
local retry_count=0
local max_retries=10
local check_interval=10 # Try again after 10 seconds
local installation_timeout=900 # 15 minutes timeout for Office download and installation
# Function to cleanup FreeRDP process
cleanup_freerdp() {
if [ -n "$freerdp_pid" ] && kill -0 "$freerdp_pid" 2>/dev/null; then
print_info "Cleaning up FreeRDP process (PID: $freerdp_pid)"
kill -TERM "$freerdp_pid" 2>/dev/null || true
sleep 3
kill -KILL "$freerdp_pid" 2>/dev/null || true
fi
}
# Register cleanup function to run on script exit
trap cleanup_freerdp EXIT
# Clear any existing success file once before attempting connections
rm -f "$SUCCESS_FILE"
# Build command arguments based on successful availability check
local cmd_args=(
/cert:ignore
+home-drive
/u:MyWindowsUser
/p:MyWindowsPassword
/v:127.0.0.1
/port:3389
)
# Add flags from successful connection test
# if [ "$FREERDP_SEC_RDP" = true ]; then
# cmd_args+=("/sec:rdp")
# fi
if [ "$FREERDP_NETWORK_LAN" = true ]; then
cmd_args+=("/network:lan")
fi
cmd_args+=(/app:program:powershell.exe,cmd:'-ExecutionPolicy Bypass -File C:\\OEM\\FirstRDPRun.ps1')
# Retry loop for FreeRDP connection
while [ $retry_count -lt $max_retries ]; do
retry_count=$((retry_count + 1))
print_info "Starting FreeRDP connection to mount home directory (Attempt $retry_count of $max_retries)..."
# Start FreeRDP in the background with home-drive enabled
if [[ "$FREERDP_COMMAND" == flatpak* ]]; then
local full_cmd="$FREERDP_COMMAND"
for arg in "${cmd_args[@]}"; do
full_cmd="$full_cmd $arg"
done
if [ "$FREERDP_XWAYLAND" = true ]; then
bash -c "WAYLAND_DISPLAY= $full_cmd" >>"$LOGFILE" 2>&1 &
else
bash -c "$full_cmd" >>"$LOGFILE" 2>&1 &
fi
else
if [ "$FREERDP_XWAYLAND" = true ]; then
env WAYLAND_DISPLAY= "$FREERDP_COMMAND" "${cmd_args[@]}" >>"$LOGFILE" 2>&1 &
else
"$FREERDP_COMMAND" "${cmd_args[@]}" >>"$LOGFILE" 2>&1 &
fi
fi
freerdp_pid=$!
# Wait briefly and check if FreeRDP started successfully
sleep 5
if kill -0 "$freerdp_pid" 2>/dev/null; then
print_success "FreeRDP connection established successfully (PID: $freerdp_pid)"
break
else
wait $freerdp_pid 2>/dev/null
local exit_code=$?
print_error "FreeRDP failed to start or exited immediately (exit code: $exit_code)"
if [ $retry_count -lt $max_retries ]; then
print_info "Retrying in 10 seconds..."
sleep 10
else
print_error "Max retries ($max_retries) reached. Check log file at $LOGFILE for details."
return 1
fi
fi
done
# Reset elapsed time for installation monitoring
elapsed_time=0
print_info "Waiting for Office installation to complete (timeout: $((installation_timeout/60)) minutes)..."
# Monitor for success file creation
while [ $elapsed_time -lt $installation_timeout ]; do
# Check if success file exists
if [ -f "$SUCCESS_FILE" ]; then
print_success "Success file detected - Office installation is complete!"
cleanup_freerdp
return 0
fi
# Check if FreeRDP process is still running
if ! kill -0 "$freerdp_pid" 2>/dev/null; then
wait $freerdp_pid 2>/dev/null
local exit_code=$?
# Check if success file was created before process ended
if [ -f "$SUCCESS_FILE" ]; then
print_success "Success file detected - Office installation is complete!"
return 0
fi
print_error "FreeRDP connection terminated (exit code: $exit_code)"
print_info "Checking if success file was created..."
sleep 2
if [ -f "$SUCCESS_FILE" ]; then
print_success "Success file found - Office installation completed successfully!"
return 0
else
print_error "Success file not found. Installation may have failed."
print_info "Check log file at $LOGFILE for details"
return 1
fi
fi
sleep $check_interval
elapsed_time=$((elapsed_time + check_interval))
done
# Timeout reached
print_error "Timeout waiting for Office installation to complete after $((installation_timeout / 60)) minutes"
print_info "Check log file at $LOGFILE for details"
# Final check for success file
if [ -f "$SUCCESS_FILE" ]; then
print_success "Success file found during cleanup - Office installation completed!"
cleanup_freerdp
return 0
fi
cleanup_freerdp
return 1
}
function desktop_files() {
print_step "8" "Installing .desktop files (app launchers)"
# Check if required directories exist
if [ ! -d "$DESKTOP_DIR" ]; then
exit_with_error "Error: Desktop directory not found: $DESKTOP_DIR"
fi
if [ ! -d "$USER_APPLICATIONS_DIR" ]; then
mkdir -p "$USER_APPLICATIONS_DIR" || exit_with_error "Failed to create $USER_APPLICATIONS_DIR"
fi
if [ ! -w "$USER_APPLICATIONS_DIR" ]; then
exit_with_error "No write permissions for $USER_APPLICATIONS_DIR"
fi
# List of Office apps
local apps=("excel" "word" "powerpoint" "onenote" "outlook" "linoffice")
local INSTALLED_COUNT=0
print_info "Processing .desktop files..."
echo "Number of apps found: ${#apps[@]}"
echo "Apps are: ${apps[*]}"
for app in "${apps[@]}"; do
echo "Starting to process app: $app"
local desktop_file="$DESKTOP_DIR/$app.desktop"
echo "Processing: $app.desktop"
# Check if source file exists
if [ ! -f "$desktop_file" ]; then
echo " Error: $app.desktop not found"
continue
fi
# Create corrected .desktop file with absolute paths
temp_file=$(mktemp) || {
echo " Error: Failed to create temporary file"
continue
}
# Replace /PATH/ with LINOFFICE_DIR and write to temp file
if ! sed "s|/PATH/|$LINOFFICE_DIR/|g" "$desktop_file" > "$temp_file"; then
echo " Error: sed command failed"
rm -f "$temp_file"
continue
fi
# Copy to user applications directory
if ! cp "$temp_file" "${USER_APPLICATIONS_DIR}/$app.desktop"; then
echo " Error: Failed to copy to applications directory"
rm -f "$temp_file"
continue
fi
# Make it executable
if ! chmod +x "${USER_APPLICATIONS_DIR}/$app.desktop"; then
echo " Error: Failed to make executable"
rm -f "$temp_file"
continue
fi
# Clean up temp file
rm -f "$temp_file"
echo " Installed: ${USER_APPLICATIONS_DIR}/$app.desktop"
((INSTALLED_COUNT++))
echo "Debug: Finished processing $app"
done
print_info "App launchers installed: $INSTALLED_COUNT"
if [ $INSTALLED_COUNT -gt 0 ]; then
print_info "Updating desktop database"
if command -v update-desktop-database >/dev/null 2>&1; then
update-desktop-database "$USER_APPLICATIONS_DIR" 2>/dev/null || true
print_success "Desktop database updated"
else
print_info "Note: update-desktop-database not found, skipping database update"
fi
print_success "Installation complete! The applications should now appear in your application menu."
print_info "Installed applications:"
for app in "${apps[@]}"; do
if [ -f "${USER_APPLICATIONS_DIR}/$app.desktop" ]; then
display_name=$(grep "^Name=" "$DESKTOP_DIR/$app.desktop" | cut -d'=' -f2)
echo " - $display_name"
fi
done
print_info "To uninstall, remove the files from: $USER_APPLICATIONS_DIR"
print_info "To recreate them, run the script with the --desktop flag."
else
print_error "No files were installed."
return 1
fi
}
# Function to run InstallOffice.ps1 via FreeRDP
try_install_office() {
if [ -z "$FREERDP_COMMAND" ]; then
detect_freerdp_command
fi
print_info "Running Office installation script via FreeRDP..."
# Connection timeout in milliseconds (e.g. 10000 = 10 seconds)
local connection_timeout=10000
# Build command arguments
local cmd_args=(
/cert:ignore
+home-drive
/u:MyWindowsUser
/p:MyWindowsPassword
/v:127.0.0.1
/port:3389
/timeout:$connection_timeout
/app:program:powershell.exe,cmd:'-ExecutionPolicy Bypass -File C:\\OEM\\InstallOffice.ps1'
)
# Add flags from successful connection test
if [ "$FREERDP_NETWORK_LAN" = true ]; then
cmd_args+=("/network:lan")
fi
# Run FreeRDP (handle flatpak vs system binary, Xwayland if needed)
if [[ "$FREERDP_COMMAND" == flatpak* ]]; then
local full_cmd=("$FREERDP_COMMAND" "${cmd_args[@]}")
if [ "$FREERDP_XWAYLAND" = true ]; then
print_info "Running via Flatpak with Xwayland disabled (forcing X11 fallback)"
bash -c "WAYLAND_DISPLAY= ${full_cmd[*]}" >>"$LOGFILE" 2>&1
else
bash -c "${full_cmd[*]}" >>"$LOGFILE" 2>&1
fi
else
if [ "$FREERDP_XWAYLAND" = true ]; then
print_info "Running system FreeRDP with Xwayland disabled (forcing X11 fallback)"
env WAYLAND_DISPLAY= "$FREERDP_COMMAND" "${cmd_args[@]}" >>"$LOGFILE" 2>&1
else
"$FREERDP_COMMAND" "${cmd_args[@]}" >>"$LOGFILE" 2>&1
fi
fi
local exit_code=$?
if [ $exit_code -eq 0 ]; then
print_success "Office installation script executed successfully via FreeRDP."
return 0
else
print_error "FreeRDP failed to run Office installation script (exit code: $exit_code)"
return 1
fi
}
# Main logic
# If --healthcheck flag is set, only run these tests without writing to progress file
if [ "$HEALTHCHECK" = true ]; then
check_requirements
check_linoffice_container
verify_container_health
if check_available; then
print_success "Everything seems fine"
else
print_error "Unable to connect via RDP"
fi
exit 0
fi
init_progress_file
# If --installoffice flag is set, only run InstallOffice.ps1 via FreeRDP
if [ "$INSTALL_OFFICE_ONLY" = true ]; then
print_info "Running InstallOffice.ps1 via FreeRDP (--installoffice mode)..."
if try_install_office; then
print_success "Office installation script executed successfully!"
else
exit_with_error "Failed to run Office installation script via FreeRDP."
fi
exit 0
fi
# If --desktop flag is set, only run desktop_files
if [ "$DESKTOP_ONLY" = true ]; then
print_info "Recreating desktop files..."
if desktop_files; then
mark_progress "$PROGRESS_DESKTOP"
print_success "App launchers (.desktop files) created successfully!"
else
exit_with_error "Failed to create app launchers (.desktop files)"
fi
exit 0
fi
# If --firstrun is set, remove the office_installed progress marker
if [ "$FIRSTRUN" = true ]; then
print_info "--firstrun specified: Forcing RDP and Office install checks."
if [ -f "$PROGRESS_FILE" ]; then
sed -i "/$PROGRESS_OFFICE/d" "$PROGRESS_FILE"
fi
fi
# Check requirements if not already completed
if ! check_progress "$PROGRESS_REQUIREMENTS"; then
if check_requirements; then
mark_progress "$PROGRESS_REQUIREMENTS"
else
echo "Requirements check failed. Cannot proceed with container setup."
exit 1
fi
else
print_info "Requirements check already completed, skipping..."
fi
# Check container status and create if needed
check_linoffice_container
if [ "$CONTAINER_EXISTS" -eq 0 ] && ! check_progress "$PROGRESS_CONTAINER"; then
print_info "Container does not exist, proceeding with setup and creation."
setup_logfile
if create_container; then
mark_progress "$PROGRESS_CONTAINER"
else
exit_with_error "Container creation failed"
fi
else
if check_progress "$PROGRESS_CONTAINER"; then
print_info "Container already created, skipping creation step."
else
print_info "Skipping container creation as LinOffice container already exists."
fi
fi
# Wait for RDP and check Office installation if not already completed or if --firstrun is set
if ! check_progress "$PROGRESS_OFFICE" || [ "$FIRSTRUN" = true ]; then
# If --firstrun, ensure the container is running before checking RDP
if [ "$FIRSTRUN" = true ]; then
if ! podman ps -q --filter "name=$CONTAINER_NAME" | grep -q .; then
print_info "Container is not running. Starting LinOffice container for --firstrun..."
if ! $COMPOSE_COMMAND --file "$COMPOSE_FILE" start; then
exit_with_error "Failed to start LinOffice container for --firstrun."
fi
print_info "Waiting 20 seconds for container to boot..."
sleep 20
fi
fi
if ! check_available; then
exit_with_error "Failed to connect to RDP server"
fi
if ! check_success; then
exit_with_error "Office installation failed or timed out"
fi
mark_progress "$PROGRESS_OFFICE"
else
print_info "Office installation already completed, skipping..."
fi
# Install desktop files if not already completed
if ! check_progress "$PROGRESS_DESKTOP"; then
if desktop_files; then
mark_progress "$PROGRESS_DESKTOP"
else
exit_with_error "Failed to install desktop files"
fi
else
print_info "Desktop files already installed, skipping this step. To recreate them, run the script with the --desktop flag."
fi
# Clean up success file
rm -f "$SUCCESS_FILE"
print_success "LinOffice setup completed successfully!"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment