]> ### unMotion 0.4.3 - Requires Unraid 7.3.2 or later. Successful registered installs/updates prune older cached unMotion TXZ downloads while retaining the verified current package for offline boots. - Application/release version stays 0.4.3; the Plugins tab shows fixed installer version 2026.10.04.01-0.4.3 for reliable upgrade ordering. - No migration, cloning, replication, recovery or protocol changes from 0.4.2. Protocol 7 peers remain compatible. - Finish active operations and shut down recovery-managed VMs before upgrading. Use unMotion controls to restart managed VMs after checking native-fence readiness. - Upgrading from 0.4.1 or earlier still requires coordinated protocol-7 maintenance: finish or remove old prepared/partial Warm Moves, pause replication and upgrade both peers together. - Existing settings, host identities and recovery authority records are retained. Keep verified backups. GPL-3.0-only. 1,'pid'=>$pid,'bootId'=>$boot,'startTicks'=>$fields[19], 'parentPid'=>(int)$fields[1],'processGroup'=>(int)$fields[2],'session'=>(int)$fields[3], 'executable'=>$executable,'executableDevice'=>(string)$exeStat['dev'], 'executableInode'=>(string)$exeStat['ino'],'argv'=>$arguments]; // Detect exit/exec/reuse while collecting the /proc fields. $again=(string)@file_get_contents($base.'/stat');$againEnd=strrpos($again,')'); $againFields=$againEnd===false?[]:preg_split('/\s+/',trim(substr($again,$againEnd+1))); if(($againFields[19]??null)!==$fields[19]||in_array($againFields[0]??'',['Z','X','x'],true) ||(string)@file_get_contents($base.'/cmdline')!==$command||@readlink($base.'/exe')!==$executable)return null; return $result; } function unmProcessCommandMatches(array $snapshot,string $script,array $arguments=[]): bool { $argv=$snapshot['argv']??[]; if(!is_array($argv)||!$argv||$script===''||$script[0]!=='/')return false; $offset=0; if(($argv[0]??'')!==$script){ // Only recognise an actual interpreter executable, never a script name // occurring later in arbitrary argv (e.g. echo/sleep/php -r bait). if(!preg_match('/^(bash|php(?:[0-9]+(?:\.[0-9]+)?)?)$/',basename((string)($snapshot['executable']??''))) ||($argv[1]??'')!==$script)return false; $offset=1; }elseif((string)($snapshot['executable']??'')!==$script)return false; return array_slice($argv,$offset+1,count($arguments))===$arguments; } function unmProcessIdentityMatches(array $identity,string $procRoot='/proc'): bool { if(($identity['schema']??null)!==1||!is_int($identity['pid']??null))return false; $current=unmProcessSnapshot($identity['pid'],$procRoot); if($current===null)return false; // Parent PID may legitimately change after the short-lived launcher exits. foreach(['pid','bootId','startTicks','processGroup','session','executable','executableDevice','executableInode','argv'] as $key){ if(!array_key_exists($key,$identity)||$identity[$key]!==$current[$key])return false; } return true; } function unmProcessReadIdentity(string $path,int $pid,string $script,array $arguments=[]): ?array { $identity=json_decode((string)@file_get_contents($path),true); if(!is_array($identity)||($identity['pid']??null)!==$pid ||!unmProcessCommandMatches($identity,$script,$arguments)||!unmProcessIdentityMatches($identity))return null; return $identity; } function unmProcessRecordedWorker(string $path,string $script,array $arguments=[]): ?array { $record=json_decode((string)@file_get_contents($path),true); if(!is_array($record))return null; return unmProcessReadIdentity($path,(int)($record['pid']??0),$script,$arguments); } function unmProcessCleanupCandidate(string $path,int $legacyPid,string $script,array $arguments=[]): ?array { $identity=unmProcessRecordedWorker($path,$script,$arguments); if($identity!==null)return $identity; $record=json_decode((string)@file_get_contents($path),true); foreach(array_unique([$legacyPid,(int)($record['pid']??0)]) as $pid){ $current=unmProcessSnapshot($pid); if($current!==null&&unmProcessCommandMatches($current,$script,$arguments)){ throw new RuntimeException('An active worker has no verifiable lifetime record. Let it finish before removing unMotion: '.$script); } } return null; } function unmProcessLegacyServiceAllowed(string $script,array $arguments): bool { return ($script==='/usr/local/sbin/unmotion-replication-scheduler'&&$arguments===[]) ||($script==='/usr/local/sbin/unmotion-replication-lifecycle'&&$arguments===['--daemon']); } function unmProcessAdoptLegacyService(string $path,int $pid,string $script,array $arguments): ?array { // One-time 0.4.1 upgrade path for the TWO known singleton daemons only. // Never replace an existing lifetime record or adopt jobs/seed workers. if(file_exists($path)||is_link($path)||!unmProcessLegacyServiceAllowed($script,$arguments))return null; $current=unmProcessSnapshot($pid); if($current===null||!unmProcessCommandMatches($current,$script,$arguments))return null; $offset=($current['argv'][0]??'')===$script?0:1; if(array_slice($current['argv'],$offset+1)!==$arguments)return null; // Persist exactly the first observed lifetime; if it changed, the caller's // mandatory second identity check refuses the stop. No fresh PID fallback. $handle=@fopen($path,'x');if($handle===false)return null; try{ if(!chmod($path,0600)||fwrite($handle,json_encode($current,JSON_UNESCAPED_SLASHES|JSON_THROW_ON_ERROR)."\n")===false)throw new RuntimeException('Unable to persist adopted service identity.'); }finally{fclose($handle);} return unmProcessReadIdentity($path,$pid,$script,$arguments); } function unmProcessRegister(string $path,int $pid,string $script,array $arguments=[]): array { $identity=unmProcessSnapshot($pid); if($identity===null||!unmProcessCommandMatches($identity,$script,$arguments))throw new RuntimeException('Unable to prove worker process identity.'); $temporary=$path.'.tmp.'.getmypid(); $json=json_encode($identity,JSON_UNESCAPED_SLASHES|JSON_PRETTY_PRINT|JSON_THROW_ON_ERROR)."\n"; if(file_put_contents($temporary,$json,LOCK_EX)===false||!chmod($temporary,0600)||!rename($temporary,$path)){ @unlink($temporary);throw new RuntimeException('Unable to persist worker process identity.'); } return $identity; } function unmProcessTree(array $identity): array { if(!unmProcessIdentityMatches($identity))return []; $candidates=[]; foreach(glob('/proc/[0-9]*/stat')?:[] as $path){ $candidate=unmProcessSnapshot((int)basename(dirname($path))); if($candidate!==null&&$candidate['session']===$identity['session']&&$candidate['processGroup']===$identity['processGroup'])$candidates[$candidate['pid']]=$candidate; } if(!unmProcessIdentityMatches($identity))return []; $owned=[$identity['pid']=>$identity]; do{ $changed=false; foreach($candidates as $pid=>$candidate){ if(!isset($owned[$pid])&&isset($owned[$candidate['parentPid']])){$owned[$pid]=$candidate;$changed=true;} } }while($changed); // Stop children before the shell so its traps can reap and restore state. return array_reverse(array_values($owned)); } function unmProcessSignal(array $identity,int $signal): bool { if(!in_array($signal,[15,9],true))throw new InvalidArgumentException('Unsupported process signal.'); if(!function_exists('posix_kill'))throw new RuntimeException('PHP POSIX support is required for verified process signalling.'); // Revalidate immediately before EVERY signal, including escalation. Never // use kill(-pgid) or pkill: those can hit new/unverified group members. if(!unmProcessIdentityMatches($identity))return false; return posix_kill($identity['pid'],$signal); } function unmProcessSignalTree(array $identity,int $signal): array { $members=unmProcessTree($identity); foreach($members as $member)unmProcessSignal($member,$signal); return $members; } function unmProcessStop(array $identity,int $graceMilliseconds=10000,bool $escalate=false): bool { $members=unmProcessSignalTree($identity,15); $deadline=microtime(true)+$graceMilliseconds/1000; do{ $alive=array_values(array_filter($members,'unmProcessIdentityMatches')); if(!$alive)return true; usleep(100000); }while(microtime(true)<$deadline); if($escalate){ // Keep the ORIGINAL identities, not freshly captured identities for the // old PID numbers. A replacement process must never inherit authority. foreach($alive as $member)unmProcessSignal($member,9); for($attempt=0;$attempt<20;$attempt++){ if(!array_filter($members,'unmProcessIdentityMatches'))return true; usleep(100000); } } return !array_filter($members,'unmProcessIdentityMatches'); } function unmProcessUpgradeInventory(): array { $workers=[];$daemons=[]; $scripts=['unmotion-worker','unmotion-clone-worker','unmotion-seed-worker','unmotion-replication-worker','unmotion-recovery-worker','unmotion-soak-cleanup','unmotion-replication-scheduler','unmotion-replication-lifecycle', // Incoming receivers have no local migration job. The forced-command // gate remains alive while its rsync/ZFS child streams; agents and the // destination-start helper also cover legacy/in-flight remote calls. 'unmotion-ssh-gate','unmotion-agent','unmotion-start-destination']; foreach(glob('/proc/[0-9]*/stat')?:[] as $path){ $identity=unmProcessSnapshot((int)basename(dirname($path)));if($identity===null)continue; foreach($scripts as $name){ $script='/usr/local/sbin/'.$name; if(!unmProcessCommandMatches($identity,$script))continue; $offset=($identity['argv'][0]??'')===$script?0:1;$arguments=array_slice($identity['argv'],$offset+1); if(unmProcessLegacyServiceAllowed($script,$arguments))$daemons[]=$identity; else $workers[]=$identity; break; } } return ['workers'=>$workers,'daemons'=>$daemons]; } function unmProcessPreinstall(): void { if(!function_exists('posix_kill'))throw new RuntimeException('PHP POSIX support is required for safe unMotion service upgrades.'); unmProcessAssertManagedGuestsStopped(); $inventory=unmProcessUpgradeInventory(); if($inventory['workers'])throw new RuntimeException('An unMotion migration, prepared-copy, replication, recovery, incoming transfer, remote request or delayed-cleanup worker is active. Let it finish or cancel it, then retry the upgrade. Do not start new operations while upgrading. No package or pairing settings have been changed.'); // Use fresh exact /proc identities, never old PID files or the old rc stop // implementation. Preserve fencing hooks and all durable recovery state. foreach($inventory['daemons'] as $identity){ if(!unmProcessStop($identity,10000))throw new RuntimeException('An exact unMotion runtime daemon did not stop. Upgrade refused. Inspect the runtime and retry; stopped daemons will restart on a successful installation or host boot.'); } // A scheduler could launch a job between the first inventory and its stop. // Never terminate that worker or replace its scripts/transport underneath it. $remaining=unmProcessUpgradeInventory(); if($remaining['workers']||$remaining['daemons'])throw new RuntimeException('An unMotion worker or daemon appeared while stopping scheduling. Upgrade refused; let active work finish and retry. Stopped runtime daemons restart on successful installation or host boot.'); echo "unMotion upgrade guard: no active workers; verified scheduling/lifecycle daemons stopped. Do not start new operations until installation completes.\n"; } // No adoption of an already-running managed QEMU into a new native start fence. // Run before stopping lifecycle services or replacing any installed file. function unmProcessAssertManagedGuestsStopped(string $base='/boot/config/plugins/unmotion'): void { foreach(glob($base.'/native-managed/*.json')?:[] as $markerPath){$m=json_decode((string)@file_get_contents($markerPath),true);$id=(string)($m['replicationId']??'');$uuid=(string)($m['vmUuid']??'');$side=(string)($m['side']??'');if(!is_array($m)||($m['schemaVersion']??null)!==1||!preg_match('/^[a-f0-9-]{36}$/',$uuid)||!preg_match('/^repl-[a-f0-9]{24}$/',$id)||!in_array($side,['source','destination'],true))throw new RuntimeException('Corrupt native managed identity prevents safe upgrade.');$paths=$side==='source'?[$base.'/replications/'.$id.'/recovery.json']:(glob($base.'/replicas/*/'.$id.'/recovery.json')?:[]);$matched=0;foreach($paths as $path){$r=json_decode((string)@file_get_contents($path),true);if(is_array($r)&&($r['replicationId']??'')===$id&&strtolower((string)($r['vmUuid']??''))===$uuid&&($r['side']??'')===$side)++$matched;}if($matched!==1)throw new RuntimeException('Exact native managed authority is missing or ambiguous; reconcile it before upgrading.');} $managed=[];foreach(array_merge(glob($base.'/replications/*/recovery.json')?:[],glob($base.'/replicas/*/*/recovery.json')?:[]) as $path){$r=json_decode((string)@file_get_contents($path),true);if(!is_array($r))throw new RuntimeException('Corrupt recovery authority must be reconciled before upgrade.');if(in_array((string)($r['state']??''),['REPLICATION_ONLY','DISARMED','REMOVED'],true)&&empty($r['armed'])&&empty($r['managedAutostart'])&&empty($r['activationId'])&&empty($r['claim'])&&empty($r['activation']))continue;$uuid=strtolower((string)($r['vmUuid']??''));if(!preg_match('/^[a-f0-9-]{36}$/',$uuid))throw new RuntimeException('Invalid managed VM identity prevents safe upgrade.');$managed[$uuid]=true;} if(!$managed)return; $run=static function(array $args):string{$p=proc_open(array_merge(['timeout','-k','2','15','virsh'],$args),[0=>['file','/dev/null','r'],1=>['pipe','w'],2=>['file','/dev/null','w']],$pipes);if(!is_resource($p))throw new RuntimeException('Cannot check managed VMs before upgrade.');$out=(string)stream_get_contents($pipes[1]);fclose($pipes[1]);if(proc_close($p)!==0)throw new RuntimeException('Libvirt state is unavailable; upgrade cannot prove managed VMs are stopped.');return trim($out);}; $inventory=strtolower($run(['list','--all','--uuid']));$defined=$inventory===''?[]:preg_split('/\s+/',$inventory);foreach($defined as $entry)if(!preg_match('/^[a-f0-9]{8}(?:-[a-f0-9]{4}){3}-[a-f0-9]{12}$/',$entry))throw new RuntimeException('Libvirt returned an invalid VM inventory; upgrade refused.'); foreach(array_keys($managed) as $uuid)if(in_array($uuid,$defined,true)&&strtolower($run(['domstate',$uuid]))!=='shut off')throw new RuntimeException('Power off recovery-managed VM '.$uuid.' before upgrading unMotion. Existing VM authority and services were left unchanged.'); } try { unmProcessPreinstall(); } catch (Throwable $error) { fwrite(STDERR, $error->getMessage()."\n"); exit(1); } ]]> https://github.com/rtho782/unmotion/releases/download/0.4.3/unmotion-0.4.3_stable-noarch-1.txz b9f36d22ddbeda2d45b7ed7e851df8039c22eef0973011d6b9c03a1ec38e56e0 /etc/rc.d/rc.unmotion restart || exit 1 /usr/local/sbin/unmotion-native-fence check-remove || exit 1 /etc/rc.d/rc.unmotion stop || exit 1 /usr/local/sbin/unmotion-cleanup || exit 1 /sbin/removepkg unmotion 2>/dev/null || true rm -f /usr/local/emhttp/plugins/dynamix.plugin.manager/post-hooks/unmotion-package-cache rm -f /etc/libvirt/hooks/qemu.d/50-unmotion-recovery rm -rf /usr/local/emhttp/plugins/unmotion /usr/local/sbin/unmotion-* /etc/rc.d/rc.unmotion rm -rf "&plgdir;"