first commit
This commit is contained in:
commit
c1eb7b0e94
8 changed files with 2304 additions and 0 deletions
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
vlcro
|
||||||
339
LICENSE
Normal file
339
LICENSE
Normal file
|
|
@ -0,0 +1,339 @@
|
||||||
|
GNU GENERAL PUBLIC LICENSE
|
||||||
|
Version 2, June 1991
|
||||||
|
|
||||||
|
Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
|
||||||
|
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||||
|
Everyone is permitted to copy and distribute verbatim copies
|
||||||
|
of this license document, but changing it is not allowed.
|
||||||
|
|
||||||
|
Preamble
|
||||||
|
|
||||||
|
The licenses for most software are designed to take away your
|
||||||
|
freedom to share and change it. By contrast, the GNU General Public
|
||||||
|
License is intended to guarantee your freedom to share and change free
|
||||||
|
software--to make sure the software is free for all its users. This
|
||||||
|
General Public License applies to most of the Free Software
|
||||||
|
Foundation's software and to any other program whose authors commit to
|
||||||
|
using it. (Some other Free Software Foundation software is covered by
|
||||||
|
the GNU Lesser General Public License instead.) You can apply it to
|
||||||
|
your programs, too.
|
||||||
|
|
||||||
|
When we speak of free software, we are referring to freedom, not
|
||||||
|
price. Our General Public Licenses are designed to make sure that you
|
||||||
|
have the freedom to distribute copies of free software (and charge for
|
||||||
|
this service if you wish), that you receive source code or can get it
|
||||||
|
if you want it, that you can change the software or use pieces of it
|
||||||
|
in new free programs; and that you know you can do these things.
|
||||||
|
|
||||||
|
To protect your rights, we need to make restrictions that forbid
|
||||||
|
anyone to deny you these rights or to ask you to surrender the rights.
|
||||||
|
These restrictions translate to certain responsibilities for you if you
|
||||||
|
distribute copies of the software, or if you modify it.
|
||||||
|
|
||||||
|
For example, if you distribute copies of such a program, whether
|
||||||
|
gratis or for a fee, you must give the recipients all the rights that
|
||||||
|
you have. You must make sure that they, too, receive or can get the
|
||||||
|
source code. And you must show them these terms so they know their
|
||||||
|
rights.
|
||||||
|
|
||||||
|
We protect your rights with two steps: (1) copyright the software, and
|
||||||
|
(2) offer you this license which gives you legal permission to copy,
|
||||||
|
distribute and/or modify the software.
|
||||||
|
|
||||||
|
Also, for each author's protection and ours, we want to make certain
|
||||||
|
that everyone understands that there is no warranty for this free
|
||||||
|
software. If the software is modified by someone else and passed on, we
|
||||||
|
want its recipients to know that what they have is not the original, so
|
||||||
|
that any problems introduced by others will not reflect on the original
|
||||||
|
authors' reputations.
|
||||||
|
|
||||||
|
Finally, any free program is threatened constantly by software
|
||||||
|
patents. We wish to avoid the danger that redistributors of a free
|
||||||
|
program will individually obtain patent licenses, in effect making the
|
||||||
|
program proprietary. To prevent this, we have made it clear that any
|
||||||
|
patent must be licensed for everyone's free use or not licensed at all.
|
||||||
|
|
||||||
|
The precise terms and conditions for copying, distribution and
|
||||||
|
modification follow.
|
||||||
|
|
||||||
|
GNU GENERAL PUBLIC LICENSE
|
||||||
|
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
|
||||||
|
|
||||||
|
0. This License applies to any program or other work which contains
|
||||||
|
a notice placed by the copyright holder saying it may be distributed
|
||||||
|
under the terms of this General Public License. The "Program", below,
|
||||||
|
refers to any such program or work, and a "work based on the Program"
|
||||||
|
means either the Program or any derivative work under copyright law:
|
||||||
|
that is to say, a work containing the Program or a portion of it,
|
||||||
|
either verbatim or with modifications and/or translated into another
|
||||||
|
language. (Hereinafter, translation is included without limitation in
|
||||||
|
the term "modification".) Each licensee is addressed as "you".
|
||||||
|
|
||||||
|
Activities other than copying, distribution and modification are not
|
||||||
|
covered by this License; they are outside its scope. The act of
|
||||||
|
running the Program is not restricted, and the output from the Program
|
||||||
|
is covered only if its contents constitute a work based on the
|
||||||
|
Program (independent of having been made by running the Program).
|
||||||
|
Whether that is true depends on what the Program does.
|
||||||
|
|
||||||
|
1. You may copy and distribute verbatim copies of the Program's
|
||||||
|
source code as you receive it, in any medium, provided that you
|
||||||
|
conspicuously and appropriately publish on each copy an appropriate
|
||||||
|
copyright notice and disclaimer of warranty; keep intact all the
|
||||||
|
notices that refer to this License and to the absence of any warranty;
|
||||||
|
and give any other recipients of the Program a copy of this License
|
||||||
|
along with the Program.
|
||||||
|
|
||||||
|
You may charge a fee for the physical act of transferring a copy, and
|
||||||
|
you may at your option offer warranty protection in exchange for a fee.
|
||||||
|
|
||||||
|
2. You may modify your copy or copies of the Program or any portion
|
||||||
|
of it, thus forming a work based on the Program, and copy and
|
||||||
|
distribute such modifications or work under the terms of Section 1
|
||||||
|
above, provided that you also meet all of these conditions:
|
||||||
|
|
||||||
|
a) You must cause the modified files to carry prominent notices
|
||||||
|
stating that you changed the files and the date of any change.
|
||||||
|
|
||||||
|
b) You must cause any work that you distribute or publish, that in
|
||||||
|
whole or in part contains or is derived from the Program or any
|
||||||
|
part thereof, to be licensed as a whole at no charge to all third
|
||||||
|
parties under the terms of this License.
|
||||||
|
|
||||||
|
c) If the modified program normally reads commands interactively
|
||||||
|
when run, you must cause it, when started running for such
|
||||||
|
interactive use in the most ordinary way, to print or display an
|
||||||
|
announcement including an appropriate copyright notice and a
|
||||||
|
notice that there is no warranty (or else, saying that you provide
|
||||||
|
a warranty) and that users may redistribute the program under
|
||||||
|
these conditions, and telling the user how to view a copy of this
|
||||||
|
License. (Exception: if the Program itself is interactive but
|
||||||
|
does not normally print such an announcement, your work based on
|
||||||
|
the Program is not required to print an announcement.)
|
||||||
|
|
||||||
|
These requirements apply to the modified work as a whole. If
|
||||||
|
identifiable sections of that work are not derived from the Program,
|
||||||
|
and can be reasonably considered independent and separate works in
|
||||||
|
themselves, then this License, and its terms, do not apply to those
|
||||||
|
sections when you distribute them as separate works. But when you
|
||||||
|
distribute the same sections as part of a whole which is a work based
|
||||||
|
on the Program, the distribution of the whole must be on the terms of
|
||||||
|
this License, whose permissions for other licensees extend to the
|
||||||
|
entire whole, and thus to each and every part regardless of who wrote it.
|
||||||
|
|
||||||
|
Thus, it is not the intent of this section to claim rights or contest
|
||||||
|
your rights to work written entirely by you; rather, the intent is to
|
||||||
|
exercise the right to control the distribution of derivative or
|
||||||
|
collective works based on the Program.
|
||||||
|
|
||||||
|
In addition, mere aggregation of another work not based on the Program
|
||||||
|
with the Program (or with a work based on the Program) on a volume of
|
||||||
|
a storage or distribution medium does not bring the other work under
|
||||||
|
the scope of this License.
|
||||||
|
|
||||||
|
3. You may copy and distribute the Program (or a work based on it,
|
||||||
|
under Section 2) in object code or executable form under the terms of
|
||||||
|
Sections 1 and 2 above provided that you also do one of the following:
|
||||||
|
|
||||||
|
a) Accompany it with the complete corresponding machine-readable
|
||||||
|
source code, which must be distributed under the terms of Sections
|
||||||
|
1 and 2 above on a medium customarily used for software interchange; or,
|
||||||
|
|
||||||
|
b) Accompany it with a written offer, valid for at least three
|
||||||
|
years, to give any third party, for a charge no more than your
|
||||||
|
cost of physically performing source distribution, a complete
|
||||||
|
machine-readable copy of the corresponding source code, to be
|
||||||
|
distributed under the terms of Sections 1 and 2 above on a medium
|
||||||
|
customarily used for software interchange; or,
|
||||||
|
|
||||||
|
c) Accompany it with the information you received as to the offer
|
||||||
|
to distribute corresponding source code. (This alternative is
|
||||||
|
allowed only for noncommercial distribution and only if you
|
||||||
|
received the program in object code or executable form with such
|
||||||
|
an offer, in accord with Subsection b above.)
|
||||||
|
|
||||||
|
The source code for a work means the preferred form of the work for
|
||||||
|
making modifications to it. For an executable work, complete source
|
||||||
|
code means all the source code for all modules it contains, plus any
|
||||||
|
associated interface definition files, plus the scripts used to
|
||||||
|
control compilation and installation of the executable. However, as a
|
||||||
|
special exception, the source code distributed need not include
|
||||||
|
anything that is normally distributed (in either source or binary
|
||||||
|
form) with the major components (compiler, kernel, and so on) of the
|
||||||
|
operating system on which the executable runs, unless that component
|
||||||
|
itself accompanies the executable.
|
||||||
|
|
||||||
|
If distribution of executable or object code is made by offering
|
||||||
|
access to copy from a designated place, then offering equivalent
|
||||||
|
access to copy the source code from the same place counts as
|
||||||
|
distribution of the source code, even though third parties are not
|
||||||
|
compelled to copy the source along with the object code.
|
||||||
|
|
||||||
|
4. You may not copy, modify, sublicense, or distribute the Program
|
||||||
|
except as expressly provided under this License. Any attempt
|
||||||
|
otherwise to copy, modify, sublicense or distribute the Program is
|
||||||
|
void, and will automatically terminate your rights under this License.
|
||||||
|
However, parties who have received copies, or rights, from you under
|
||||||
|
this License will not have their licenses terminated so long as such
|
||||||
|
parties remain in full compliance.
|
||||||
|
|
||||||
|
5. You are not required to accept this License, since you have not
|
||||||
|
signed it. However, nothing else grants you permission to modify or
|
||||||
|
distribute the Program or its derivative works. These actions are
|
||||||
|
prohibited by law if you do not accept this License. Therefore, by
|
||||||
|
modifying or distributing the Program (or any work based on the
|
||||||
|
Program), you indicate your acceptance of this License to do so, and
|
||||||
|
all its terms and conditions for copying, distributing or modifying
|
||||||
|
the Program or works based on it.
|
||||||
|
|
||||||
|
6. Each time you redistribute the Program (or any work based on the
|
||||||
|
Program), the recipient automatically receives a license from the
|
||||||
|
original licensor to copy, distribute or modify the Program subject to
|
||||||
|
these terms and conditions. You may not impose any further
|
||||||
|
restrictions on the recipients' exercise of the rights granted herein.
|
||||||
|
You are not responsible for enforcing compliance by third parties to
|
||||||
|
this License.
|
||||||
|
|
||||||
|
7. If, as a consequence of a court judgment or allegation of patent
|
||||||
|
infringement or for any other reason (not limited to patent issues),
|
||||||
|
conditions are imposed on you (whether by court order, agreement or
|
||||||
|
otherwise) that contradict the conditions of this License, they do not
|
||||||
|
excuse you from the conditions of this License. If you cannot
|
||||||
|
distribute so as to satisfy simultaneously your obligations under this
|
||||||
|
License and any other pertinent obligations, then as a consequence you
|
||||||
|
may not distribute the Program at all. For example, if a patent
|
||||||
|
license would not permit royalty-free redistribution of the Program by
|
||||||
|
all those who receive copies directly or indirectly through you, then
|
||||||
|
the only way you could satisfy both it and this License would be to
|
||||||
|
refrain entirely from distribution of the Program.
|
||||||
|
|
||||||
|
If any portion of this section is held invalid or unenforceable under
|
||||||
|
any particular circumstance, the balance of the section is intended to
|
||||||
|
apply and the section as a whole is intended to apply in other
|
||||||
|
circumstances.
|
||||||
|
|
||||||
|
It is not the purpose of this section to induce you to infringe any
|
||||||
|
patents or other property right claims or to contest validity of any
|
||||||
|
such claims; this section has the sole purpose of protecting the
|
||||||
|
integrity of the free software distribution system, which is
|
||||||
|
implemented by public license practices. Many people have made
|
||||||
|
generous contributions to the wide range of software distributed
|
||||||
|
through that system in reliance on consistent application of that
|
||||||
|
system; it is up to the author/donor to decide if he or she is willing
|
||||||
|
to distribute software through any other system and a licensee cannot
|
||||||
|
impose that choice.
|
||||||
|
|
||||||
|
This section is intended to make thoroughly clear what is believed to
|
||||||
|
be a consequence of the rest of this License.
|
||||||
|
|
||||||
|
8. If the distribution and/or use of the Program is restricted in
|
||||||
|
certain countries either by patents or by copyrighted interfaces, the
|
||||||
|
original copyright holder who places the Program under this License
|
||||||
|
may add an explicit geographical distribution limitation excluding
|
||||||
|
those countries, so that distribution is permitted only in or among
|
||||||
|
countries not thus excluded. In such case, this License incorporates
|
||||||
|
the limitation as if written in the body of this License.
|
||||||
|
|
||||||
|
9. The Free Software Foundation may publish revised and/or new versions
|
||||||
|
of the General Public License from time to time. Such new versions will
|
||||||
|
be similar in spirit to the present version, but may differ in detail to
|
||||||
|
address new problems or concerns.
|
||||||
|
|
||||||
|
Each version is given a distinguishing version number. If the Program
|
||||||
|
specifies a version number of this License which applies to it and "any
|
||||||
|
later version", you have the option of following the terms and conditions
|
||||||
|
either of that version or of any later version published by the Free
|
||||||
|
Software Foundation. If the Program does not specify a version number of
|
||||||
|
this License, you may choose any version ever published by the Free Software
|
||||||
|
Foundation.
|
||||||
|
|
||||||
|
10. If you wish to incorporate parts of the Program into other free
|
||||||
|
programs whose distribution conditions are different, write to the author
|
||||||
|
to ask for permission. For software which is copyrighted by the Free
|
||||||
|
Software Foundation, write to the Free Software Foundation; we sometimes
|
||||||
|
make exceptions for this. Our decision will be guided by the two goals
|
||||||
|
of preserving the free status of all derivatives of our free software and
|
||||||
|
of promoting the sharing and reuse of software generally.
|
||||||
|
|
||||||
|
NO WARRANTY
|
||||||
|
|
||||||
|
11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
|
||||||
|
FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
|
||||||
|
OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
|
||||||
|
PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
|
||||||
|
OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
|
||||||
|
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
|
||||||
|
TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
|
||||||
|
PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
|
||||||
|
REPAIR OR CORRECTION.
|
||||||
|
|
||||||
|
12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||||
|
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
|
||||||
|
REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
|
||||||
|
INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
|
||||||
|
OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
|
||||||
|
TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
|
||||||
|
YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
|
||||||
|
PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
|
||||||
|
POSSIBILITY OF SUCH DAMAGES.
|
||||||
|
|
||||||
|
END OF TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
How to Apply These Terms to Your New Programs
|
||||||
|
|
||||||
|
If you develop a new program, and you want it to be of the greatest
|
||||||
|
possible use to the public, the best way to achieve this is to make it
|
||||||
|
free software which everyone can redistribute and change under these terms.
|
||||||
|
|
||||||
|
To do so, attach the following notices to the program. It is safest
|
||||||
|
to attach them to the start of each source file to most effectively
|
||||||
|
convey the exclusion of warranty; and each file should have at least
|
||||||
|
the "copyright" line and a pointer to where the full notice is found.
|
||||||
|
|
||||||
|
<one line to give the program's name and a brief idea of what it does.>
|
||||||
|
Copyright (C) <year> <name of author>
|
||||||
|
|
||||||
|
This program 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 2 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program 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 this program; if not, write to the Free Software Foundation, Inc.,
|
||||||
|
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||||
|
|
||||||
|
Also add information on how to contact you by electronic and paper mail.
|
||||||
|
|
||||||
|
If the program is interactive, make it output a short notice like this
|
||||||
|
when it starts in an interactive mode:
|
||||||
|
|
||||||
|
Gnomovision version 69, Copyright (C) year name of author
|
||||||
|
Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
||||||
|
This is free software, and you are welcome to redistribute it
|
||||||
|
under certain conditions; type `show c' for details.
|
||||||
|
|
||||||
|
The hypothetical commands `show w' and `show c' should show the appropriate
|
||||||
|
parts of the General Public License. Of course, the commands you use may
|
||||||
|
be called something other than `show w' and `show c'; they could even be
|
||||||
|
mouse-clicks or menu items--whatever suits your program.
|
||||||
|
|
||||||
|
You should also get your employer (if you work as a programmer) or your
|
||||||
|
school, if any, to sign a "copyright disclaimer" for the program, if
|
||||||
|
necessary. Here is a sample; alter the names:
|
||||||
|
|
||||||
|
Yoyodyne, Inc., hereby disclaims all copyright interest in the program
|
||||||
|
`Gnomovision' (which makes passes at compilers) written by James Hacker.
|
||||||
|
|
||||||
|
<signature of Ty Coon>, 1 April 1989
|
||||||
|
Ty Coon, President of Vice
|
||||||
|
|
||||||
|
This General Public License does not permit incorporating your program into
|
||||||
|
proprietary programs. If your program is a subroutine library, you may
|
||||||
|
consider it more useful to permit linking proprietary applications with the
|
||||||
|
library. If this is what you want to do, use the GNU Lesser General
|
||||||
|
Public License instead of this License.
|
||||||
25
README.md
Normal file
25
README.md
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
# vlcro
|
||||||
|
|
||||||
|
WIP README
|
||||||
|
|
||||||
|
A parallel frontend for zypper. Speeds up slow zypper operations by running them in parallel across chroot-isolated workers
|
||||||
|
|
||||||
|
|
||||||
|
inspirated by [zypperoni](https://github.com/pavinjosdev/zypperoni)
|
||||||
|
|
||||||
|
|
||||||
|
## Build
|
||||||
|
|
||||||
|
```
|
||||||
|
go build -o vlcro .
|
||||||
|
```
|
||||||
|
|
||||||
|
To install copy to path
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```
|
||||||
|
sudo vlcro <command> [options]
|
||||||
|
```
|
||||||
|
|
||||||
|
Downloads are handled in parallel; the actual install is handed back to zypper.
|
||||||
272
chroot.go
Normal file
272
chroot.go
Normal file
|
|
@ -0,0 +1,272 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"syscall"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// mount tracker
|
||||||
|
|
||||||
|
type MountTracker struct {
|
||||||
|
mounts []string
|
||||||
|
mu sync.Mutex
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mt *MountTracker) Add(path string) {
|
||||||
|
mt.mu.Lock()
|
||||||
|
defer mt.mu.Unlock()
|
||||||
|
mt.mounts = append(mt.mounts, path)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mt *MountTracker) GetAll() []string {
|
||||||
|
mt.mu.Lock()
|
||||||
|
defer mt.mu.Unlock()
|
||||||
|
result := make([]string, len(mt.mounts))
|
||||||
|
copy(result, mt.mounts)
|
||||||
|
sort.SliceStable(result, func(i, j int) bool {
|
||||||
|
return len(result[i]) > len(result[j])
|
||||||
|
})
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mt *MountTracker) Reset() {
|
||||||
|
mt.mu.Lock()
|
||||||
|
defer mt.mu.Unlock()
|
||||||
|
mt.mounts = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// mount helpers
|
||||||
|
|
||||||
|
func findActiveMountCount(path string) int {
|
||||||
|
if path == "" {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
cmd := exec.Command("findmnt", "-R", "-n", "-o", "TARGET", path)
|
||||||
|
out, err := cmd.Output()
|
||||||
|
if err != nil {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
lines := strings.Split(strings.TrimSpace(string(out)), "\n")
|
||||||
|
if len(lines) == 1 && lines[0] == "" {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return len(lines)
|
||||||
|
}
|
||||||
|
|
||||||
|
func hasActiveMounts(path string) bool {
|
||||||
|
return findActiveMountCount(path) > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func retryMount(source, target, fstype string, flags uintptr, data string, maxRetries int) error {
|
||||||
|
var lastErr error
|
||||||
|
for attempt := 0; attempt < maxRetries; attempt++ {
|
||||||
|
if attempt > 0 {
|
||||||
|
time.Sleep(retryDelay)
|
||||||
|
}
|
||||||
|
err := syscall.Mount(source, target, fstype, flags, data)
|
||||||
|
if err == nil {
|
||||||
|
debugf("Mounted %s -> %s", source, target)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
lastErr = err
|
||||||
|
debugf("Mount attempt %d failed for %s: %v", attempt+1, target, err)
|
||||||
|
}
|
||||||
|
return fmt.Errorf("mount %s failed after %d attempts: %w", target, maxRetries, lastErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
func retryUnmount(target string, flags int, maxRetries int) error {
|
||||||
|
var lastErr error
|
||||||
|
for attempt := 0; attempt < maxRetries; attempt++ {
|
||||||
|
if attempt > 0 {
|
||||||
|
time.Sleep(retryDelay)
|
||||||
|
}
|
||||||
|
err := syscall.Unmount(target, flags)
|
||||||
|
if err == nil || err == syscall.EINVAL || err == syscall.ENOENT {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
lastErr = err
|
||||||
|
debugf("Unmount attempt %d failed for %s: %v", attempt+1, target, err)
|
||||||
|
}
|
||||||
|
return fmt.Errorf("unmount %s failed after %d attempts: %w", target, maxRetries, lastErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
// chroot setup
|
||||||
|
|
||||||
|
func setupChroot(tmpDir, uuid string, needDev bool, tracker *MountTracker) error {
|
||||||
|
base := filepath.Join(tmpDir, uuid)
|
||||||
|
rootfs := filepath.Join(base, "rootfs")
|
||||||
|
|
||||||
|
dirs := []string{
|
||||||
|
rootfs,
|
||||||
|
filepath.Join(base, "run"),
|
||||||
|
filepath.Join(base, "var", "cache", "zypp"),
|
||||||
|
filepath.Join(base, "var", "lib", "ca-certificates"),
|
||||||
|
}
|
||||||
|
for _, d := range dirs {
|
||||||
|
if err := os.MkdirAll(d, 0755); err != nil {
|
||||||
|
return fmt.Errorf("mkdir %s: %w", d, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resolvPath := "/etc/resolv.conf"
|
||||||
|
if linkTarget, err := os.Readlink(resolvPath); err == nil {
|
||||||
|
if !filepath.IsAbs(linkTarget) {
|
||||||
|
linkTarget = filepath.Join("/etc", linkTarget)
|
||||||
|
}
|
||||||
|
target := filepath.Join(base, linkTarget)
|
||||||
|
os.MkdirAll(filepath.Dir(target), 0755)
|
||||||
|
if data, err := os.ReadFile(linkTarget); err == nil {
|
||||||
|
os.WriteFile(target, data, 0644)
|
||||||
|
}
|
||||||
|
} else if data, err := os.ReadFile(resolvPath); err == nil {
|
||||||
|
target := filepath.Join(base, resolvPath)
|
||||||
|
os.MkdirAll(filepath.Dir(target), 0755)
|
||||||
|
os.WriteFile(target, data, 0644)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := retryMount("/", rootfs, "", syscall.MS_BIND|syscall.MS_RDONLY, "", maxRetries); err != nil {
|
||||||
|
return fmt.Errorf("mount rootfs: %w", err)
|
||||||
|
}
|
||||||
|
tracker.Add(rootfs)
|
||||||
|
|
||||||
|
if needDev {
|
||||||
|
devPath := filepath.Join(rootfs, "dev")
|
||||||
|
if err := retryMount("/dev", devPath, "", syscall.MS_BIND, "", maxRetries); err != nil {
|
||||||
|
return fmt.Errorf("mount /dev bind: %w", err)
|
||||||
|
}
|
||||||
|
tracker.Add(devPath)
|
||||||
|
|
||||||
|
tmpPath := filepath.Join(rootfs, "tmp")
|
||||||
|
if err := retryMount("tmpfs", tmpPath, "tmpfs", 0, "", maxRetries); err != nil {
|
||||||
|
return fmt.Errorf("mount tmpfs: %w", err)
|
||||||
|
}
|
||||||
|
tracker.Add(tmpPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
runPath := filepath.Join(rootfs, "run")
|
||||||
|
if err := retryMount(filepath.Join(base, "run"), runPath, "", syscall.MS_BIND, "", maxRetries); err != nil {
|
||||||
|
return fmt.Errorf("mount run: %w", err)
|
||||||
|
}
|
||||||
|
tracker.Add(runPath)
|
||||||
|
|
||||||
|
varPath := filepath.Join(rootfs, "var")
|
||||||
|
if err := retryMount(filepath.Join(base, "var"), varPath, "", syscall.MS_BIND, "", maxRetries); err != nil {
|
||||||
|
return fmt.Errorf("mount var: %w", err)
|
||||||
|
}
|
||||||
|
tracker.Add(varPath)
|
||||||
|
|
||||||
|
cachePath := filepath.Join(rootfs, "var", "cache", "zypp")
|
||||||
|
if err := retryMount("/var/cache/zypp", cachePath, "", syscall.MS_BIND, "", maxRetries); err != nil {
|
||||||
|
debugf("Warning: failed to mount cache (non-critical): %v", err)
|
||||||
|
} else {
|
||||||
|
tracker.Add(cachePath)
|
||||||
|
}
|
||||||
|
|
||||||
|
certPath := filepath.Join(rootfs, "var", "lib", "ca-certificates")
|
||||||
|
if err := retryMount("/var/lib/ca-certificates", certPath, "", syscall.MS_BIND|syscall.MS_RDONLY, "", maxRetries); err != nil {
|
||||||
|
debugf("Warning: failed to mount ca-certs (non-critical): %v", err)
|
||||||
|
} else {
|
||||||
|
tracker.Add(certPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// unmount
|
||||||
|
|
||||||
|
func unmountAll(tmpDir string, tracker *MountTracker) error {
|
||||||
|
if tmpDir == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
mounts := tracker.GetAll()
|
||||||
|
var errs []string
|
||||||
|
|
||||||
|
for _, mountPath := range mounts {
|
||||||
|
cmd := exec.Command("findmnt", "-n", "-o", "TARGET", mountPath)
|
||||||
|
if err := cmd.Run(); err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := retryUnmount(mountPath, syscall.MNT_DETACH, maxRetries); err != nil {
|
||||||
|
debugf("Failed to unmount %s: %v", mountPath, err)
|
||||||
|
if err := syscall.Unmount(mountPath, syscall.MNT_DETACH|syscall.MNT_FORCE); err != nil {
|
||||||
|
errs = append(errs, fmt.Sprintf("%s: %v", mountPath, err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(errs) > 0 {
|
||||||
|
warningf("Some unmounts failed: %s", strings.Join(errs, "; "))
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// cleanup
|
||||||
|
|
||||||
|
func vlcroCleanup(tmpDir string, tracker *MountTracker) {
|
||||||
|
releaseZyppLock()
|
||||||
|
|
||||||
|
if tmpDir == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := os.Stat(tmpDir); os.IsNotExist(err) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
absPath, err := filepath.Abs(tmpDir)
|
||||||
|
if err != nil || !strings.HasPrefix(absPath, "/tmp/vlcro_") {
|
||||||
|
errorf("Refusing to cleanup unsafe temp directory: %s", tmpDir)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
infof("%s", colorText(colorInfo, "Cleaning up temp mounts..."))
|
||||||
|
unmountAll(tmpDir, tracker)
|
||||||
|
|
||||||
|
time.Sleep(500 * time.Millisecond)
|
||||||
|
|
||||||
|
for attempt := 0; attempt < 5; attempt++ {
|
||||||
|
if !hasActiveMounts(tmpDir) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
warningf("Active mounts still detected under %s, retrying unmount...", tmpDir)
|
||||||
|
unmountAll(tmpDir, tracker)
|
||||||
|
time.Sleep(500 * time.Millisecond)
|
||||||
|
}
|
||||||
|
|
||||||
|
if hasActiveMounts(tmpDir) {
|
||||||
|
warningf("Could not unmount all filesystems under %s. Skipping temp directory removal.", tmpDir)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
infof("%s", colorText(colorInfo, "Cleaning up temp directory..."))
|
||||||
|
for attempt := 0; attempt < 3; attempt++ {
|
||||||
|
if err := os.RemoveAll(tmpDir); err == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
time.Sleep(500 * time.Millisecond)
|
||||||
|
}
|
||||||
|
|
||||||
|
warningf("Could not fully remove temp directory %s (will clean up at next boot)", tmpDir)
|
||||||
|
}
|
||||||
|
|
||||||
|
// uuid
|
||||||
|
|
||||||
|
func randomUUID() string {
|
||||||
|
b := make([]byte, 16)
|
||||||
|
if _, err := rand.Read(b); err != nil {
|
||||||
|
return fmt.Sprintf("fallback-%d", time.Now().UnixNano())
|
||||||
|
}
|
||||||
|
b[6] = (b[6] & 0x0f) | 0x40
|
||||||
|
b[8] = (b[8] & 0x3f) | 0x80
|
||||||
|
return fmt.Sprintf("%x-%x-%x-%x-%x", b[0:4], b[4:6], b[6:8], b[8:10], b[10:16])
|
||||||
|
}
|
||||||
3
go.mod
Normal file
3
go.mod
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
module vlcro
|
||||||
|
|
||||||
|
go 1.26.2
|
||||||
355
main.go
Normal file
355
main.go
Normal file
|
|
@ -0,0 +1,355 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"os/signal"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"syscall"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
vlcroVersion = "0.1.0"
|
||||||
|
zypperPidFile = "/run/zypp.pid"
|
||||||
|
maxRetries = 3
|
||||||
|
retryDelay = 100_000_000 // 100ms as int64 nanoseconds
|
||||||
|
)
|
||||||
|
|
||||||
|
var cfg *config
|
||||||
|
|
||||||
|
type config struct {
|
||||||
|
version bool
|
||||||
|
noConfirm bool
|
||||||
|
jobs int
|
||||||
|
debug bool
|
||||||
|
noColor bool
|
||||||
|
command string
|
||||||
|
force bool
|
||||||
|
downloadOnly bool
|
||||||
|
packages []string
|
||||||
|
txInfo []packageInfo // parsed packages from dry-run
|
||||||
|
}
|
||||||
|
|
||||||
|
// CLI
|
||||||
|
|
||||||
|
func printHelp() {
|
||||||
|
// Ensure cfg is available for colorStart
|
||||||
|
if cfg == nil {
|
||||||
|
cfg = &config{jobs: 10}
|
||||||
|
}
|
||||||
|
|
||||||
|
// highlight colors
|
||||||
|
hi := colorStart(colorInfo) // section headers / "vlcro"
|
||||||
|
hc := colorStart(colorWarning) // usage keyword
|
||||||
|
he := colorStart(colorError) // options
|
||||||
|
hd := colorStart(colorInput) // command bold-ish
|
||||||
|
d := "\033[0m"
|
||||||
|
if !isTerminal || cfg.noColor {
|
||||||
|
d = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
usage := fmt.Sprintf(`%susage%s: vlcro [-h] [-v] [-y] [-j N] [--debug] [--no-color]
|
||||||
|
%s {refresh,ref,dist-upgrade,dup,install,in,install-new-recommends,inr} ...
|
||||||
|
|
||||||
|
%s%s%s (%sv%s%s) makes zypper faster by running slow operations in parallel.
|
||||||
|
|
||||||
|
%soptions%s:
|
||||||
|
%s-h, --help%s show this help message and exit
|
||||||
|
%s-v, -V, --version%s print version number and exit
|
||||||
|
%s-y, --no-confirm%s automatic yes to prompts, run non-interactively
|
||||||
|
%s-j, --jobs%s number of parallel operations (1-64, default: 10)
|
||||||
|
%s--debug%s enable debug output
|
||||||
|
%s--no-color%s disable color output
|
||||||
|
|
||||||
|
%scommands%s:
|
||||||
|
%srefresh%s (%sref%s) refresh all enabled repos
|
||||||
|
%s-f, --force%s force a complete refresh
|
||||||
|
|
||||||
|
%sdist-upgrade%s (%sdup%s) perform distribution upgrade
|
||||||
|
%s-d, --download-only%s download packages without installing
|
||||||
|
|
||||||
|
%sinstall%s (%sin%s) install one or more packages
|
||||||
|
%s-d, --download-only%s download packages without installing
|
||||||
|
%s<package>%s package name(s) to install
|
||||||
|
|
||||||
|
%sinstall-new-recommends%s (%sinr%s)
|
||||||
|
install new packages recommended by already installed ones
|
||||||
|
%s-d, --download-only%s download packages without installing
|
||||||
|
`,
|
||||||
|
hc, d, hc,
|
||||||
|
hi, "vlcro", d, hi, vlcroVersion, d,
|
||||||
|
hi, d,
|
||||||
|
he, d, he, d, he, d, he, d, he, d, he, d,
|
||||||
|
hi, d,
|
||||||
|
hd, d, hd, d, he, d,
|
||||||
|
hd, d, hd, d, he, d,
|
||||||
|
hd, d, hd, d, he, d, hd, d,
|
||||||
|
hd, d, hd, d, he, d)
|
||||||
|
|
||||||
|
fmt.Print(usage)
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseArgs() *config {
|
||||||
|
c, err := parseArgsInto(os.Args[1:])
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintln(os.Stderr, err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseJobs(c *config, value string) error {
|
||||||
|
n, err := strconv.Atoi(value)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("Invalid jobs value: %s", value)
|
||||||
|
}
|
||||||
|
if n < 1 || n > 64 {
|
||||||
|
return fmt.Errorf("Invalid jobs value: %d. Must be between 1 and 64", n)
|
||||||
|
}
|
||||||
|
c.jobs = n
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func isRefreshCommand(cmd string) bool {
|
||||||
|
return cmd == "refresh" || cmd == "ref"
|
||||||
|
}
|
||||||
|
|
||||||
|
func isInstallCommand(cmd string) bool {
|
||||||
|
return cmd == "install" || cmd == "in"
|
||||||
|
}
|
||||||
|
|
||||||
|
func isKnownCommand(cmd string) bool {
|
||||||
|
switch cmd {
|
||||||
|
case "refresh", "ref", "dist-upgrade", "dup", "install", "in",
|
||||||
|
"install-new-recommends", "inr":
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseArgsInto(args []string) (*config, error) {
|
||||||
|
c := &config{jobs: 10}
|
||||||
|
i := 0
|
||||||
|
cmdFound := false
|
||||||
|
packagesOnly := false
|
||||||
|
|
||||||
|
for i < len(args) {
|
||||||
|
arg := args[i]
|
||||||
|
consumedNext := false
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case arg == "-h" || arg == "--help":
|
||||||
|
printHelp()
|
||||||
|
os.Exit(0)
|
||||||
|
case arg == "-v" || arg == "-V" || arg == "--version":
|
||||||
|
c.version = true
|
||||||
|
i++
|
||||||
|
continue
|
||||||
|
case arg == "-y" || arg == "--no-confirm":
|
||||||
|
c.noConfirm = true
|
||||||
|
i++
|
||||||
|
continue
|
||||||
|
case arg == "--debug":
|
||||||
|
c.debug = true
|
||||||
|
i++
|
||||||
|
continue
|
||||||
|
case arg == "--no-color":
|
||||||
|
c.noColor = true
|
||||||
|
i++
|
||||||
|
continue
|
||||||
|
case arg == "-f" || arg == "--force":
|
||||||
|
c.force = true
|
||||||
|
i++
|
||||||
|
continue
|
||||||
|
case arg == "-d" || arg == "--download-only":
|
||||||
|
c.downloadOnly = true
|
||||||
|
i++
|
||||||
|
continue
|
||||||
|
case strings.HasPrefix(arg, "--"):
|
||||||
|
name, value, hasValue := strings.Cut(arg, "=")
|
||||||
|
if name == "--jobs" {
|
||||||
|
if !hasValue {
|
||||||
|
if i+1 >= len(args) {
|
||||||
|
return nil, fmt.Errorf("Error: --jobs requires an argument")
|
||||||
|
}
|
||||||
|
value = args[i+1]
|
||||||
|
i += 2
|
||||||
|
} else {
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
if err := parseJobs(c, value); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("Unknown option: %s", arg)
|
||||||
|
case strings.HasPrefix(arg, "-") && len(arg) > 1:
|
||||||
|
body := arg[1:]
|
||||||
|
for k := 0; k < len(body); k++ {
|
||||||
|
switch body[k] {
|
||||||
|
case 'h':
|
||||||
|
printHelp()
|
||||||
|
os.Exit(0)
|
||||||
|
case 'v', 'V':
|
||||||
|
c.version = true
|
||||||
|
case 'y':
|
||||||
|
c.noConfirm = true
|
||||||
|
case 'f':
|
||||||
|
c.force = true
|
||||||
|
case 'd':
|
||||||
|
c.downloadOnly = true
|
||||||
|
case 'j':
|
||||||
|
rest := body[k+1:]
|
||||||
|
if strings.HasPrefix(rest, "=") {
|
||||||
|
rest = rest[1:]
|
||||||
|
}
|
||||||
|
if rest == "" {
|
||||||
|
if i+1 >= len(args) {
|
||||||
|
return nil, fmt.Errorf("Error: --jobs requires an argument")
|
||||||
|
}
|
||||||
|
rest = args[i+1]
|
||||||
|
consumedNext = true
|
||||||
|
}
|
||||||
|
if err := parseJobs(c, rest); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
k = len(body)
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("Unknown option: -%s", string(body[k]))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !consumedNext {
|
||||||
|
i++
|
||||||
|
} else {
|
||||||
|
i += 2
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
default:
|
||||||
|
if !cmdFound {
|
||||||
|
c.command = arg
|
||||||
|
cmdFound = true
|
||||||
|
packagesOnly = isInstallCommand(arg)
|
||||||
|
i++
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if !packagesOnly {
|
||||||
|
return nil, fmt.Errorf("Unexpected argument: %s", arg)
|
||||||
|
}
|
||||||
|
c.packages = append(c.packages, arg)
|
||||||
|
i++
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !cmdFound {
|
||||||
|
return c, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if !isKnownCommand(c.command) {
|
||||||
|
return nil, fmt.Errorf("Unknown command: %s", c.command)
|
||||||
|
}
|
||||||
|
if c.force && !isRefreshCommand(c.command) {
|
||||||
|
return nil, fmt.Errorf("Option --force is only valid with the refresh command")
|
||||||
|
}
|
||||||
|
if c.downloadOnly && isRefreshCommand(c.command) {
|
||||||
|
return nil, fmt.Errorf("Option --download-only is not valid with the refresh command")
|
||||||
|
}
|
||||||
|
|
||||||
|
return c, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// dependency check (exec.LookPath)
|
||||||
|
|
||||||
|
func checkDependencies() {
|
||||||
|
required := []string{"zypper", "findmnt", "bash"}
|
||||||
|
missing := []string{}
|
||||||
|
for _, program := range required {
|
||||||
|
if _, err := exec.LookPath(program); err != nil {
|
||||||
|
missing = append(missing, program)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(missing) > 0 {
|
||||||
|
msg := fmt.Sprintf("Bailing out, missing required dependencies: %s\n"+
|
||||||
|
"The following are required for vlcro to function: %s",
|
||||||
|
strings.Join(missing, ", "),
|
||||||
|
strings.Join(required, ", "))
|
||||||
|
errorf("%s", colorText(colorError, msg))
|
||||||
|
os.Exit(4)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// main
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
cfg = parseArgs()
|
||||||
|
|
||||||
|
if cfg.version {
|
||||||
|
fmt.Printf("vlcro v%s\n", vlcroVersion)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if cfg.command == "" {
|
||||||
|
printHelp()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if os.Getuid() != 0 {
|
||||||
|
errorf("%s", colorText(colorError, "Bailing out, program must be run with root privileges"))
|
||||||
|
os.Exit(3)
|
||||||
|
}
|
||||||
|
|
||||||
|
checkDependencies()
|
||||||
|
|
||||||
|
if pid, program := checkZypperRunning(); pid != 0 {
|
||||||
|
msg := fmt.Sprintf("zypper is already invoked by the application with pid %d (%s).\nClose this application before trying again.", pid, program)
|
||||||
|
errorf("%s", colorText(colorError, msg))
|
||||||
|
os.Exit(5)
|
||||||
|
}
|
||||||
|
|
||||||
|
tmpDir, err := os.MkdirTemp("/tmp", "vlcro_")
|
||||||
|
if err != nil {
|
||||||
|
errorf("%s", colorText(colorError, fmt.Sprintf("Failed to create temp directory: %v", err)))
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
var exitCode int
|
||||||
|
defer func() {
|
||||||
|
os.Exit(exitCode)
|
||||||
|
}()
|
||||||
|
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
sigChan := make(chan os.Signal, 1)
|
||||||
|
signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM)
|
||||||
|
go func() {
|
||||||
|
<-sigChan
|
||||||
|
infof("%s", colorText(colorInfo, "Cancelling pending tasks..."))
|
||||||
|
os.Stdin.Close()
|
||||||
|
cancel()
|
||||||
|
}()
|
||||||
|
|
||||||
|
switch cfg.command {
|
||||||
|
case "refresh", "ref":
|
||||||
|
if err := handleRefresh(ctx, tmpDir); err != nil {
|
||||||
|
exitCode = 1
|
||||||
|
}
|
||||||
|
case "dist-upgrade", "dup":
|
||||||
|
if err := handleDistUpgrade(ctx, tmpDir); err != nil {
|
||||||
|
exitCode = 1
|
||||||
|
}
|
||||||
|
case "install", "in":
|
||||||
|
if err := handleInstall(ctx, tmpDir); err != nil {
|
||||||
|
exitCode = 1
|
||||||
|
}
|
||||||
|
case "install-new-recommends", "inr":
|
||||||
|
if err := handleINR(ctx, tmpDir); err != nil {
|
||||||
|
exitCode = 1
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
printHelp()
|
||||||
|
exitCode = 1
|
||||||
|
}
|
||||||
|
}
|
||||||
453
ui.go
Normal file
453
ui.go
Normal file
|
|
@ -0,0 +1,453 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"syscall"
|
||||||
|
"time"
|
||||||
|
"unsafe"
|
||||||
|
)
|
||||||
|
|
||||||
|
// color system
|
||||||
|
|
||||||
|
type colorType string
|
||||||
|
|
||||||
|
const (
|
||||||
|
colorInput colorType = "input"
|
||||||
|
colorSuccess colorType = "success"
|
||||||
|
colorInfo colorType = "info"
|
||||||
|
colorWarning colorType = "warning"
|
||||||
|
colorError colorType = "error"
|
||||||
|
colorException colorType = "exception"
|
||||||
|
)
|
||||||
|
|
||||||
|
var colorCodes = map[colorType][3]int{
|
||||||
|
colorInput: {0, 200, 200},
|
||||||
|
colorSuccess: {80, 220, 80},
|
||||||
|
colorInfo: {255, 175, 50},
|
||||||
|
colorWarning: {255, 220, 60},
|
||||||
|
colorError: {255, 90, 90},
|
||||||
|
colorException: {220, 120, 255},
|
||||||
|
}
|
||||||
|
|
||||||
|
var isTerminal = func() bool {
|
||||||
|
fi, err := os.Stdout.Stat()
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return fi.Mode()&os.ModeCharDevice != 0
|
||||||
|
}()
|
||||||
|
|
||||||
|
func colorText(textType colorType, text string) string {
|
||||||
|
if !isTerminal || (cfg != nil && cfg.noColor) {
|
||||||
|
return text
|
||||||
|
}
|
||||||
|
c, ok := colorCodes[textType]
|
||||||
|
if !ok {
|
||||||
|
return text
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("\033[38;2;%d;%d;%dm%s\033[0m", c[0], c[1], c[2], text)
|
||||||
|
}
|
||||||
|
|
||||||
|
func colorStart(textType colorType) string {
|
||||||
|
if !isTerminal || (cfg != nil && cfg.noColor) {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
c, ok := colorCodes[textType]
|
||||||
|
if !ok {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("\033[38;2;%d;%d;%dm", c[0], c[1], c[2])
|
||||||
|
}
|
||||||
|
|
||||||
|
// terminal width
|
||||||
|
|
||||||
|
func getTerminalWidth() int {
|
||||||
|
if !isTerminal {
|
||||||
|
return 80
|
||||||
|
}
|
||||||
|
type winsize struct {
|
||||||
|
Row uint16
|
||||||
|
Col uint16
|
||||||
|
Xpixel uint16
|
||||||
|
Ypixel uint16
|
||||||
|
}
|
||||||
|
ws := winsize{}
|
||||||
|
_, _, errno := syscall.Syscall(syscall.SYS_IOCTL, uintptr(os.Stdout.Fd()), uintptr(0x5413), uintptr(unsafe.Pointer(&ws)))
|
||||||
|
if errno != 0 || ws.Col == 0 {
|
||||||
|
return 80
|
||||||
|
}
|
||||||
|
return int(ws.Col)
|
||||||
|
}
|
||||||
|
|
||||||
|
// output serialization
|
||||||
|
|
||||||
|
var outputMu sync.Mutex
|
||||||
|
|
||||||
|
// logging
|
||||||
|
|
||||||
|
func logMsg(level, format string, args ...interface{}) {
|
||||||
|
outputMu.Lock()
|
||||||
|
defer outputMu.Unlock()
|
||||||
|
|
||||||
|
_clearProgressLine()
|
||||||
|
|
||||||
|
msg := fmt.Sprintf(format, args...)
|
||||||
|
ts := time.Now().Format("15:04:05")
|
||||||
|
fmt.Fprintf(os.Stdout, "%s %s %s\n", ts, colorText(colorType(strings.ToLower(level)), fmt.Sprintf("%-7s", level)), msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
func infof(format string, args ...interface{}) { logMsg("INFO", format, args...) }
|
||||||
|
func errorf(format string, args ...interface{}) { logMsg("ERROR", format, args...) }
|
||||||
|
func warningf(format string, args ...interface{}) {
|
||||||
|
logMsg("WARNING", format, args...)
|
||||||
|
}
|
||||||
|
func debugf(format string, args ...interface{}) {
|
||||||
|
if cfg.debug {
|
||||||
|
logMsg("DEBUG", format, args...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// status messages
|
||||||
|
|
||||||
|
func statusMsg(text string) {
|
||||||
|
outputMu.Lock()
|
||||||
|
defer outputMu.Unlock()
|
||||||
|
_clearProgressLine()
|
||||||
|
fmt.Printf("%s... ", text)
|
||||||
|
fmt.Println(colorText(colorSuccess, "Done"))
|
||||||
|
}
|
||||||
|
|
||||||
|
// download line
|
||||||
|
|
||||||
|
func downloadLine(counter int, name string, size float64) {
|
||||||
|
outputMu.Lock()
|
||||||
|
defer outputMu.Unlock()
|
||||||
|
_clearProgressLine()
|
||||||
|
if size > 0 {
|
||||||
|
fmt.Printf("Get:%-3d %s [%s]\n", counter, colorText(colorInfo, name), formatSize(size))
|
||||||
|
} else {
|
||||||
|
fmt.Printf("Get:%-3d %s\n", counter, colorText(colorInfo, name))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// fetch summary
|
||||||
|
|
||||||
|
func fetchSummary(totalBytes float64, elapsed time.Duration) {
|
||||||
|
outputMu.Lock()
|
||||||
|
defer outputMu.Unlock()
|
||||||
|
_clearProgressLine()
|
||||||
|
speed := float64(0)
|
||||||
|
if elapsed.Seconds() > 0 {
|
||||||
|
speed = totalBytes / elapsed.Seconds()
|
||||||
|
}
|
||||||
|
fmt.Printf("Fetched %s in %s", formatSize(totalBytes), formatDuration(elapsed))
|
||||||
|
if speed > 0 {
|
||||||
|
fmt.Printf(" (%s/s)", formatSpeed(speed))
|
||||||
|
}
|
||||||
|
fmt.Println()
|
||||||
|
}
|
||||||
|
|
||||||
|
func formatSpeed(bytesPerSec float64) string {
|
||||||
|
const KB = 1000
|
||||||
|
const MB = 1000 * KB
|
||||||
|
switch {
|
||||||
|
case bytesPerSec >= MB:
|
||||||
|
return fmt.Sprintf("%.2f MB/s", bytesPerSec/MB)
|
||||||
|
case bytesPerSec >= KB:
|
||||||
|
return fmt.Sprintf("%.2f kB/s", bytesPerSec/KB)
|
||||||
|
default:
|
||||||
|
return fmt.Sprintf("%.0f B/s", bytesPerSec)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// size formatting
|
||||||
|
|
||||||
|
func formatSize(bytes float64) string {
|
||||||
|
const (
|
||||||
|
KiB = 1000
|
||||||
|
MiB = 1000 * KiB
|
||||||
|
GiB = 1000 * MiB
|
||||||
|
)
|
||||||
|
switch {
|
||||||
|
case bytes >= GiB:
|
||||||
|
return fmt.Sprintf("%.2f GiB", bytes/GiB)
|
||||||
|
case bytes >= MiB:
|
||||||
|
return fmt.Sprintf("%.2f MiB", bytes/MiB)
|
||||||
|
case bytes >= KiB:
|
||||||
|
return fmt.Sprintf("%.2f KiB", bytes/KiB)
|
||||||
|
default:
|
||||||
|
return fmt.Sprintf("%.0f B", bytes)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func formatDuration(d time.Duration) string {
|
||||||
|
if d < time.Minute {
|
||||||
|
return fmt.Sprintf("%ds", int(d.Seconds()))
|
||||||
|
}
|
||||||
|
if d >= time.Hour {
|
||||||
|
h := int(d.Hours())
|
||||||
|
m := int(d.Minutes()) % 60
|
||||||
|
s := int(d.Seconds()) % 60
|
||||||
|
if h >= 100 {
|
||||||
|
return fmt.Sprintf("%dh%02dm", h, m)
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%dh%02dm%02ds", h, m, s)
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%dm%02ds", int(d.Minutes()), int(d.Seconds())%60)
|
||||||
|
}
|
||||||
|
|
||||||
|
// progress tracking
|
||||||
|
|
||||||
|
type progressState struct {
|
||||||
|
mu sync.Mutex
|
||||||
|
total int
|
||||||
|
completed int
|
||||||
|
startTime time.Time
|
||||||
|
drawn bool
|
||||||
|
spinnerIdx int
|
||||||
|
stop chan struct{}
|
||||||
|
}
|
||||||
|
|
||||||
|
var prog *progressState
|
||||||
|
|
||||||
|
var spinnerChars = [4]byte{'|', '/', '-', '\\'}
|
||||||
|
|
||||||
|
func newProgressState(total int) *progressState {
|
||||||
|
return &progressState{
|
||||||
|
total: total,
|
||||||
|
startTime: time.Now(),
|
||||||
|
stop: make(chan struct{}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *progressState) increment() {
|
||||||
|
p.mu.Lock()
|
||||||
|
defer p.mu.Unlock()
|
||||||
|
if p.completed < p.total {
|
||||||
|
p.completed++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *progressState) startTicker() {
|
||||||
|
go func() {
|
||||||
|
ticker := time.NewTicker(200 * time.Millisecond)
|
||||||
|
defer ticker.Stop()
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-p.stop:
|
||||||
|
return
|
||||||
|
case <-ticker.C:
|
||||||
|
outputMu.Lock()
|
||||||
|
_drawProgressLine()
|
||||||
|
outputMu.Unlock()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *progressState) stopTicker() {
|
||||||
|
close(p.stop)
|
||||||
|
}
|
||||||
|
|
||||||
|
func _clearProgressLine() {
|
||||||
|
if prog == nil || !prog.drawn || !isTerminal || cfg.noColor {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fmt.Print("\r\033[K")
|
||||||
|
prog.drawn = false
|
||||||
|
}
|
||||||
|
|
||||||
|
func _drawProgressLine() {
|
||||||
|
if prog == nil || !isTerminal || cfg.noColor || prog.total == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
prog.mu.Lock()
|
||||||
|
completed := prog.completed
|
||||||
|
total := prog.total
|
||||||
|
startTime := prog.startTime
|
||||||
|
prog.mu.Unlock()
|
||||||
|
|
||||||
|
// Refresh terminal width each frame so a live resize never wraps the line.
|
||||||
|
width := getTerminalWidth()
|
||||||
|
|
||||||
|
prog.spinnerIdx++
|
||||||
|
|
||||||
|
fmt.Print("\r\033[K" + buildProgressLine(width, completed, total, time.Since(startTime), prog.spinnerIdx))
|
||||||
|
prog.drawn = true
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildProgressLine(width, completed, total int, elapsed time.Duration, spinnerIdx int) string {
|
||||||
|
percent := 0
|
||||||
|
if total > 0 {
|
||||||
|
percent = int(float64(completed) / float64(total) * 100)
|
||||||
|
}
|
||||||
|
|
||||||
|
// overall bar: "Progress: [ NN%] [####.....]" gracefully stolen from APT
|
||||||
|
prefix := fmt.Sprintf("Progress: [%3d%%] [%c ", percent, spinnerChars[spinnerIdx%len(spinnerChars)])
|
||||||
|
|
||||||
|
suffix := fmt.Sprintf("] %d/%d", completed, total)
|
||||||
|
|
||||||
|
if completed > 0 && elapsed.Seconds() > 1 {
|
||||||
|
rate := float64(completed) / elapsed.Seconds()
|
||||||
|
remaining := float64(total-completed) / rate
|
||||||
|
suffix += fmt.Sprintf(" %s", formatDuration(time.Duration(remaining*float64(time.Second))))
|
||||||
|
}
|
||||||
|
|
||||||
|
barW := width - len(prefix) - len(suffix)
|
||||||
|
if barW < 1 {
|
||||||
|
line := prefix + suffix
|
||||||
|
if len(line) > width {
|
||||||
|
line = line[:width]
|
||||||
|
}
|
||||||
|
return line
|
||||||
|
}
|
||||||
|
|
||||||
|
filled := int(float64(barW) * float64(percent) / 100)
|
||||||
|
if filled > barW {
|
||||||
|
filled = barW
|
||||||
|
}
|
||||||
|
bar := strings.Repeat("#", filled) + strings.Repeat(".", barW-filled)
|
||||||
|
|
||||||
|
return prefix + colorText(colorInfo, bar) + suffix
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *progressState) finish() {
|
||||||
|
p.stopTicker()
|
||||||
|
outputMu.Lock()
|
||||||
|
defer outputMu.Unlock()
|
||||||
|
_clearProgressLine()
|
||||||
|
fmt.Println()
|
||||||
|
}
|
||||||
|
|
||||||
|
// transaction summary
|
||||||
|
|
||||||
|
type packageInfo struct {
|
||||||
|
Name string
|
||||||
|
Edition string
|
||||||
|
Arch string
|
||||||
|
Action string
|
||||||
|
Size float64
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p packageInfo) String() string {
|
||||||
|
return fmt.Sprintf("%s-%s.%s", p.Name, p.Edition, p.Arch)
|
||||||
|
}
|
||||||
|
|
||||||
|
func printTransactionSummary(installs, removes []packageInfo, downloadSize, diffBytes float64, numPkgs int) {
|
||||||
|
outputMu.Lock()
|
||||||
|
defer outputMu.Unlock()
|
||||||
|
_clearProgressLine()
|
||||||
|
|
||||||
|
fmt.Println()
|
||||||
|
|
||||||
|
// Count upgrades vs new installs
|
||||||
|
nUpgrades := 0
|
||||||
|
nInstalls := 0
|
||||||
|
for _, pkg := range installs {
|
||||||
|
if pkg.Edition != "" && pkg.Arch != "" {
|
||||||
|
nUpgrades++
|
||||||
|
} else {
|
||||||
|
nInstalls++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
nRemoves := len(removes)
|
||||||
|
|
||||||
|
// Packages to download (upgrades)
|
||||||
|
if nUpgrades > 0 {
|
||||||
|
names := make([]string, nUpgrades)
|
||||||
|
for i, pkg := range installs {
|
||||||
|
if i >= nUpgrades {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
names[i] = colorText(colorInfo, pkg.String())
|
||||||
|
}
|
||||||
|
fmt.Printf("The following packages will be upgraded:\n")
|
||||||
|
printWrappedNames(names, getTerminalWidth(), " ")
|
||||||
|
fmt.Println()
|
||||||
|
}
|
||||||
|
|
||||||
|
// New installs
|
||||||
|
if nInstalls > 0 {
|
||||||
|
names := make([]string, nInstalls)
|
||||||
|
for i := nUpgrades; i < len(installs) && i-nUpgrades < nInstalls; i++ {
|
||||||
|
names[i-nUpgrades] = colorText(colorInfo, installs[i].String())
|
||||||
|
}
|
||||||
|
fmt.Printf("The following NEW packages will be installed:\n")
|
||||||
|
printWrappedNames(names, getTerminalWidth(), " ")
|
||||||
|
fmt.Println()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Removed packages
|
||||||
|
if nRemoves > 0 {
|
||||||
|
names := make([]string, nRemoves)
|
||||||
|
for i, pkg := range removes {
|
||||||
|
names[i] = colorText(colorError, pkg.String())
|
||||||
|
}
|
||||||
|
fmt.Printf("The following packages will be REMOVED:\n")
|
||||||
|
printWrappedNames(names, getTerminalWidth(), " ")
|
||||||
|
fmt.Println()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Summary line
|
||||||
|
parts := []string{}
|
||||||
|
if nUpgrades > 0 {
|
||||||
|
parts = append(parts, fmt.Sprintf("%d upgraded", nUpgrades))
|
||||||
|
}
|
||||||
|
if nInstalls > 0 {
|
||||||
|
parts = append(parts, fmt.Sprintf("%d newly installed", nInstalls))
|
||||||
|
}
|
||||||
|
if nRemoves > 0 {
|
||||||
|
parts = append(parts, fmt.Sprintf("%d to remove", nRemoves))
|
||||||
|
}
|
||||||
|
parts = append(parts, fmt.Sprintf("%d not upgraded", 0))
|
||||||
|
fmt.Printf("%s.\n", strings.Join(parts, ", "))
|
||||||
|
|
||||||
|
// Download and space info
|
||||||
|
if downloadSize > 0 {
|
||||||
|
fmt.Printf("Need to get %s of archives.\n", formatSize(downloadSize))
|
||||||
|
}
|
||||||
|
|
||||||
|
spacePrefix := ""
|
||||||
|
if diffBytes >= 0 {
|
||||||
|
spacePrefix = "+"
|
||||||
|
}
|
||||||
|
fmt.Printf("After this operation, %s%s of additional disk space will be used.\n",
|
||||||
|
spacePrefix, formatSize(diffBytes))
|
||||||
|
}
|
||||||
|
|
||||||
|
// printWrappedNames prints package names wrapped to fit within termWidth.
|
||||||
|
func printWrappedNames(names []string, termWidth int, indent string) {
|
||||||
|
lineLen := len(indent)
|
||||||
|
first := true
|
||||||
|
for _, name := range names {
|
||||||
|
nameLen := len(name)
|
||||||
|
if !first && lineLen+1+nameLen > termWidth-2 {
|
||||||
|
fmt.Println()
|
||||||
|
lineLen = len(indent)
|
||||||
|
first = true
|
||||||
|
}
|
||||||
|
if !first {
|
||||||
|
fmt.Print(" ")
|
||||||
|
lineLen++
|
||||||
|
}
|
||||||
|
fmt.Printf("%s%s", indent, name)
|
||||||
|
if first {
|
||||||
|
lineLen += nameLen
|
||||||
|
first = false
|
||||||
|
} else {
|
||||||
|
lineLen += nameLen
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fmt.Println()
|
||||||
|
}
|
||||||
|
|
||||||
|
func min(a, b int) int {
|
||||||
|
if a < b {
|
||||||
|
return a
|
||||||
|
}
|
||||||
|
return b
|
||||||
|
}
|
||||||
856
zypper.go
Normal file
856
zypper.go
Normal file
|
|
@ -0,0 +1,856 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"encoding/xml"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"syscall"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// zypper host execution (no shell, no bash)
|
||||||
|
|
||||||
|
func zypperHostExec(args []string) (string, int) {
|
||||||
|
cmd := exec.Command("zypper", args...)
|
||||||
|
cmd.Env = []string{
|
||||||
|
"PATH=/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin",
|
||||||
|
"HOME=/root",
|
||||||
|
"LANG=C.UTF-8",
|
||||||
|
}
|
||||||
|
output, err := cmd.Output()
|
||||||
|
out := strings.TrimSpace(string(output))
|
||||||
|
if err != nil {
|
||||||
|
if exitErr, ok := err.(*exec.ExitError); ok {
|
||||||
|
return out, exitErr.ExitCode()
|
||||||
|
}
|
||||||
|
return out, -1
|
||||||
|
}
|
||||||
|
return out, 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func zypperDryRun(extraArgs []string) (string, int) {
|
||||||
|
args := []string{"--non-interactive", "--no-cd", "--xmlout"}
|
||||||
|
args = append(args, extraArgs...)
|
||||||
|
cmd := exec.Command("zypper", args...)
|
||||||
|
output, err := cmd.Output()
|
||||||
|
out := strings.TrimSpace(string(output))
|
||||||
|
if err != nil {
|
||||||
|
if exitErr, ok := err.(*exec.ExitError); ok {
|
||||||
|
return out, exitErr.ExitCode()
|
||||||
|
}
|
||||||
|
return out, -1
|
||||||
|
}
|
||||||
|
return out, 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// zypper chroot execution
|
||||||
|
|
||||||
|
func runZypper(ctx context.Context, rootfs string, args ...string) (stdout, stderr string, exitCode int, err error) {
|
||||||
|
info, err := os.Stat(rootfs)
|
||||||
|
if err != nil {
|
||||||
|
return "", "", -1, fmt.Errorf("rootfs does not exist: %w", err)
|
||||||
|
}
|
||||||
|
if !info.IsDir() {
|
||||||
|
return "", "", -1, fmt.Errorf("rootfs is a not a directory")
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd := exec.CommandContext(ctx, "/usr/bin/zypper", args...)
|
||||||
|
cmd.SysProcAttr = &syscall.SysProcAttr{Chroot: rootfs}
|
||||||
|
cmd.Dir = "/"
|
||||||
|
cmd.Env = []string{
|
||||||
|
"PATH=/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin",
|
||||||
|
"ZYPP_CURL2=1",
|
||||||
|
"ZYPP_PCK_PRELOAD=1",
|
||||||
|
"ZYPP_SINGLE_RPMTRANS=1",
|
||||||
|
"HOME=/root",
|
||||||
|
"LANG=C.UTF-8",
|
||||||
|
}
|
||||||
|
cmd.Stdin = nil
|
||||||
|
|
||||||
|
var outBuf, errBuf bytes.Buffer
|
||||||
|
cmd.Stdout = &outBuf
|
||||||
|
cmd.Stderr = &errBuf
|
||||||
|
|
||||||
|
err = cmd.Run()
|
||||||
|
if err != nil {
|
||||||
|
if exitErr, ok := err.(*exec.ExitError); ok {
|
||||||
|
exitCode = exitErr.ExitCode()
|
||||||
|
} else {
|
||||||
|
exitCode = -1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return outBuf.String(), errBuf.String(), exitCode, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// handover (terminal passthrough)
|
||||||
|
|
||||||
|
func handover(command string) int {
|
||||||
|
cmd := exec.Command("bash", "-c", command)
|
||||||
|
cmd.Stdin = os.Stdin
|
||||||
|
cmd.Stdout = os.Stdout
|
||||||
|
cmd.Stderr = os.Stderr
|
||||||
|
if err := cmd.Run(); err != nil {
|
||||||
|
if exitErr, ok := err.(*exec.ExitError); ok {
|
||||||
|
return exitErr.ExitCode()
|
||||||
|
}
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// xml parsing helpers
|
||||||
|
|
||||||
|
func findElements(xmlData, name string) []map[string]string {
|
||||||
|
decoder := xml.NewDecoder(strings.NewReader(xmlData))
|
||||||
|
var results []map[string]string
|
||||||
|
for {
|
||||||
|
tok, err := decoder.Token()
|
||||||
|
if err == io.EOF {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if se, ok := tok.(xml.StartElement); ok && se.Name.Local == name {
|
||||||
|
attrs := make(map[string]string)
|
||||||
|
for _, attr := range se.Attr {
|
||||||
|
attrs[attr.Name.Local] = attr.Value
|
||||||
|
}
|
||||||
|
results = append(results, attrs)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return results
|
||||||
|
}
|
||||||
|
|
||||||
|
func findMessages(xmlData string) []string {
|
||||||
|
decoder := xml.NewDecoder(strings.NewReader(xmlData))
|
||||||
|
var results []string
|
||||||
|
for {
|
||||||
|
tok, err := decoder.Token()
|
||||||
|
if err == io.EOF {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if se, ok := tok.(xml.StartElement); ok && se.Name.Local == "message" {
|
||||||
|
var text strings.Builder
|
||||||
|
for {
|
||||||
|
innerTok, err := decoder.Token()
|
||||||
|
if err == io.EOF || err != nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if ee, ok := innerTok.(xml.EndElement); ok && ee.Name.Local == "message" {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if cd, ok := innerTok.(xml.CharData); ok {
|
||||||
|
text.Write(cd)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
results = append(results, text.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return results
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseInstallSummary(xmlData string) (downloadSize, diffBytes float64, numPkgs int, found bool) {
|
||||||
|
for _, s := range findElements(xmlData, "install-summary") {
|
||||||
|
debugf("install-summary attrs: download-size=%q space-usage-diff=%q packages-to-change=%q",
|
||||||
|
s["download-size"], s["space-usage-diff"], s["packages-to-change"])
|
||||||
|
fmt.Sscanf(s["download-size"], "%f", &downloadSize)
|
||||||
|
fmt.Sscanf(s["space-usage-diff"], "%f", &diffBytes)
|
||||||
|
fmt.Sscanf(s["packages-to-change"], "%d", &numPkgs)
|
||||||
|
found = true
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if downloadSize == 0 {
|
||||||
|
downloadSize = findChildTextFloat(xmlData, "install-summary", "download-size")
|
||||||
|
}
|
||||||
|
if diffBytes == 0 {
|
||||||
|
diffBytes = findChildTextFloat(xmlData, "install-summary", "space-usage-diff")
|
||||||
|
}
|
||||||
|
if numPkgs == 0 {
|
||||||
|
numPkgs = int(findChildTextFloat(xmlData, "install-summary", "packages-to-change"))
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func findChildTextFloat(xmlData, parent, child string) float64 {
|
||||||
|
decoder := xml.NewDecoder(strings.NewReader(xmlData))
|
||||||
|
inParent := 0
|
||||||
|
for {
|
||||||
|
tok, err := decoder.Token()
|
||||||
|
if err != nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
switch t := tok.(type) {
|
||||||
|
case xml.StartElement:
|
||||||
|
if t.Name.Local == parent {
|
||||||
|
inParent++
|
||||||
|
} else if inParent > 0 && t.Name.Local == child {
|
||||||
|
var text strings.Builder
|
||||||
|
for {
|
||||||
|
innerTok, err := decoder.Token()
|
||||||
|
if err != nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if ee, ok := innerTok.(xml.EndElement); ok && ee.Name.Local == child {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if cd, ok := innerTok.(xml.CharData); ok {
|
||||||
|
text.Write(cd)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var val float64
|
||||||
|
fmt.Sscanf(strings.TrimSpace(text.String()), "%f", &val)
|
||||||
|
return val
|
||||||
|
}
|
||||||
|
case xml.EndElement:
|
||||||
|
if t.Name.Local == parent && inParent > 0 {
|
||||||
|
inParent--
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseAllSolvables(xmlData string) []packageInfo {
|
||||||
|
var pkgs []packageInfo
|
||||||
|
for _, s := range findElements(xmlData, "solvable") {
|
||||||
|
if s["type"] == "package" {
|
||||||
|
var size float64
|
||||||
|
fmt.Sscanf(s["size"], "%f", &size)
|
||||||
|
pkgs = append(pkgs, packageInfo{
|
||||||
|
Name: s["name"],
|
||||||
|
Edition: s["edition"],
|
||||||
|
Arch: s["arch"],
|
||||||
|
Action: "install",
|
||||||
|
Size: size,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return pkgs
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseToUpdate(xmlData string) []packageInfo {
|
||||||
|
var pkgs []packageInfo
|
||||||
|
for _, u := range findElements(xmlData, "update") {
|
||||||
|
if u["kind"] == "package" {
|
||||||
|
var size float64
|
||||||
|
fmt.Sscanf(u["size"], "%f", &size)
|
||||||
|
pkgs = append(pkgs, packageInfo{
|
||||||
|
Name: u["name"],
|
||||||
|
Edition: u["edition"],
|
||||||
|
Arch: u["arch"],
|
||||||
|
Action: "upgrade",
|
||||||
|
Size: size,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return pkgs
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseToRemove(xmlData string) []packageInfo {
|
||||||
|
decoder := xml.NewDecoder(strings.NewReader(xmlData))
|
||||||
|
var results []packageInfo
|
||||||
|
inToRemove := 0
|
||||||
|
for {
|
||||||
|
tok, err := decoder.Token()
|
||||||
|
if err == io.EOF {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
switch t := tok.(type) {
|
||||||
|
case xml.StartElement:
|
||||||
|
if t.Name.Local == "to-remove" {
|
||||||
|
inToRemove++
|
||||||
|
} else if inToRemove > 0 && t.Name.Local == "solvable" {
|
||||||
|
attrs := make(map[string]string)
|
||||||
|
for _, attr := range t.Attr {
|
||||||
|
attrs[attr.Name.Local] = attr.Value
|
||||||
|
}
|
||||||
|
if attrs["type"] == "package" {
|
||||||
|
results = append(results, packageInfo{
|
||||||
|
Name: attrs["name"],
|
||||||
|
Edition: attrs["edition"],
|
||||||
|
Arch: attrs["arch"],
|
||||||
|
Action: "remove",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case xml.EndElement:
|
||||||
|
if t.Name.Local == "to-remove" {
|
||||||
|
inToRemove--
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return results
|
||||||
|
}
|
||||||
|
|
||||||
|
func filterPackages(allPkgs, rmPkgs []packageInfo) []packageInfo {
|
||||||
|
rmSet := make(map[string]bool, len(rmPkgs))
|
||||||
|
for _, p := range rmPkgs {
|
||||||
|
rmSet[p.String()] = true
|
||||||
|
}
|
||||||
|
var result []packageInfo
|
||||||
|
for _, p := range allPkgs {
|
||||||
|
if !rmSet[p.String()] {
|
||||||
|
result = append(result, p)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sort.Slice(result, func(i, j int) bool {
|
||||||
|
return result[i].String() < result[j].String()
|
||||||
|
})
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// shared handler logic
|
||||||
|
|
||||||
|
type transactionInfo struct {
|
||||||
|
installs []packageInfo
|
||||||
|
removes []packageInfo
|
||||||
|
downloadSize float64
|
||||||
|
diffBytes float64
|
||||||
|
numPkgs int
|
||||||
|
nothingToDo bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func dryRunAndParse(extraArgs []string) (*transactionInfo, error) {
|
||||||
|
xmlOutput, ret := zypperDryRun(extraArgs)
|
||||||
|
debugf("%s", xmlOutput)
|
||||||
|
|
||||||
|
if strings.Contains(xmlOutput, "Nothing to do") {
|
||||||
|
return &transactionInfo{nothingToDo: true}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
downloadSize, diffBytes, numPkgs, found := parseInstallSummary(xmlOutput)
|
||||||
|
|
||||||
|
if ret != 0 {
|
||||||
|
if !found || numPkgs == 0 {
|
||||||
|
msgs := findMessages(xmlOutput)
|
||||||
|
if len(msgs) > 0 {
|
||||||
|
return nil, fmt.Errorf("zypper error:\n%s", strings.Join(msgs, "\n"))
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("zypper dry-run failed with exit code %d", ret)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !found || numPkgs == 0 {
|
||||||
|
return &transactionInfo{nothingToDo: true}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
installs := parseAllSolvables(xmlOutput)
|
||||||
|
removes := parseToRemove(xmlOutput)
|
||||||
|
filtered := filterPackages(installs, removes)
|
||||||
|
|
||||||
|
return &transactionInfo{
|
||||||
|
installs: filtered,
|
||||||
|
removes: removes,
|
||||||
|
downloadSize: downloadSize,
|
||||||
|
diffBytes: diffBytes,
|
||||||
|
numPkgs: numPkgs,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func showTransactionAndConfirm(info *transactionInfo) bool {
|
||||||
|
if info.nothingToDo {
|
||||||
|
infof("%s", colorText(colorInfo, "Nothing to do. Exiting..."))
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
printTransactionSummary(info.installs, info.removes, info.downloadSize, info.diffBytes, info.numPkgs)
|
||||||
|
|
||||||
|
fmt.Println()
|
||||||
|
if cfg.noConfirm {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return queryYesNo("Do you want to continue?", "yes")
|
||||||
|
}
|
||||||
|
|
||||||
|
func pkgNames(pkgs []packageInfo) []string {
|
||||||
|
names := make([]string, len(pkgs))
|
||||||
|
for i, p := range pkgs {
|
||||||
|
names[i] = p.String()
|
||||||
|
}
|
||||||
|
return names
|
||||||
|
}
|
||||||
|
|
||||||
|
// worker pool
|
||||||
|
|
||||||
|
type job struct {
|
||||||
|
item string
|
||||||
|
itemCounter int
|
||||||
|
totalItems int
|
||||||
|
workerIdx int
|
||||||
|
size float64
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeLogMessages(item string, counter, total int) map[string]string {
|
||||||
|
m := make(map[string]string)
|
||||||
|
forceStr := ""
|
||||||
|
if cfg.force {
|
||||||
|
forceStr = "force "
|
||||||
|
}
|
||||||
|
switch cfg.command {
|
||||||
|
case "refresh", "ref":
|
||||||
|
m["error"] = fmt.Sprintf("Error %srefreshing repo [%d/%d] %q", forceStr, counter, total, item)
|
||||||
|
m["exception"] = fmt.Sprintf("SIGINT while %srefreshing repo [%d/%d] %q", forceStr, counter, total, item)
|
||||||
|
default:
|
||||||
|
m["error"] = fmt.Sprintf("Error downloading package [%d/%d] %q", counter, total, item)
|
||||||
|
m["exception"] = fmt.Sprintf("SIGINT while downloading package [%d/%d] %q", counter, total, item)
|
||||||
|
}
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
|
||||||
|
func refreshArgs() []string {
|
||||||
|
if cfg.force {
|
||||||
|
return []string{"refresh", "--force"}
|
||||||
|
}
|
||||||
|
return []string{"refresh"}
|
||||||
|
}
|
||||||
|
|
||||||
|
func worker(ctx context.Context, wg *sync.WaitGroup, uuidPool chan string, tmpDir string, jobsChan chan job) {
|
||||||
|
defer wg.Done()
|
||||||
|
for j := range jobsChan {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
|
||||||
|
uuid := <-uuidPool
|
||||||
|
msgs := makeLogMessages(j.item, j.itemCounter, j.totalItems)
|
||||||
|
|
||||||
|
if prog != nil {
|
||||||
|
prog.increment()
|
||||||
|
}
|
||||||
|
downloadLine(j.itemCounter, j.item, j.size)
|
||||||
|
|
||||||
|
rootfs := filepath.Join(tmpDir, uuid, "rootfs")
|
||||||
|
var zypperArgs []string
|
||||||
|
if cfg.command == "refresh" || cfg.command == "ref" {
|
||||||
|
zypperArgs = []string{"--non-interactive"}
|
||||||
|
zypperArgs = append(zypperArgs, refreshArgs()...)
|
||||||
|
zypperArgs = append(zypperArgs, j.item)
|
||||||
|
} else {
|
||||||
|
zypperArgs = []string{"--non-interactive", "download", j.item}
|
||||||
|
}
|
||||||
|
|
||||||
|
var stdout, stderr string
|
||||||
|
var exitCode int
|
||||||
|
var err error
|
||||||
|
|
||||||
|
for attempt := 1; attempt <= maxRetries; attempt++ {
|
||||||
|
stdout, stderr, exitCode, err = runZypper(ctx, rootfs, zypperArgs...)
|
||||||
|
|
||||||
|
if ctx.Err() != nil {
|
||||||
|
debugf("%s", msgs["exception"])
|
||||||
|
uuidPool <- uuid
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err == nil && exitCode == 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
if attempt < maxRetries {
|
||||||
|
warningf("Attempt %d failed for %q. Retrying...", attempt, j.item)
|
||||||
|
time.Sleep(1 * time.Second)
|
||||||
|
} else {
|
||||||
|
errorf("%s", colorText(colorError, fmt.Sprintf("%s. zypper exit code: %d", msgs["error"], exitCode)))
|
||||||
|
if stderr != "" {
|
||||||
|
debugf("[stderr]\n%s", stderr)
|
||||||
|
}
|
||||||
|
if stdout != "" {
|
||||||
|
debugf("[stdout]\n%s", stdout)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
uuidPool <- uuid
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func mainTask(ctx context.Context, taskItems []string, tmpDir string, needDev bool, tracker *MountTracker) error {
|
||||||
|
numWorkers := cfg.jobs
|
||||||
|
if len(taskItems) < numWorkers {
|
||||||
|
numWorkers = len(taskItems)
|
||||||
|
}
|
||||||
|
|
||||||
|
uuidPool := make(chan string, numWorkers)
|
||||||
|
allUUIDs := make([]string, numWorkers)
|
||||||
|
|
||||||
|
for i := 0; i < numWorkers; i++ {
|
||||||
|
u := randomUUID()
|
||||||
|
allUUIDs[i] = u
|
||||||
|
uuidPool <- u
|
||||||
|
}
|
||||||
|
|
||||||
|
if needDev {
|
||||||
|
for _, uuid := range allUUIDs {
|
||||||
|
if err := setupChroot(tmpDir, uuid, needDev, tracker); err != nil {
|
||||||
|
errorf("%s", colorText(colorError, fmt.Sprintf("Chroot setup failed: %v", err)))
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
var setupWg sync.WaitGroup
|
||||||
|
setupErrs := make(chan error, numWorkers)
|
||||||
|
for _, uuid := range allUUIDs {
|
||||||
|
setupWg.Add(1)
|
||||||
|
go func(u string) {
|
||||||
|
defer setupWg.Done()
|
||||||
|
if err := setupChroot(tmpDir, u, needDev, tracker); err != nil {
|
||||||
|
setupErrs <- err
|
||||||
|
}
|
||||||
|
}(uuid)
|
||||||
|
}
|
||||||
|
setupWg.Wait()
|
||||||
|
close(setupErrs)
|
||||||
|
if err := <-setupErrs; err != nil {
|
||||||
|
errorf("%s", colorText(colorError, fmt.Sprintf("Chroot setup failed: %v", err)))
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
totalItems := len(taskItems)
|
||||||
|
|
||||||
|
totalBytes := float64(0)
|
||||||
|
if cfg.txInfo != nil {
|
||||||
|
for _, p := range cfg.txInfo {
|
||||||
|
totalBytes += p.Size
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pkgSizes := make(map[string]float64)
|
||||||
|
if cfg.txInfo != nil {
|
||||||
|
for _, p := range cfg.txInfo {
|
||||||
|
pkgSizes[p.String()] = p.Size
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
prog = newProgressState(totalItems)
|
||||||
|
prog.startTicker()
|
||||||
|
defer func() {
|
||||||
|
prog.finish()
|
||||||
|
prog = nil
|
||||||
|
}()
|
||||||
|
|
||||||
|
jobsChan := make(chan job, totalItems)
|
||||||
|
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
for i := 0; i < numWorkers; i++ {
|
||||||
|
wg.Add(1)
|
||||||
|
go worker(ctx, &wg, uuidPool, tmpDir, jobsChan)
|
||||||
|
}
|
||||||
|
|
||||||
|
itemCounter := 0
|
||||||
|
for _, item := range taskItems {
|
||||||
|
itemCounter++
|
||||||
|
size := pkgSizes[item]
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
close(jobsChan)
|
||||||
|
wg.Wait()
|
||||||
|
return ctx.Err()
|
||||||
|
case jobsChan <- job{item: item, itemCounter: itemCounter, totalItems: totalItems, workerIdx: (itemCounter - 1) % numWorkers, size: size}:
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
close(jobsChan)
|
||||||
|
wg.Wait()
|
||||||
|
|
||||||
|
if totalBytes > 0 {
|
||||||
|
fetchSummary(totalBytes, time.Since(prog.startTime))
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// prompt
|
||||||
|
|
||||||
|
func queryYesNo(question, defaultVal string) bool {
|
||||||
|
valid := map[string]bool{"yes": true, "y": true, "ye": true, "no": false, "n": false}
|
||||||
|
var prompt string
|
||||||
|
switch defaultVal {
|
||||||
|
case "":
|
||||||
|
prompt = " [y/n]: "
|
||||||
|
case "yes":
|
||||||
|
prompt = " [Y/n]: "
|
||||||
|
case "no":
|
||||||
|
prompt = " [y/N]: "
|
||||||
|
default:
|
||||||
|
prompt = " [y/n]: "
|
||||||
|
}
|
||||||
|
reader := bufio.NewReader(os.Stdin)
|
||||||
|
for {
|
||||||
|
fmt.Print(colorText(colorInput, question+prompt))
|
||||||
|
choice, err := reader.ReadString('\n')
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println()
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
choice = strings.ToLower(strings.TrimSpace(choice))
|
||||||
|
if defaultVal != "" && choice == "" {
|
||||||
|
return valid[defaultVal]
|
||||||
|
}
|
||||||
|
if v, ok := valid[choice]; ok {
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
fmt.Print(colorText(colorWarning, "Please respond with 'yes' or 'no' (or 'y' or 'n').\n"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// zypp lock
|
||||||
|
|
||||||
|
func getZyppLock() int {
|
||||||
|
ourPid := os.Getpid()
|
||||||
|
os.MkdirAll(filepath.Dir(zypperPidFile), 0755)
|
||||||
|
os.WriteFile(zypperPidFile, []byte(fmt.Sprintf("%d", ourPid)), 0644)
|
||||||
|
return ourPid
|
||||||
|
}
|
||||||
|
|
||||||
|
func releaseZyppLock() bool {
|
||||||
|
if os.Getuid() == 0 {
|
||||||
|
if err := os.WriteFile(zypperPidFile, []byte{}, 0644); err != nil {
|
||||||
|
debugf("Warning: failed to clear zypp lock file: %v", err)
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkZypperRunning() (int, string) {
|
||||||
|
data, err := os.ReadFile(zypperPidFile)
|
||||||
|
if err != nil {
|
||||||
|
return 0, ""
|
||||||
|
}
|
||||||
|
pidStr := strings.TrimSpace(string(data))
|
||||||
|
if pidStr == "" {
|
||||||
|
return 0, ""
|
||||||
|
}
|
||||||
|
var pid int
|
||||||
|
fmt.Sscanf(pidStr, "%d", &pid)
|
||||||
|
if pid == 0 {
|
||||||
|
return 0, ""
|
||||||
|
}
|
||||||
|
|
||||||
|
commPath := fmt.Sprintf("/proc/%d/comm", pid)
|
||||||
|
commData, err := os.ReadFile(commPath)
|
||||||
|
if err != nil {
|
||||||
|
return 0, ""
|
||||||
|
}
|
||||||
|
program := strings.TrimSpace(string(commData))
|
||||||
|
if program == "" {
|
||||||
|
return 0, ""
|
||||||
|
}
|
||||||
|
return pid, program
|
||||||
|
}
|
||||||
|
|
||||||
|
// command handlers
|
||||||
|
|
||||||
|
func handleRefresh(ctx context.Context, tmpDir string) error {
|
||||||
|
tracker := &MountTracker{}
|
||||||
|
defer vlcroCleanup(tmpDir, tracker)
|
||||||
|
|
||||||
|
statusMsg("Reading package lists")
|
||||||
|
xmlOutput, _ := zypperDryRun([]string{"repos"})
|
||||||
|
statusMsg("Enumerating enabled repos")
|
||||||
|
|
||||||
|
getZyppLock()
|
||||||
|
|
||||||
|
repos := findElements(xmlOutput, "repo")
|
||||||
|
var aliases []string
|
||||||
|
for _, r := range repos {
|
||||||
|
if r["enabled"] == "1" {
|
||||||
|
aliases = append(aliases, r["alias"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
debugf("Enabled repos: %v", aliases)
|
||||||
|
|
||||||
|
if len(aliases) == 0 {
|
||||||
|
infof("%s", colorText(colorInfo, "No repos found. Exiting..."))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return mainTask(ctx, aliases, tmpDir, true, tracker)
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleDistUpgrade(ctx context.Context, tmpDir string) error {
|
||||||
|
tracker := &MountTracker{}
|
||||||
|
defer vlcroCleanup(tmpDir, tracker)
|
||||||
|
|
||||||
|
statusMsg("Reading package lists")
|
||||||
|
statusMsg("Building dependency tree")
|
||||||
|
info, err := dryRunAndParse([]string{"dist-upgrade", "--dry-run"})
|
||||||
|
if err != nil {
|
||||||
|
errorf("%s", colorText(colorError, fmt.Sprintf("%v\nThere are package conflicts that must be manually resolved. See output of:\nzypper --non-interactive --no-cd dist-upgrade --dry-run", err)))
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if info.nothingToDo {
|
||||||
|
infof("%s", colorText(colorInfo, "Nothing to do. Exiting..."))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg.txInfo = info.installs
|
||||||
|
|
||||||
|
if !showTransactionAndConfirm(info) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
getZyppLock()
|
||||||
|
|
||||||
|
if info.downloadSize > 0 {
|
||||||
|
statusMsg("Downloading packages")
|
||||||
|
err := mainTask(ctx, pkgNames(info.installs), tmpDir, false, tracker)
|
||||||
|
if err == context.Canceled {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
warningf("%s", colorText(colorWarning, fmt.Sprintf("Parallel execution failed (%v). Falling back to standard zypper...", err)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
vlcroCleanup(tmpDir, tracker)
|
||||||
|
|
||||||
|
if !cfg.downloadOnly {
|
||||||
|
infof("%s", colorText(colorInfo, "Vlcro has finished its tasks. Handing you over to zypper..."))
|
||||||
|
nonInteractive := ""
|
||||||
|
if cfg.noConfirm {
|
||||||
|
nonInteractive = "--non-interactive"
|
||||||
|
}
|
||||||
|
code := handover(fmt.Sprintf("env ZYPP_CURL2=1 ZYPP_PCK_PRELOAD=1 ZYPP_SINGLE_RPMTRANS=1 zypper %s --no-cd dist-upgrade", nonInteractive))
|
||||||
|
if code != 0 {
|
||||||
|
return fmt.Errorf("zypper dist-upgrade failed with exit code %d", code)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleInstall(ctx context.Context, tmpDir string) error {
|
||||||
|
if len(cfg.packages) == 0 {
|
||||||
|
errorf("%s", colorText(colorError, "No packages specified for installation"))
|
||||||
|
return fmt.Errorf("no packages specified")
|
||||||
|
}
|
||||||
|
|
||||||
|
tracker := &MountTracker{}
|
||||||
|
defer vlcroCleanup(tmpDir, tracker)
|
||||||
|
|
||||||
|
statusMsg("Reading package lists")
|
||||||
|
statusMsg("Building dependency tree")
|
||||||
|
dryRunArgs := []string{"install", "--dry-run"}
|
||||||
|
dryRunArgs = append(dryRunArgs, cfg.packages...)
|
||||||
|
info, err := dryRunAndParse(dryRunArgs)
|
||||||
|
if err != nil {
|
||||||
|
errorf("%s", colorText(colorError, fmt.Sprintf("Error: %v", err)))
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if info.nothingToDo {
|
||||||
|
infof("%s", colorText(colorInfo, "Nothing to do. Exiting..."))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg.txInfo = info.installs
|
||||||
|
|
||||||
|
if !showTransactionAndConfirm(info) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
getZyppLock()
|
||||||
|
|
||||||
|
if info.downloadSize > 0 {
|
||||||
|
statusMsg("Downloading packages")
|
||||||
|
err := mainTask(ctx, pkgNames(info.installs), tmpDir, false, tracker)
|
||||||
|
if err == context.Canceled {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
warningf("%s", colorText(colorWarning, fmt.Sprintf("Parallel execution failed (%v). Falling back to standard zypper...", err)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
vlcroCleanup(tmpDir, tracker)
|
||||||
|
|
||||||
|
if !cfg.downloadOnly {
|
||||||
|
infof("%s", colorText(colorInfo, "Vlcro has finished its tasks. Handing you over to zypper..."))
|
||||||
|
nonInteractive := ""
|
||||||
|
if cfg.noConfirm {
|
||||||
|
nonInteractive = "--non-interactive"
|
||||||
|
}
|
||||||
|
code := handover(fmt.Sprintf("env ZYPP_CURL2=1 ZYPP_PCK_PRELOAD=1 ZYPP_SINGLE_RPMTRANS=1 zypper %s --no-cd install %s", nonInteractive, strings.Join(cfg.packages, " ")))
|
||||||
|
if code != 0 {
|
||||||
|
return fmt.Errorf("zypper install failed with exit code %d", code)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleINR(ctx context.Context, tmpDir string) error {
|
||||||
|
tracker := &MountTracker{}
|
||||||
|
defer vlcroCleanup(tmpDir, tracker)
|
||||||
|
|
||||||
|
statusMsg("Reading package lists")
|
||||||
|
statusMsg("Building dependency tree")
|
||||||
|
info, err := dryRunAndParse([]string{"install-new-recommends", "--dry-run"})
|
||||||
|
if err != nil {
|
||||||
|
errorf("%s", colorText(colorError, fmt.Sprintf("Error: %v", err)))
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if info.nothingToDo {
|
||||||
|
infof("%s", colorText(colorInfo, "Nothing to do. Exiting..."))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg.txInfo = info.installs
|
||||||
|
|
||||||
|
if !showTransactionAndConfirm(info) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
getZyppLock()
|
||||||
|
|
||||||
|
if info.downloadSize > 0 {
|
||||||
|
statusMsg("Downloading packages")
|
||||||
|
err := mainTask(ctx, pkgNames(info.installs), tmpDir, false, tracker)
|
||||||
|
if err == context.Canceled {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
warningf("%s", colorText(colorWarning, fmt.Sprintf("Parallel execution failed (%v). Falling back to standard zypper...", err)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
vlcroCleanup(tmpDir, tracker)
|
||||||
|
|
||||||
|
if !cfg.downloadOnly {
|
||||||
|
infof("%s", colorText(colorInfo, "Vlcro has finished its tasks. Handing you over to zypper..."))
|
||||||
|
nonInteractive := ""
|
||||||
|
if cfg.noConfirm {
|
||||||
|
nonInteractive = "--non-interactive"
|
||||||
|
}
|
||||||
|
code := handover(fmt.Sprintf("env ZYPP_CURL2=1 ZYPP_PCK_PRELOAD=1 ZYPP_SINGLE_RPMTRANS=1 zypper %s --no-cd install-new-recommends", nonInteractive))
|
||||||
|
if code != 0 {
|
||||||
|
return fmt.Errorf("zypper install-new-recommends failed with exit code %d", code)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue