diff --git a/pkg/storaged/content-views.jsx b/pkg/storaged/content-views.jsx index 23531db5c8e2..836d123f9355 100644 --- a/pkg/storaged/content-views.jsx +++ b/pkg/storaged/content-views.jsx @@ -24,6 +24,7 @@ import { init_active_usage_processes } from "./dialog.jsx"; import * as utils from "./utils.js"; +import { set_crypto_auto_option } from "./utils.js"; import React from "react"; import { Card, CardBody, CardHeader, CardTitle } from '@patternfly/react-core/dist/esm/components/Card/index.js'; @@ -38,13 +39,11 @@ import { ListingTable } from "cockpit-components-table.jsx"; import { ListingPanel } from 'cockpit-components-listing-panel.jsx'; import { StorageButton, StorageBarMenu, StorageMenuItem, StorageUsageBar } from "./storage-controls.jsx"; import * as PK from "packagekit.js"; -import { - format_dialog, parse_options, extract_option, unparse_options -} from "./format-dialog.jsx"; +import { format_dialog } from "./format-dialog.jsx"; import { job_progress_wrapper } from "./jobs-panel.jsx"; import { FilesystemTab, is_mounted, mounting_dialog, get_fstab_config } from "./fsys-tab.jsx"; -import { CryptoTab, edit_config } from "./crypto-tab.jsx"; +import { CryptoTab } from "./crypto-tab.jsx"; import { get_existing_passphrase, unlock_with_type } from "./crypto-keyslots.jsx"; import { BlockVolTab, PoolVolTab, VDOPoolTab } from "./lvol-tabs.jsx"; import { PartitionTab } from "./part-tab.jsx"; @@ -75,38 +74,6 @@ function next_default_logical_volume_name(client, vgroup, prefix) { return name; } -export function set_crypto_options(block, readonly, auto, nofail, netdev) { - return edit_config(block, (config, commit) => { - const opts = config.options ? parse_options(utils.decode_filename(config.options.v)) : []; - if (readonly !== null) { - extract_option(opts, "readonly"); - if (readonly) - opts.push("readonly"); - } - if (auto !== null) { - extract_option(opts, "noauto"); - if (!auto) - opts.push("noauto"); - } - if (nofail !== null) { - extract_option(opts, "nofail"); - if (nofail) - opts.push("nofail"); - } - if (netdev !== null) { - extract_option(opts, "_netdev"); - if (netdev) - opts.push("_netdev"); - } - config.options = { t: 'ay', v: utils.encode_filename(unparse_options(opts)) }; - return commit(); - }); -} - -export function set_crypto_auto_option(block, flag) { - return set_crypto_options(block, null, flag, null, null); -} - function create_tabs(client, target, options) { function endsWith(str, suffix) { return str.indexOf(suffix, str.length - suffix.length) !== -1; diff --git a/pkg/storaged/crypto-keyslots.jsx b/pkg/storaged/crypto-keyslots.jsx index f8e86f5f7d62..358ea47f6c84 100644 --- a/pkg/storaged/crypto-keyslots.jsx +++ b/pkg/storaged/crypto-keyslots.jsx @@ -39,11 +39,9 @@ import { dialog_open, SelectOneRadio, TextInput, PassInput, Skip } from "./dialog.jsx"; -import { decode_filename, encode_filename, get_block_mntopts, block_name, for_each_async, get_children } from "./utils.js"; +import { decode_filename, encode_filename, get_block_mntopts, block_name, for_each_async, get_children, parse_options, unparse_options, edit_crypto_config } from "./utils.js"; import { fmt_to_fragments } from "utils.jsx"; import { StorageButton } from "./storage-controls.jsx"; -import { parse_options, unparse_options } from "./format-dialog.jsx"; -import { edit_config } from "./crypto-tab.jsx"; import clevis_luks_passphrase_sh from "./clevis-luks-passphrase.sh"; @@ -384,7 +382,7 @@ function ensure_crypto_option(steps, progress, client, block, option) { const new_crypto_options = crypto_options.concat([option]); progress(cockpit.format(_("Adding \"$0\" to encryption options"), option), null); - return edit_config(block, (config, commit) => { + return edit_crypto_config(block, (config, commit) => { config.options = { t: 'ay', v: encode_filename(unparse_options(new_crypto_options)) }; return commit(); }); diff --git a/pkg/storaged/crypto-tab.jsx b/pkg/storaged/crypto-tab.jsx index e17eab66cce4..6b1aa9c0e004 100644 --- a/pkg/storaged/crypto-tab.jsx +++ b/pkg/storaged/crypto-tab.jsx @@ -21,8 +21,7 @@ import { DescriptionList, DescriptionListDescription, DescriptionListGroup, Desc import { Flex, FlexItem } from "@patternfly/react-core/dist/esm/layouts/Flex/index.js"; import cockpit from "cockpit"; import { dialog_open, TextInput, PassInput } from "./dialog.jsx"; -import { encode_filename, decode_filename, block_name } from "./utils.js"; -import { parse_options, unparse_options, extract_option } from "./format-dialog.jsx"; +import { encode_filename, decode_filename, block_name, parse_options, unparse_options, extract_option, edit_crypto_config } from "./utils.js"; import { is_mounted } from "./fsys-tab.jsx"; import React from "react"; @@ -50,33 +49,6 @@ function parse_tag_mtime(tag) { return null; } -export function edit_config(block, modify) { - let old_config, new_config; - - function commit() { - new_config[1]["track-parents"] = { t: 'b', v: true }; - if (old_config) - return block.UpdateConfigurationItem(old_config, new_config, { }); - else - return block.AddConfigurationItem(new_config, { }); - } - - return block.GetSecretConfiguration({}).then( - function (items) { - old_config = items.find(c => c[0] == "crypttab"); - new_config = ["crypttab", old_config ? Object.assign({ }, old_config[1]) : { }]; - - // UDisks insists on always having a "passphrase-contents" field when - // adding a crypttab entry, but doesn't include one itself when returning - // an entry without a stored passphrase. - // - if (!new_config[1]['passphrase-contents']) - new_config[1]['passphrase-contents'] = { t: 'ay', v: encode_filename("") }; - - return modify(new_config[1], commit); - }); -} - export class CryptoTab extends React.Component { constructor() { super(); @@ -141,7 +113,7 @@ export class CryptoTab extends React.Component { this.monitor_slots(block); function edit_stored_passphrase() { - edit_config(block, function (config, commit) { + edit_crypto_config(block, function (config, commit) { dialog_open({ Title: _("Stored passphrase"), Fields: [ @@ -188,7 +160,7 @@ export class CryptoTab extends React.Component { const content_block = client.blocks_cleartext[block.path]; const is_fsys = fsys_config || (content_block && content_block.IdUsage == "filesystem"); - edit_config(block, function (config, commit) { + edit_crypto_config(block, function (config, commit) { dialog_open({ Title: _("Encryption options"), Fields: [ diff --git a/pkg/storaged/dialog.jsx b/pkg/storaged/dialog.jsx index 3bc707515d1c..2576a8dee4c8 100644 --- a/pkg/storaged/dialog.jsx +++ b/pkg/storaged/dialog.jsx @@ -1140,10 +1140,14 @@ const UsersPopover = ({ users }) => { ); }; -export const TeardownMessage = (usage) => { +export const TeardownMessage = (usage, expect_single_unmount) => { if (usage.length == 0) return null; + if (expect_single_unmount && usage.length == 1 && + usage[0].usage == "mounted" && usage[0].location == expect_single_unmount) + return ; + const rows = []; usage.forEach((use, index) => { if (use.block) { @@ -1178,7 +1182,7 @@ export const TeardownMessage = (usage) => { ); }; -export function init_active_usage_processes(client, usage) { +export function init_active_usage_processes(client, usage, expect_single_unmount) { return { title: _("Checking related processes"), func: dlg => { @@ -1191,7 +1195,7 @@ export function init_active_usage_processes(client, usage) { } else return Promise.resolve(); }).then(() => { - dlg.set_attribute("Teardown", TeardownMessage(usage)); + dlg.set_attribute("Teardown", TeardownMessage(usage, expect_single_unmount)); const usage_with_users = usage.filter(u => u.users); const n_processes = usage_with_users.reduce((sum, u) => sum + u.users.filter(u => u.pid).length, 0); const n_services = usage_with_users.reduce((sum, u) => sum + u.users.filter(u => u.unit).length, 0); @@ -1207,6 +1211,9 @@ export function init_active_usage_processes(client, usage) { } export const StopProcessesMessage = ({ mount_point, users }) => { + if (!users || users.length == 0) + return null; + const process_rows = users.filter(u => u.pid).map(u => { return { columns: [ diff --git a/pkg/storaged/format-dialog.jsx b/pkg/storaged/format-dialog.jsx index 79460661ec41..5b8802a0e086 100644 --- a/pkg/storaged/format-dialog.jsx +++ b/pkg/storaged/format-dialog.jsx @@ -19,6 +19,7 @@ import cockpit from "cockpit"; import * as utils from "./utils.js"; +import { edit_crypto_config, parse_options, unparse_options, extract_option } from "./utils.js"; import React from "react"; import { FormHelperText } from "@patternfly/react-core/dist/esm/components/Form/index.js"; @@ -33,35 +34,11 @@ import { } from "./dialog.jsx"; import { get_fstab_config, is_valid_mount_point } from "./fsys-tab.jsx"; -import { edit_config } from "./crypto-tab.jsx"; import { init_existing_passphrase, unlock_with_type } from "./crypto-keyslots.jsx"; import { job_progress_wrapper } from "./jobs-panel.jsx"; const _ = cockpit.gettext; -export function parse_options(options) { - if (options) - return (options.split(",") - .map(function (s) { return s.trim() }) - .filter(function (s) { return s != "" })); - else - return []; -} - -export function unparse_options(split) { - return split.join(","); -} - -export function extract_option(split, opt) { - const index = split.indexOf(opt); - if (index >= 0) { - split.splice(index, 1); - return true; - } else { - return false; - } -} - export function initial_tab_options(client, block, for_fstab) { const options = { }; @@ -530,11 +507,11 @@ function format_dialog_internal(client, path, start, size, enable_dos_extended, return block_ptable.CreatePartitionAndFormat(start, vals.size, "", "", { }, vals.type, options); } else if (keep_keys) { - return (edit_config(block, - (config, commit) => { - config.options = new_crypto_options; - return commit(); - }) + return (edit_crypto_config(block, + (config, commit) => { + config.options = new_crypto_options; + return commit(); + }) .then(() => maybe_unlock()) .then(content_block => { return content_block.Format(vals.type, options); diff --git a/pkg/storaged/fsys-tab.jsx b/pkg/storaged/fsys-tab.jsx index bc247d98fb4d..d0c96361514c 100644 --- a/pkg/storaged/fsys-tab.jsx +++ b/pkg/storaged/fsys-tab.jsx @@ -24,17 +24,14 @@ import { Flex, FlexItem } from "@patternfly/react-core/dist/esm/layouts/Flex/ind import cockpit from "cockpit"; import * as utils from "./utils.js"; +import { parse_options, unparse_options, extract_option, set_crypto_options, set_crypto_auto_option } from "./utils.js"; import { dialog_open, TextInput, PassInput, CheckBoxes, SelectOne, - StopProcessesMessage, stop_processes_danger_message + TeardownMessage, init_active_usage_processes } from "./dialog.jsx"; import { StorageButton, StorageLink } from "./storage-controls.jsx"; -import { - initial_tab_options, parse_options, unparse_options, extract_option, - mount_explanation -} from "./format-dialog.jsx"; -import { set_crypto_options, set_crypto_auto_option } from "./content-views.jsx"; +import { initial_tab_options, mount_explanation } from "./format-dialog.jsx"; import { init_existing_passphrase, unlock_with_type } from "./crypto-keyslots.jsx"; import client from "./client.js"; @@ -78,7 +75,7 @@ export function get_fstab_config(block, also_child_config) { return []; } -export function find_blocks_for_mount_point(client, mount_point, self) { +function find_blocks_for_mount_point(client, mount_point, self) { const blocks = []; function is_self(b) { @@ -99,7 +96,7 @@ function nice_block_name(block) { return utils.block_name(client.blocks[block.CryptoBackingDevice] || block); } -export function is_valid_mount_point(client, block, val) { +export async function is_valid_mount_point(client, block, val, ignore_overmounting) { if (val === "") return _("Mount point cannot be empty"); @@ -107,6 +104,16 @@ export function is_valid_mount_point(client, block, val) { if (other_blocks.length > 0) return cockpit.format(_("Mount point is already used for $0"), other_blocks.map(nice_block_name).join(", ")); + + if (!ignore_overmounting) { + const children = utils.find_children_for_mount_point(client, val, block); + if (Object.keys(children).length > 0) + return <> + {_("Filesystems are already mounted below this mountpoint.")} + {Object.keys(children).map(m =>
{cockpit.format("• $0 on $1", nice_block_name(children[m]), m)}
)} + {_("Please unmount them first.")} + ; + } } export function get_cryptobacking_noauto(client, block) { @@ -182,7 +189,6 @@ export function mounting_dialog(client, block, mode, forced_options) { const extra_options = unparse_options(split_options); const is_filesystem_mounted = is_mounted(client, block); - let mount_point_users = null; function maybe_update_config(new_dir, new_opts, passphrase, passphrase_type) { let new_config = null; @@ -220,13 +226,6 @@ export function mounting_dialog(client, block, mode, forced_options) { } } - function maybe_unmount() { - if (block_fsys && block_fsys.MountPoints.indexOf(utils.encode_filename(old_dir)) >= 0) - return client.unmount_at(old_dir, mount_point_users); - else - return Promise.resolve(); - } - function get_block_fsys() { if (block_fsys) return Promise.resolve(block_fsys); @@ -296,7 +295,7 @@ export function mounting_dialog(client, block, mode, forced_options) { // backs. return (utils.reload_systemd() - .then(maybe_unmount) + .then(() => utils.teardown_active_usage(client, usage)) .then(maybe_unlock) .then(() => { if (!old_config && new_config) @@ -330,7 +329,8 @@ export function mounting_dialog(client, block, mode, forced_options) { TextInput("mount_point", _("Mount point"), { value: old_dir, - validate: val => is_valid_mount_point(client, block, val) + validate: val => is_valid_mount_point(client, block, val, + mode == "update" && !is_filesystem_mounted) }), CheckBoxes("mount_options", _("Mount options"), { @@ -378,17 +378,6 @@ export function mounting_dialog(client, block, mode, forced_options) { ]); } - let teardown = null; - if (!is_filesystem_mounted && block_fsys && block_fsys.MountPoints.length > 0) - teardown = ( - <> - {teardown} -
-

{cockpit.format(_("The filesystem is already mounted at $0. Proceeding will unmount it."), - utils.decode_filename(block_fsys.MountPoints[0]))}

-
- ); - const mode_title = { mount: _("Mount filesystem"), unmount: _("Unmount filesystem $0"), @@ -431,10 +420,13 @@ export function mounting_dialog(client, block, mode, forced_options) { return Promise.resolve(); } + const usage = utils.get_active_usage(client, block.path); + console.log("USAGE", usage); + const dlg = dialog_open({ Title: cockpit.format(mode_title[mode], old_dir), Fields: fields, - Teardown: teardown, + Teardown: TeardownMessage(usage, old_dir), update: function (dlg, vals, trigger) { if (trigger == "at_boot") dlg.set_options("at_boot", { explanation: mount_explanation[vals.at_boot] }); @@ -470,19 +462,7 @@ export function mounting_dialog(client, block, mode, forced_options) { } }, Inits: [ - { - title: _("Checking related processes"), - func: dlg => { - return client.find_mount_users(old_dir, is_filesystem_mounted) - .then(users => { - mount_point_users = users; - if (users.length > 0) { - dlg.set_attribute("Teardown", ); - dlg.add_danger(stop_processes_danger_message(users)); - } - }); - } - }, + init_active_usage_processes(client, usage, old_dir), (block.IdUsage == "crypto" && mode == "mount") ? init_existing_passphrase(block, true, type => { passphrase_type = type }) : null diff --git a/pkg/storaged/nfs-details.jsx b/pkg/storaged/nfs-details.jsx index ecf247bb670c..15d082a6e801 100644 --- a/pkg/storaged/nfs-details.jsx +++ b/pkg/storaged/nfs-details.jsx @@ -26,10 +26,10 @@ import { dialog_open, TextInput, ComboBox, CheckBoxes, StopProcessesMessage, stop_processes_danger_message } from "./dialog.jsx"; -import * as format from "./format-dialog.jsx"; import { StdDetailsLayout } from "./details.jsx"; import { StorageButton, StorageUsageBar } from "./storage-controls.jsx"; +import { parse_options, unparse_options, extract_option } from "./utils.js"; const _ = cockpit.gettext; @@ -80,10 +80,10 @@ function get_exported_directories(server) { export function nfs_fstab_dialog(client, entry) { const mount_options = entry ? entry.fields[3] : "defaults"; - const split_options = format.parse_options(mount_options == "defaults" ? "" : mount_options); - const opt_auto = !format.extract_option(split_options, "noauto"); - const opt_ro = format.extract_option(split_options, "ro"); - const extra_options = format.unparse_options(split_options); + const split_options = parse_options(mount_options == "defaults" ? "" : mount_options); + const opt_auto = !extract_option(split_options, "noauto"); + const opt_ro = extract_option(split_options, "ro"); + const extra_options = unparse_options(split_options); function mounting_options(vals) { let opts = []; @@ -92,8 +92,8 @@ export function nfs_fstab_dialog(client, entry) { if (vals.mount_options.ro) opts.push("ro"); if (vals.mount_options.extra !== false) - opts = opts.concat(format.parse_options(vals.mount_options.extra)); - return format.unparse_options(opts); + opts = opts.concat(parse_options(vals.mount_options.extra)); + return unparse_options(opts); } function show(busy) { diff --git a/pkg/storaged/utils.js b/pkg/storaged/utils.js index b94b0a38730d..20971698b330 100644 --- a/pkg/storaged/utils.js +++ b/pkg/storaged/utils.js @@ -47,6 +47,88 @@ export function compare_versions(a, b) { return a_ints.length - b_ints.length; } +export function parse_options(options) { + if (options) + return (options.split(",") + .map(function (s) { return s.trim() }) + .filter(function (s) { return s != "" })); + else + return []; +} + +export function unparse_options(split) { + return split.join(","); +} + +export function extract_option(split, opt) { + const index = split.indexOf(opt); + if (index >= 0) { + split.splice(index, 1); + return true; + } else { + return false; + } +} + +export function edit_crypto_config(block, modify) { + let old_config, new_config; + + function commit() { + new_config[1]["track-parents"] = { t: 'b', v: true }; + if (old_config) + return block.UpdateConfigurationItem(old_config, new_config, { }); + else + return block.AddConfigurationItem(new_config, { }); + } + + return block.GetSecretConfiguration({}).then( + function (items) { + old_config = items.find(c => c[0] == "crypttab"); + new_config = ["crypttab", old_config ? Object.assign({ }, old_config[1]) : { }]; + + // UDisks insists on always having a "passphrase-contents" field when + // adding a crypttab entry, but doesn't include one itself when returning + // an entry without a stored passphrase. + // + if (!new_config[1]['passphrase-contents']) + new_config[1]['passphrase-contents'] = { t: 'ay', v: encode_filename("") }; + + return modify(new_config[1], commit); + }); +} + +export function set_crypto_options(block, readonly, auto, nofail, netdev) { + return edit_crypto_config(block, (config, commit) => { + const opts = config.options ? parse_options(decode_filename(config.options.v)) : []; + if (readonly !== null) { + extract_option(opts, "readonly"); + if (readonly) + opts.push("readonly"); + } + if (auto !== null) { + extract_option(opts, "noauto"); + if (!auto) + opts.push("noauto"); + } + if (nofail !== null) { + extract_option(opts, "nofail"); + if (nofail) + opts.push("nofail"); + } + if (netdev !== null) { + extract_option(opts, "_netdev"); + if (netdev) + opts.push("_netdev"); + } + config.options = { t: 'ay', v: encode_filename(unparse_options(opts)) }; + return commit(); + }); +} + +export function set_crypto_auto_option(block, flag) { + return set_crypto_options(block, null, flag, null, null); +} + export let hostnamed = cockpit.dbus("org.freedesktop.hostname1").proxy(); // for unit tests @@ -659,8 +741,34 @@ export function get_children(client, path) { return children; } +export function find_children_for_mount_point(client, mount_point, self) { + const children = {}; + + function is_self(b) { + return self && (b == self || client.blocks[b.CryptoBackingDevice] == self); + } + + for (const p in client.blocks) { + const b = client.blocks[p]; + const fs = client.blocks_fsys[p]; + + if (is_self(b)) + continue; + + if (fs) { + for (const mp of fs.MountPoints) { + const mpd = decode_filename(mp); + if (mpd.length > mount_point.length && mpd.indexOf(mount_point) == 0 && mpd[mount_point.length] == "/") + children[mpd] = b; + } + } + } + + return children; +} + export function get_active_usage(client, path, top_action, child_action) { - function get_usage(path, level) { + function get_usage(usage, path, level) { const block = client.blocks[path]; const fsys = client.blocks_fsys[path]; const mdraid = block && client.mdraids[block.MDRaidMember]; @@ -670,7 +778,7 @@ export function get_active_usage(client, path, top_action, child_action) { const stratis_blockdev = block && client.blocks_stratis_blockdev[path]; const stratis_pool = stratis_blockdev && client.stratis_pools[stratis_blockdev.Pool]; - const usage = flatten(get_children_for_teardown(client, path).map(p => get_usage(p, level + 1))); + get_children_for_teardown(client, path).map(p => get_usage(usage, p, level + 1)); function get_actions(teardown_action) { const actions = []; @@ -682,16 +790,34 @@ export function get_active_usage(client, path, top_action, child_action) { return actions; } + function enter_unmount(block, location, is_top) { + for (const u of usage) { + if (u.usage == 'mounted' && u.location == location) { + if (is_top) { + u.actions = get_actions(_("unmount")); + u.set_noauto = false; + } + return; + } + } + usage.push({ + level, + block, + usage: 'mounted', + location, + set_noauto: !is_top, + actions: is_top ? get_actions(_("unmount")) : [_("unmount")], + blocking: false + }); + } + if (fsys && fsys.MountPoints.length > 0) { fsys.MountPoints.forEach(mp => { - usage.push({ - level, - usage: 'mounted', - block, - location: decode_filename(mp), - actions: get_actions(_("unmount")), - blocking: false, - }); + const mpd = decode_filename(mp); + const children = find_children_for_mount_point(client, mpd, null); + for (const c in children) + enter_unmount(children[c], c, false); + enter_unmount(block, mpd, true); }); } else if (mdraid) { const active_state = mdraid.ActiveDevices.find(as => as[0] == block.path); @@ -746,7 +872,8 @@ export function get_active_usage(client, path, top_action, child_action) { return usage; } - let usage = get_usage(path, 0); + let usage = []; + get_usage(usage, path, 0); if (usage.length == 1 && usage[0].level == 0 && usage[0].usage == "none") usage = []; @@ -757,6 +884,37 @@ export function get_active_usage(client, path, top_action, child_action) { return usage; } +async function set_fsys_noauto(client, block, mount_point) { + for (const conf of block.Configuration) { + if (conf[0] == "fstab" && + decode_filename(conf[1].dir.v) == mount_point) { + const options = parse_options(get_block_mntopts(conf[1])); + if (options.indexOf("noauto") >= 0) + continue; + options.push("noauto"); + const new_conf = [ + "fstab", + Object.assign({ }, conf[1], + { + opts: { + t: 'ay', + v: encode_filename(unparse_options(options)) + } + }) + ]; + await block.UpdateConfigurationItem(conf, new_conf, { }); + } + } + + const crypto_backing = client.blocks[block.CryptoBackingDevice]; + if (crypto_backing) { + const crypto_backing_crypto = client.blocks_crypto[crypto_backing.path]; + await set_crypto_auto_option(crypto_backing, false); + if (crypto_backing_crypto) + await crypto_backing_crypto.Lock({}); + } +} + export function teardown_active_usage(client, usage) { // The code below is complicated by the fact that the last // physical volume of a volume group can not be removed @@ -767,10 +925,12 @@ export function teardown_active_usage(client, usage) { // physical volumes here, and it is easiest to catch this // condition upfront by reshuffling the data structures. - function unmount(mounteds) { - return Promise.all(mounteds.map(m => { - return client.unmount_at(m.location, m.users); - })); + async function unmount(mounteds) { + for (const m of mounteds) { + await client.unmount_at(m.location, m.users); + if (m.set_noauto) + await set_fsys_noauto(client, m.block, m.location); + } } function mdraid_remove(members) { diff --git a/test/reference b/test/reference index b0a214b926e8..444adbab6864 160000 --- a/test/reference +++ b/test/reference @@ -1 +1 @@ -Subproject commit b0a214b926e86a52b8cf3ad9a5681511fc03fca6 +Subproject commit 444adbab6864a348695889d5bb294566691ffab7