summaryrefslogtreecommitdiff
path: root/session-common.sh.inc
blob: ab7379daa78b145b097f68f4513a5214494aba3b (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
# Parabola Install Wizard - common functions
#
# Copyright (C) 2020,2022 bill-auger <bill-auger@programmer.net>
#
# SPDX-License-Identifier: GPL-3.0-or-later
#
# This file is part of Parabola Install Wizard.
#
# Parabola Install Wizard is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Parabola Install Wizard is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Parabola Install Wizard. If not, see <http://www.gnu.org/licenses/>.


readonly THIS_DIR="$(cd $(dirname ${BASH_SOURCE[0]}) ; pwd)"

readonly DEF_KEYMAP='us'
readonly DEF_LOCALE='en_US'
readonly DEF_LANGUAGE='en_US.UTF-8'
readonly DEF_TRKEY='en'
readonly DEF_INSTALL='offline'
readonly DEF_PKG_SET='standard'
readonly DEF_INIT='systemd'
readonly DEF_WMDE='cli'
readonly DEF_HOSTNAME='parabola'
readonly DEF_TIMEZONE='UTC'


## sanity checks ##

readonly DIALOG_ERR_MSG="ERROR: can not find the \`dialog\` program"
readonly PRIVILEGE_ERR_MSG="ERROR: this program requires superuser privilege"
readonly STATEFILE_ERR_MSG="ERROR: can not write to state file"
which dialog &> /dev/null             || ! echo "${DIALOG_ERR_MSG}"    || exit
(( ! $EUID ))                         || ! echo "${PRIVILEGE_ERR_MSG}" || exit
[[ -w "${THIS_DIR}"/.session_state ]] || ! echo "${STATEFILE_ERR_MSG}" || exit


## state helpers ##

SetStateVar() # (var_name value*)
{
  local var_name=$1 ; shift ;
  local value="$*"

  sed -i "/^${var_name}=.*/d"    "${THIS_DIR}"/.session_state
  echo "${var_name}=${value}" >> "${THIS_DIR}"/.session_state
}

GetStateVar() # (var_name [def_value])
{
  local var_name=$1
  local def_value=$2
  local stored_value=$(grep "${var_name}=" "${THIS_DIR}"/.session_state | cut -d '=' -f 2)

  if   [[ -n "${stored_value}" ]] && [[ "${stored_value}" != '_UNDEFINED_' ]]
  then echo ${stored_value}
  else echo ${def_value}
  fi
}


## dialog prompt helpers ##

# WizardDlg() notes:
# when the "Cancel" button or <ESC> key is pressed, in a 'msgbox',
#    dialog exits with a non-zero status; and nothing is printed to STDOUT
# install.sh runs under `set -e`; so we can not propogate the non-zero status,
#   otherwise, all callers of these helpers would need to supress it
# callers should instead, exit upon the empty result, when a selection is mandatory
# because an 'inputbox' may be blank, when the "OK" button is pressed, we emit a space,
#   in order to maintain the invariant, such that the "OK" result is never empty
#
# the exception is 'yesno' dialogs, which exit with a non-zero status upon the "No" button
# the "Cancel" and "No" buttons are identical, and indistinguishable at run-time
# the $YESNO_YES or $YESNO_NO values are printed explicitly for 'yesno' dialogs,
#   so that callers can handle the 'yesno' dialogs in the same way as 'msgbox' dialogs
readonly CANCEL_STATUS=1

# dialog prompts for session-init.sh
InitDlg() # (title prompt default_option options*)
{
  local title="$1"
  local prompt="$2"
  local default_option="$3"
  local options=("${@:4}")

  dialog --stdout --sleep 1 --timeout 30 --no-tags --no-cancel   \
         --backtitle    "$title"                                 \
         --default-item "${default_option}"                      \
         --menu         "$prompt" $H $W $N_ITEMS "${options[@]}"
}

# dialog prompts for install.sh
WizardDlg() # (title dialog_args*)
{
  # ASSERT: all callers should pass the dialog type as positional parameter #2

  local title="$1"
  local options=("${@:2}")
  local dialog_type=${options[0]}
  local result
  local status

  result=$( DIALOG_ESC=${CANCEL_STATUS}                \
            dialog --stdout --insecure --sleep 1       \
                   --backtitle "${TR[wizard-${Lang}]}" \
                   --title     "${title}"              \
                   "${options[@]}"                     ) && status=$? || status=$?
  clear >&2

  (( ! status || status == CANCEL_STATUS ))

  if   [[ "${dialog_type}" == '--menu' ]]
  then echo "${result}"
  elif [[ "${dialog_type}" == '--yesno' ]]
  then echo $(( ! status ))
  elif [[ "${dialog_type}" == '--inputbox'    ]] ||
       [[ "${dialog_type}" == '--passwordbox' ]]
  then if   (( status != CANCEL_STATUS ))
       then [[ -n "${result}" ]] && echo "${result}" || echo ' '
       fi
  fi

  (( status == CANCEL_STATUS )) && status=0 || true ;

  return ${status}
}

ValidateId() # (login)
{
  local login="$*"

  [[ "${login}" =~ ^[[:space:]]*[^[:space:]]+[[:space:]]*$ ]] # TODO: improve this
}


## partitioning helpers ##

readonly DISK_RECORD_SEP='|'
readonly DISK_FIELD_SEP=','

declare -A PartsData # raw data
declare -a DlgParams # dialog options

GetDisksPartsData()
{
  local line
  local dev
  local format

  # example parted output:
  #   BYT;
  #   /dev/sdb:9664MB:scsi:512:512:msdos:ATA X HARDDISK:;
  #   1:1049kB:9663MB:9662MB:ext4::;                      # formatted
  #   1:9663MB:9664MB:1048kB:::;                          # un-formatted
  # example output:
  #   /dev/sda:42.9GB|/dev/sda1:42.9GB:ext4
  #   /dev/sdb:9664MB|/dev/sdb1:9662MB:ext4|/dev/sdb2:1048kB
  sudo parted --list --machine | \
  while read line
  do
    if   [[ "${line}" == 'BYT;' ]]
    then echo ' '
    else if   [[ "${line}" =~ ^(/dev/sd[a-z]):([^:]*): ]]
         then dev="${BASH_REMATCH[1]}"
              echo -n "${dev}${DISK_FIELD_SEP}${BASH_REMATCH[2]}"
         elif [[ "${line}" =~ ^([0-9]):[^:]*:[^:]*:([^:]*):([^:]*): ]]
         then format=$( [[ -n "${BASH_REMATCH[3]}" ]] && echo ${BASH_REMATCH[3]} || : )
              echo -n "${DISK_RECORD_SEP}${dev}${BASH_REMATCH[1]}"
              echo -n "${DISK_FIELD_SEP}${BASH_REMATCH[2]}"
              echo -n "${DISK_FIELD_SEP}${format}"
         fi
    fi
  done
}

GetDiskData() # (disk_data_n)
{
  if   [[ "$1" =~ ^[0-9]+$ ]]
  then local disk_data_n=$1
       local disks_parts_data=( $(GetDisksPartsData) )
       local disk_data=${disks_parts_data[${disk_data_n}]%%${DISK_RECORD_SEP}*}

       tr "${DISK_FIELD_SEP}" ' ' <<<${disk_data}
  fi
}

GetDiskPartsData() # (disk_data_n)
{
  local disk_data_n=$1
  local disks_parts_data=( $(GetDisksPartsData) )
  local disk_parts_data=${disks_parts_data[${disk_data_n}]#*${DISK_RECORD_SEP}}

  tr "${DISK_RECORD_SEP}" ' ' <<<${disk_parts_data}
}

GetDiskPartData() # (disk_data_n part_data_n)
{
  local disk_data_n=$1
  local part_data_n=$2
  local disk_parts_data=( $(GetDiskPartsData ${disk_data_n}) )
  local disk_parts_data=${disk_parts_data[${part_data_n}]}

  tr "${DISK_FIELD_SEP}" ' ' <<<${disk_parts_data}
}

GetDevice() # (disk_data_n)
{
  local disk_data_n=$1
  local disk_data=$(GetDiskData ${disk_data_n})
  local device=${disk_data/ *}

  echo ${device}
}

PopulatePartsData() # (disk_data_n) # modifies $PartsData
{
  local disk_data_n=$1
  local disk_parts_data=( $(GetDiskPartsData ${disk_data_n}) )
  local part_data_n
  local disk_part_data
  local dlg_part_n
  PartsData=()

  # populate params array for dialog - example output:
  #   1 "/dev/sdb1 9662MB ext4" 2 "/dev/sdb2 1048kB"
  for (( part_data_n=0 ; part_data_n < ${#disk_parts_data[@]} ; ++part_data_n ))
  do  disk_part_data="$(GetDiskPartData ${disk_data_n} ${part_data_n})"
      dlg_part_n=$(( ${part_data_n} + 1 ))
      if ! [[ "${disk_part_data}" =~ linux-swap ]]           && \
         ! mount | grep "^${disk_part_data/ *} " > /dev/null
      then PartsData[${dlg_part_n}]="${disk_part_data}"
      fi
  done
}

PopulateDisksOptions() # modifies $DlgParams
{
  local disks_part_data=( $(GetDisksPartsData) )
  local disk_data_n
  DlgParams=()

  # populate params array for dialog - example output:
  #   ( 1 "/dev/sda 42.9GB" 2 "/dev/sdb 9664MB" )
  for (( disk_data_n=0 ; disk_data_n < ${#disks_part_data[@]} ; ++disk_data_n ))
  do  DlgParams+=( $(( ${disk_data_n} + 1 )) "$(GetDiskData ${disk_data_n})" )
  done
}

PopulatePartOptions() # (device_n)
{
  local dlg_device_n=$1
  local part_data_n
  DlgParams=()

  PopulatePartsData $(( ${dlg_device_n} - 1 )) # populates PartsData
  for part_data_n in $(tr ' ' '\n' <<<${!PartsData[@]} | sort)
  do DlgParams+=( ${part_data_n} "${PartsData[${part_data_n}]}" )
  done
}

RemoveOption() # (part_n)
{
  local dlg_part_n=$1
  unset 'PartsData['"${dlg_part_n}"']'
}


## error logging helpers ##

LogError() # (source_file func_name line_n)
{
  local SOURCE_FILE="${BASH_SOURCE[1]}"
  local FUNC_NAME="$( [[ -n "$1" ]] && echo "$1" || echo "FUNC_NAME" )"
  local LINE_N=$2
  local N_CONTEXT_LINES=3
  local N_LINES=$((      1 + (2 * N_CONTEXT_LINES) ))
  local BEGIN_LINE_N=$(( LINE_N - N_CONTEXT_LINES  ))
  local END_LINE_N=$((   LINE_N + N_CONTEXT_LINES  ))
  local marker line

  (( BEGIN_LINE_N < 0 )) && BEGIN_LINE_N=0

  echo "ERROR: in ${SOURCE_FILE} ${FUNC_NAME}()::${LINE_N}" >&2
  sed 's|\\$||' "${SOURCE_FILE}" | pr -tn                      | \
  tail -n +${BEGIN_LINE_N} | head -n ${N_LINES} | tr '\n' '\n' | \
  while read line
  do    line_n=$(sed -E 's|([0-9]+).*|\1|' <<<${line})
        (( line_n == LINE_N )) && marker='==>' || marker='   '
        printf "%s %s\n" "${marker}" "${line}" >&2
  done
}


## debugging helpers ##

MOCK_INITKEYRING()       { SetStateVar 'KEYRING' 'ready' ; }
MOCK_SELECTDEFAULTS()    { SetStateVar 'INSTALL' ${DEF_INSTALL} ; SetStateVar 'BASE' ${DEF_PKG_SET} ; SetStateVar 'INIT' ${DEF_INIT} ; SetStateVar 'WMDE' ${DEF_WMDE} ; SetStateVar 'HOSTNAME' ${DEF_HOSTNAME} ; SetStateVar 'TIMEZONE' ${DEF_TIMEZONE} ; SetStateVar 'KEYMAP' $(GetStateVar 'XKBMAP' ${DEF_KEYMAP} ) ; SetStateVar 'LOCALES' $(GetStateVar 'LANG' ${DEF_LOCALE} ) ; SetStateVar 'LANGUAGE' $(GetStateVar 'LANG' ${DEF_LANGUAGE}) ; }
MOCK_SELECTDEFAULTS_NO() { : ; }
MOCK_SELECTLOGINS()      { SetStateVar 'ROOT_PASS' ROOT_PASS ; MOCK_SELECTLOGINS_USER ; }
MOCK_SELECTLOGINS_USER() { SetStateVar 'USER_LOGIN' USER_LOGIN ; SetStateVar 'USER_PASS' USER_PASS ; }
MOCK_SELECTBASE()        { SetStateVar 'INSTALL' ${DEF_INSTALL} ; SetStateVar 'BASE' ${DEF_PKG_SET} ; SetStateVar 'INIT' ${DEF_INIT} ; }
MOCK_SELECTBOOT()        { SetStateVar 'BOOT' 'grub' ; }
MOCK_SELECTWMDE()        { [[ -n "$(GetStateVar 'WMDE')" ]] || SetStateVar 'WMDE' 'cli' ; }
MOCK_SELECTENV()         { SetStateVar 'HOSTNAME' ${DEF_HOSTNAME} ; SetStateVar 'TIMEZONE' ${DEF_TIMEZONE} ; SetStateVar 'KEYMAP' $(GetStateVar 'XKBMAP' ${DEF_KEYMAP}) ; SetStateVar 'LOCALES' $(GetStateVar 'LANG' ${DEF_LOCALE}) ; SetStateVar 'LANGUAGE' $(GetStateVar 'LANG' ${DEF_LANGUAGE}) ; }


## translations for user-facing strings ##

source "${THIS_DIR}"/translations.sh.inc

SetLang() { Lang=$( (( ${TRANSLATIONS[$1]} )) && echo $1 || echo 'en' ) ; }

Lang='' ; SetLang $(GetStateVar 'TR_KEY' 'en') ;