mirror of
https://github.com/MAKS-IT-COM/uscheduler.git
synced 2026-09-30 04:58:09 +02:00
Compare commits
No commits in common. "main" and "v1.1.0" have entirely different histories.
21
CHANGELOG.md
21
CHANGELOG.md
@ -7,27 +7,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
## [1.2.0] - 2026-09-21
|
|
||||||
|
|
||||||
### Added
|
|
||||||
|
|
||||||
- Windows setup exe offers **Standard** or **Portable** install. Portable keeps binaries, scripts, logs, and settings in the selected folder (`Scripts`, `Logs`, and `Data` subfolders). `--prepare-data --portable` does the same from the CLI; the portable zip includes a `portable` marker so unpacked copies stay self-contained.
|
|
||||||
|
|
||||||
### Changed
|
|
||||||
|
|
||||||
- Relicensed the project from MIT to **Apache 2.0**.
|
|
||||||
|
|
||||||
### Fixed
|
|
||||||
|
|
||||||
- Linux Flatpak GNOME app icon uses X11/XWayland (`UsePlatformDetect` only). Avalonia 12.1.2 native Wayland still hangs on GNOME `xdg_toplevel.configure(0, 0)`. App id is lowercase `com.maks_it.uscheduler`. Uninstall the old `com.maks_it.UScheduler` id before installing a new bundle. AppStream lives in `data/`.
|
|
||||||
- Flatpak AppStream version is the same shared release version as zip and MSI (`DotNetReleaseVersion` from the UI csproj / `Directory.Build.props`), so `flatpak info` matches the bundle file name.
|
|
||||||
|
|
||||||
## [1.1.1] - 2026-09-06
|
|
||||||
|
|
||||||
### Fixed
|
|
||||||
|
|
||||||
- Windows setup exe now ships bundled Scripts next to the worker and runs `--prepare-data` after files are installed, so example scripts are copied to `C:\MaksIT\Scripts` when those folders do not already exist.
|
|
||||||
|
|
||||||
## [1.1.0] - 2026-09-06
|
## [1.1.0] - 2026-09-06
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|||||||
@ -173,8 +173,4 @@ Follow [Keep a Changelog](https://keepachangelog.com/) format:
|
|||||||
- Bug fixes
|
- Bug fixes
|
||||||
```
|
```
|
||||||
|
|
||||||
## License
|
|
||||||
|
|
||||||
By contributing, you agree that your contributions are licensed under the terms in [LICENSE.md](LICENSE.md) (Apache 2.0).
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
214
LICENSE.md
214
LICENSE.md
@ -1,201 +1,21 @@
|
|||||||
Apache License
|
MIT License
|
||||||
Version 2.0, January 2004
|
|
||||||
http://www.apache.org/licenses/
|
|
||||||
|
|
||||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
Copyright (c) 2017 - 2026 Maksym Sadovnychyy (Maks-IT)
|
||||||
|
|
||||||
1. Definitions.
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
"License" shall mean the terms and conditions for use, reproduction,
|
The above copyright notice and this permission notice shall be included in all
|
||||||
and distribution as defined by Sections 1 through 9 of this document.
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
"Licensor" shall mean the copyright owner or entity authorized by
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
the copyright owner that is granting the License.
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
"Legal Entity" shall mean the union of the acting entity and all
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
other entities that control, are controlled by, or are under common
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
control with that entity. For the purposes of this definition,
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
"control" means (i) the power, direct or indirect, to cause the
|
SOFTWARE.
|
||||||
direction or management of such entity, whether by contract or
|
|
||||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
|
||||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
|
||||||
|
|
||||||
"You" (or "Your") shall mean an individual or Legal Entity
|
|
||||||
exercising permissions granted by this License.
|
|
||||||
|
|
||||||
"Source" form shall mean the preferred form for making modifications,
|
|
||||||
including but not limited to software source code, documentation
|
|
||||||
source, and configuration files.
|
|
||||||
|
|
||||||
"Object" form shall mean any form resulting from mechanical
|
|
||||||
transformation or translation of a Source form, including but
|
|
||||||
not limited to compiled object code, generated documentation,
|
|
||||||
and conversions to other media types.
|
|
||||||
|
|
||||||
"Work" shall mean the work of authorship, whether in Source or
|
|
||||||
Object form, made available under the License, as indicated by a
|
|
||||||
copyright notice that is included in or attached to the work
|
|
||||||
(an example is provided in the Appendix below).
|
|
||||||
|
|
||||||
"Derivative Works" shall mean any work, whether in Source or Object
|
|
||||||
form, that is based on (or derived from) the Work and for which the
|
|
||||||
editorial revisions, annotations, elaborations, or other modifications
|
|
||||||
represent, as a whole, an original work of authorship. For the purposes
|
|
||||||
of this License, Derivative Works shall not include works that remain
|
|
||||||
separable from, or merely link (or bind by name) to the interfaces of,
|
|
||||||
the Work and Derivative Works thereof.
|
|
||||||
|
|
||||||
"Contribution" shall mean any work of authorship, including
|
|
||||||
the original version of the Work and any modifications or additions
|
|
||||||
to that Work or Derivative Works thereof, that is intentionally
|
|
||||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
|
||||||
or by an individual or Legal Entity authorized to submit on behalf of
|
|
||||||
the copyright owner. For the purposes of this definition, "submitted"
|
|
||||||
means any form of electronic, verbal, or written communication sent
|
|
||||||
to the Licensor or its representatives, including but not limited to
|
|
||||||
communication on electronic mailing lists, source code control systems,
|
|
||||||
and issue tracking systems that are managed by, or on behalf of, the
|
|
||||||
Licensor for the purpose of discussing and improving the Work, but
|
|
||||||
excluding communication that is conspicuously marked or otherwise
|
|
||||||
designated in writing by the copyright owner as "Not a Contribution."
|
|
||||||
|
|
||||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
|
||||||
on behalf of whom a Contribution has been received by Licensor and
|
|
||||||
subsequently incorporated within the Work.
|
|
||||||
|
|
||||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
|
||||||
this License, each Contributor hereby grants to You a perpetual,
|
|
||||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
||||||
copyright license to reproduce, prepare Derivative Works of,
|
|
||||||
publicly display, publicly perform, sublicense, and distribute the
|
|
||||||
Work and such Derivative Works in Source or Object form.
|
|
||||||
|
|
||||||
3. Grant of Patent License. Subject to the terms and conditions of
|
|
||||||
this License, each Contributor hereby grants to You a perpetual,
|
|
||||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
||||||
(except as stated in this section) patent license to make, have made,
|
|
||||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
|
||||||
where such license applies only to those patent claims licensable
|
|
||||||
by such Contributor that are necessarily infringed by their
|
|
||||||
Contribution(s) alone or by combination of their Contribution(s)
|
|
||||||
with the Work to which such Contribution(s) was submitted. If You
|
|
||||||
institute patent litigation against any entity (including a
|
|
||||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
|
||||||
or a Contribution incorporated within the Work constitutes direct
|
|
||||||
or contributory patent infringement, then any patent licenses
|
|
||||||
granted to You under this License for that Work shall terminate
|
|
||||||
as of the date such litigation is filed.
|
|
||||||
|
|
||||||
4. Redistribution. You may reproduce and distribute copies of the
|
|
||||||
Work or Derivative Works thereof in any medium, with or without
|
|
||||||
modifications, and in Source or Object form, provided that You
|
|
||||||
meet the following conditions:
|
|
||||||
|
|
||||||
(a) You must give any other recipients of the Work or
|
|
||||||
Derivative Works a copy of this License; and
|
|
||||||
|
|
||||||
(b) You must cause any modified files to carry prominent notices
|
|
||||||
stating that You changed the files; and
|
|
||||||
|
|
||||||
(c) You must retain, in the Source form of any Derivative Works
|
|
||||||
that You distribute, all copyright, patent, trademark, and
|
|
||||||
attribution notices from the Source form of the Work,
|
|
||||||
excluding those notices that do not pertain to any part of
|
|
||||||
the Derivative Works; and
|
|
||||||
|
|
||||||
(d) If the Work includes a "NOTICE" text file as part of its
|
|
||||||
distribution, then any Derivative Works that You distribute must
|
|
||||||
include a readable copy of the attribution notices contained
|
|
||||||
within such NOTICE file, excluding those notices that do not
|
|
||||||
pertain to any part of the Derivative Works, in at least one
|
|
||||||
of the following places: within a NOTICE text file distributed
|
|
||||||
as part of the Derivative Works; within the Source form or
|
|
||||||
documentation, if provided along with the Derivative Works; or,
|
|
||||||
within a display generated by the Derivative Works, if and
|
|
||||||
wherever such third-party notices normally appear. The contents
|
|
||||||
of the NOTICE file are for informational purposes only and
|
|
||||||
do not modify the License. You may add Your own attribution
|
|
||||||
notices within Derivative Works that You distribute, alongside
|
|
||||||
or as an addendum to the NOTICE text from the Work, provided
|
|
||||||
that such additional attribution notices cannot be construed
|
|
||||||
as modifying the License.
|
|
||||||
|
|
||||||
You may add Your own copyright statement to Your modifications and
|
|
||||||
may provide additional or different license terms and conditions
|
|
||||||
for use, reproduction, or distribution of Your modifications, or
|
|
||||||
for any such Derivative Works as a whole, provided Your use,
|
|
||||||
reproduction, and distribution of the Work otherwise complies with
|
|
||||||
the conditions stated in this License.
|
|
||||||
|
|
||||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
|
||||||
any Contribution intentionally submitted for inclusion in the Work
|
|
||||||
by You to the Licensor shall be under the terms and conditions of
|
|
||||||
this License, without any additional terms or conditions.
|
|
||||||
Notwithstanding the above, nothing herein shall supersede or modify
|
|
||||||
the terms of any separate license agreement you may have executed
|
|
||||||
with Licensor regarding such Contributions.
|
|
||||||
|
|
||||||
6. Trademarks. This License does not grant permission to use the trade
|
|
||||||
names, trademarks, service marks, or product names of the Licensor,
|
|
||||||
except as required for reasonable and customary use in describing the
|
|
||||||
origin of the Work and reproducing the content of the NOTICE file.
|
|
||||||
|
|
||||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
|
||||||
agreed to in writing, Licensor provides the Work (and each
|
|
||||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
|
||||||
implied, including, without limitation, any warranties or conditions
|
|
||||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
|
||||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
|
||||||
appropriateness of using or redistributing the Work and assume any
|
|
||||||
risks associated with Your exercise of permissions under this License.
|
|
||||||
|
|
||||||
8. Limitation of Liability. In no event and under no legal theory,
|
|
||||||
whether in tort (including negligence), contract, or otherwise,
|
|
||||||
unless required by applicable law (such as deliberate and grossly
|
|
||||||
negligent acts) or agreed to in writing, shall any Contributor be
|
|
||||||
liable to You for damages, including any direct, indirect, special,
|
|
||||||
incidental, or consequential damages of any character arising as a
|
|
||||||
result of this License or out of the use or inability to use the
|
|
||||||
Work (including but not limited to damages for loss of goodwill,
|
|
||||||
work stoppage, computer failure or malfunction, or any and all
|
|
||||||
other commercial damages or losses), even if such Contributor
|
|
||||||
has been advised of the possibility of such damages.
|
|
||||||
|
|
||||||
9. Accepting Warranty or Additional Liability. While redistributing
|
|
||||||
the Work or Derivative Works thereof, You may choose to offer,
|
|
||||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
|
||||||
or other liability obligations and/or rights consistent with this
|
|
||||||
License. However, in accepting such obligations, You may act only
|
|
||||||
on Your own behalf and on Your sole responsibility, not on behalf
|
|
||||||
of any other Contributor, and only if You agree to indemnify,
|
|
||||||
defend, and hold each Contributor harmless for any liability
|
|
||||||
incurred by, or claims asserted against, such Contributor by reason
|
|
||||||
of your accepting any such warranty or additional liability.
|
|
||||||
|
|
||||||
END OF TERMS AND CONDITIONS
|
|
||||||
|
|
||||||
APPENDIX: How to apply the Apache License to your work.
|
|
||||||
|
|
||||||
To apply the Apache License to your work, attach the following
|
|
||||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
|
||||||
replaced with your own identifying information. (Don't include
|
|
||||||
the brackets!) The text should be enclosed in the appropriate
|
|
||||||
comment syntax for the file format. We also recommend that a
|
|
||||||
file or class name and description of purpose be included on the
|
|
||||||
same "printed page" as the copyright notice for easier
|
|
||||||
identification within third-party archives.
|
|
||||||
|
|
||||||
Copyright 2017-2026 Maksym Sadovnychyy (MAKS-IT)
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
you may not use this file except in compliance with the License.
|
|
||||||
You may obtain a copy of the License at
|
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
See the License for the specific language governing permissions and
|
|
||||||
limitations under the License.
|
|
||||||
|
|||||||
70
README.md
70
README.md
@ -4,7 +4,7 @@
|
|||||||

|

|
||||||

|

|
||||||

|

|
||||||

|

|
||||||

|

|
||||||
|
|
||||||
A modern scheduler built on **.NET 10** for running PowerShell scripts and console applications on **Windows and Linux**.
|
A modern scheduler built on **.NET 10** for running PowerShell scripts and console applications on **Windows and Linux**.
|
||||||
@ -57,7 +57,7 @@ See [CONTRIBUTING.md](CONTRIBUTING.md) for development setup, commit format, and
|
|||||||
|
|
||||||
## Scripts Examples
|
## Scripts Examples
|
||||||
|
|
||||||
> **Note:** These examples are **bundled with the release** and copied to `C:\MaksIT\Scripts` (standard) or the install folder's `Scripts` directory (portable) by the Windows setup exe (and by `--install` / `--prepare-data`) **only if that folder or script is not already present**. Existing files and script folders are never overwritten or merged. They are listed in the default configuration but **disabled by default**. To enable an example, set `"Disabled": false` in the shared `settings.json` (or use the UI).
|
> **Note:** These examples are **bundled with the release** and copied to `C:\MaksIT\Scripts` on first install **only if that folder or script is not already present**. Existing files and script folders are never overwritten or merged. They are listed in the default configuration but **disabled by default**. To enable an example, set `"Disabled": false` in `%ProgramData%\MaksIT\UScheduler\settings.json` (or use the UI).
|
||||||
|
|
||||||
- [Hyper-V Backup](./src/Scripts/HyperV-Backup/README.md) - Production-ready Hyper-V VM backup solution with scheduling and retention management
|
- [Hyper-V Backup](./src/Scripts/HyperV-Backup/README.md) - Production-ready Hyper-V VM backup solution with scheduling and retention management
|
||||||
- [Native-Sync](./src/Scripts/Native-Sync/README.md) - Production-ready file synchronization solution using pure PowerShell with no external dependencies
|
- [Native-Sync](./src/Scripts/Native-Sync/README.md) - Production-ready file synchronization solution using pure PowerShell with no external dependencies
|
||||||
@ -88,31 +88,15 @@ See [CONTRIBUTING.md](CONTRIBUTING.md) for development setup, commit format, and
|
|||||||
|
|
||||||
### Install layout
|
### Install layout
|
||||||
|
|
||||||
The Windows setup exe lets you choose **Standard** or **Portable**.
|
|
||||||
|
|
||||||
**Standard** (default):
|
|
||||||
|
|
||||||
| Location | Purpose | Who can write |
|
| Location | Purpose | Who can write |
|
||||||
|----------|---------|----------------|
|
|----------|---------|----------------|
|
||||||
| `C:\Program Files\MaksIT\UScheduler` | Worker (`MaksIT.UScheduler.exe`) and UI (`MaksIT.UScheduler.UI.exe`) | Administrators |
|
| `C:\Program Files\MaksIT\UScheduler` | Worker (`MaksIT.UScheduler.exe`) and UI (`MaksIT.UScheduler.UI.exe`) | Administrators |
|
||||||
| `C:\MaksIT\Scripts` | Scheduled scripts (all users). The Windows setup exe (and `--install` / `--prepare-data`) copies bundled examples only into missing folders; existing scripts are never overwritten. | Users (after install) |
|
| `C:\MaksIT\Scripts` | Scheduled scripts (all users). Install copies bundled examples only into missing folders; existing scripts are never overwritten. | Users (after install) |
|
||||||
| `C:\MaksIT\Logs` | Service and script logs | Users (after install) |
|
| `C:\MaksIT\Logs` | Service and script logs | Users (after install) |
|
||||||
| `%ProgramData%\MaksIT\UScheduler\settings.json` | Shared schedule configuration | Users (after install) |
|
| `%ProgramData%\MaksIT\UScheduler\settings.json` | Shared schedule configuration | Users (after install) |
|
||||||
| `%AppData%\MaksIT\UScheduler\settings.json` | Per-user UI prefs (service bin path override) | Current user |
|
| `%AppData%\MaksIT\UScheduler\settings.json` | Per-user UI prefs (service bin path override) | Current user |
|
||||||
|
|
||||||
**Portable** (everything in one folder):
|
Registering the service (or `MaksIT.UScheduler --prepare-data`) creates the `C:\MaksIT` and ProgramData folders and grants the Users group modify rights, so the UI can stay unelevated.
|
||||||
|
|
||||||
| Location | Purpose |
|
|
||||||
|----------|---------|
|
|
||||||
| *Install folder* (you choose it) | Worker and UI |
|
|
||||||
| *Install folder*`\Scripts` | Scheduled scripts |
|
|
||||||
| *Install folder*`\Logs` | Service and script logs |
|
|
||||||
| *Install folder*`\Data\settings.json` | Shared schedule configuration |
|
|
||||||
| *Install folder*`\Data\ui-settings.json` | UI prefs |
|
|
||||||
|
|
||||||
A `portable` file in that folder (or a parent folder) marks the layout. Pick a writable location such as `C:\MaksIT\UScheduler` if you do not want Program Files. The Users group is granted modify rights on `Scripts`, `Logs`, and `Data` so the UI can stay unelevated.
|
|
||||||
|
|
||||||
The Windows setup exe, registering the service, or `MaksIT.UScheduler --prepare-data` creates the data folders, copies bundled example scripts when missing, and grants the Users group modify rights so the UI can stay unelevated. Add `--portable` to keep data next to the binaries instead of `C:\MaksIT` and ProgramData.
|
|
||||||
|
|
||||||
### Using CLI Commands
|
### Using CLI Commands
|
||||||
|
|
||||||
@ -139,9 +123,6 @@ MaksIT.UScheduler --help
|
|||||||
|
|
||||||
# Create data folders and ACLs without installing the service
|
# Create data folders and ACLs without installing the service
|
||||||
MaksIT.UScheduler --prepare-data
|
MaksIT.UScheduler --prepare-data
|
||||||
|
|
||||||
# Portable layout: scripts, logs, and settings next to the executable
|
|
||||||
MaksIT.UScheduler --prepare-data --portable
|
|
||||||
```
|
```
|
||||||
|
|
||||||
On Windows the file is `MaksIT.UScheduler.exe`.
|
On Windows the file is `MaksIT.UScheduler.exe`.
|
||||||
@ -153,8 +134,7 @@ On Windows the file is `MaksIT.UScheduler.exe`.
|
|||||||
| `--start` | | Start the service |
|
| `--start` | | Start the service |
|
||||||
| `--stop` | | Stop the service |
|
| `--stop` | | Stop the service |
|
||||||
| `--status` | | Query service status |
|
| `--status` | | Query service status |
|
||||||
| `--prepare-data` | | Create scripts, logs, and shared settings (elevated) |
|
| `--prepare-data` | | Create `C:\MaksIT\Scripts`, `C:\MaksIT\Logs`, and shared settings (elevated) |
|
||||||
| `--portable` | | With `--install` / `--prepare-data`: keep data in the install folder |
|
|
||||||
| `--help` | `-h` | Show help message |
|
| `--help` | `-h` | Show help message |
|
||||||
|
|
||||||
> **Note:** Service management commands require administrator / root privileges.
|
> **Note:** Service management commands require administrator / root privileges.
|
||||||
@ -192,35 +172,11 @@ The UI is an **Avalonia** desktop app (Windows and Linux) for service registrati
|
|||||||
|
|
||||||
### Getting Started
|
### Getting Started
|
||||||
|
|
||||||
When you unpack the portable zip, launch `MaksIT.UScheduler.UI.exe` (or `Start-UScheduler.bat`). The zip is a portable layout: scripts, logs, and settings stay in the extracted folder. GitHub releases also ship a Windows setup exe (worker + UI; choose Standard or Portable on the install page) and a Flatpak of the UI.
|
When you unpack the portable zip, launch `MaksIT.UScheduler.UI.exe`. GitHub releases also ship a Windows setup exe (installs worker + UI to `C:\Program Files\MaksIT\UScheduler`) and a Flatpak of the UI.
|
||||||
|
|
||||||
#### Linux (Flatpak)
|
|
||||||
|
|
||||||
**User** (this account only):
|
|
||||||
|
|
||||||
```bash
|
|
||||||
flatpak install --user ./maksit-uscheduler-{version}.flatpak
|
|
||||||
flatpak run com.maks_it.uscheduler
|
|
||||||
```
|
|
||||||
|
|
||||||
**System** (all users):
|
|
||||||
|
|
||||||
```bash
|
|
||||||
sudo flatpak install --system ./maksit-uscheduler-{version}.flatpak
|
|
||||||
flatpak run com.maks_it.uscheduler
|
|
||||||
```
|
|
||||||
|
|
||||||
Uninstall: `flatpak uninstall --user com.maks_it.uscheduler` or `sudo flatpak uninstall --system com.maks_it.uscheduler`.
|
|
||||||
|
|
||||||
The previous id `com.maks_it.UScheduler` is replaced by this lowercase id. Uninstall the old app before installing the new bundle if it was installed.
|
|
||||||
|
|
||||||
If GNOME or KDE does not show a launcher icon, `flatpak run` may warn that `/var/lib/flatpak/exports/share` and `~/.local/share/flatpak/exports/share` are not on `XDG_DATA_DIRS`. Log out and back in once so the session picks up those paths.
|
|
||||||
|
|
||||||
Linux uses X11/XWayland (Avalonia native Wayland still hangs on GNOME). The sandbox grants `--filesystem=home` for scripts and logs. The Flatpak is the UI; the worker is a separate Linux systemd install. AppStream and the desktop file live in [`data/`](data/).
|
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
> **Note:** Service management (register, start, stop, unregister) prompts for administrator approval without restarting the UI. Schedule edits go to `%ProgramData%\MaksIT\UScheduler\settings.json` on a standard install (or `Data\settings.json` next to a portable install) and do not require elevation after the first install.
|
> **Note:** Service management (register, start, stop, unregister) prompts for administrator approval without restarting the UI. Schedule edits go to `%ProgramData%\MaksIT\UScheduler\settings.json` and do not require elevation after the first install.
|
||||||
|
|
||||||
### Settings View
|
### Settings View
|
||||||
|
|
||||||
@ -230,12 +186,12 @@ The Settings view is your starting point for configuring UScheduler.
|
|||||||
|
|
||||||
| Feature | Description |
|
| Feature | Description |
|
||||||
|---------|-------------|
|
|---------|-------------|
|
||||||
| **Service Bin Path** | Path to the worker folder (auto-detected from Program Files, the UI directory, or a portable folder; override stored in `%AppData%/MaksIT/UScheduler/settings.json` or `Data\ui-settings.json` when portable) |
|
| **Service Bin Path** | Path to the worker folder (auto-detected from Program Files or the UI directory; override stored in `%AppData%/MaksIT/UScheduler/settings.json`) |
|
||||||
| **Service Status** | Real-time status indicator (Running, Stopped, Starting, Stopping, Paused, Not Installed) |
|
| **Service Status** | Real-time status indicator (Running, Stopped, Starting, Stopping, Paused, Not Installed) |
|
||||||
| **Register/Unregister** | Install or remove the Windows service or systemd unit (UAC / polkit prompt, UI stays open) |
|
| **Register/Unregister** | Install or remove the Windows service or systemd unit (UAC / polkit prompt, UI stays open) |
|
||||||
| **Start/Stop** | Control the service state (same in-app elevation) |
|
| **Start/Stop** | Control the service state (same in-app elevation) |
|
||||||
| **Refresh** | Update the current service status display |
|
| **Refresh** | Update the current service status display |
|
||||||
| **Reload Settings** | Refresh shared configuration from `%ProgramData%\MaksIT\UScheduler\settings.json` (or `Data\settings.json` when portable) |
|
| **Reload Settings** | Refresh shared configuration from `%ProgramData%\MaksIT\UScheduler\settings.json` |
|
||||||
|
|
||||||
### Main View — Schedule Management
|
### Main View — Schedule Management
|
||||||
|
|
||||||
@ -306,9 +262,9 @@ Features:
|
|||||||
|
|
||||||
### Machine-wide `settings.json`
|
### Machine-wide `settings.json`
|
||||||
|
|
||||||
Host logging (log levels, Event Log source) stays in shipped `appsettings.json` next to `MaksIT.UScheduler.exe`. Schedule configuration is **not** written there — a leftover `Configuration` block is copied once into the machine-wide file:
|
Host logging (log levels, Event Log source) stays in shipped `appsettings.json` next to `MaksIT.UScheduler.exe` under Program Files. Schedule configuration is **not** written there — a leftover `Configuration` block is copied once into the machine-wide file:
|
||||||
|
|
||||||
`%ProgramData%\MaksIT\UScheduler\settings.json` (standard) or `{install folder}\Data\settings.json` (portable)
|
`%ProgramData%\MaksIT\UScheduler\settings.json`
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
@ -482,7 +438,7 @@ Invoke-ScheduledExecution -Config $Config -Automated:$Automated -CurrentDateTime
|
|||||||
1. Copy template
|
1. Copy template
|
||||||
2. Modify `$Config`
|
2. Modify `$Config`
|
||||||
3. Implement `Start-BusinessLogic`
|
3. Implement `Start-BusinessLogic`
|
||||||
4. Add script to the shared `settings.json` (ProgramData on a standard install, or `Data\settings.json` when portable — or use the UI)
|
4. Add script to `%ProgramData%\MaksIT\UScheduler\settings.json` (or use the UI)
|
||||||
|
|
||||||
That’s it — the full scheduling engine is reused automatically.
|
That’s it — the full scheduling engine is reused automatically.
|
||||||
|
|
||||||
@ -559,6 +515,6 @@ Email: maksym.sadovnychyy@gmail.com
|
|||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
Apache 2.0 — see [LICENSE.md](LICENSE.md).
|
This project is licensed under the MIT License. See [LICENSE.md](LICENSE.md) for details.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
@ -1,12 +0,0 @@
|
|||||||
[Desktop Entry]
|
|
||||||
Type=Application
|
|
||||||
Name=UScheduler
|
|
||||||
GenericName=Job scheduler
|
|
||||||
Comment=Schedule PowerShell scripts and processes
|
|
||||||
Exec=maksit-uscheduler
|
|
||||||
Icon=com.maks_it.uscheduler
|
|
||||||
StartupWMClass=com.maks_it.uscheduler
|
|
||||||
StartupNotify=true
|
|
||||||
Terminal=false
|
|
||||||
Categories=Utility;System;
|
|
||||||
Keywords=scheduler;powershell;cron;jobs;
|
|
||||||
@ -1,49 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<component type="desktop-application">
|
|
||||||
<id>com.maks_it.uscheduler</id>
|
|
||||||
<name>UScheduler</name>
|
|
||||||
<summary>Schedule PowerShell scripts and processes</summary>
|
|
||||||
<developer id="com.maks_it">
|
|
||||||
<name>MaksIT</name>
|
|
||||||
</developer>
|
|
||||||
<metadata_license>CC0-1.0</metadata_license>
|
|
||||||
<project_license>Apache-2.0</project_license>
|
|
||||||
<description>
|
|
||||||
<p>Schedule and run PowerShell scripts and console processes from an Avalonia desktop UI. Register the worker, edit schedules, and follow logs without a browser.</p>
|
|
||||||
<p>The Flatpak is the UI. Script files and logs live under the home folder. The worker is a separate install on Linux (systemd) or Windows (service).</p>
|
|
||||||
<ul>
|
|
||||||
<li>Script list, schedules, and log viewing</li>
|
|
||||||
<li>Register, start, and stop the worker (prompts for elevation)</li>
|
|
||||||
<li>Windows and Linux script compatibility flags</li>
|
|
||||||
</ul>
|
|
||||||
</description>
|
|
||||||
<launchable type="desktop-id">com.maks_it.uscheduler.desktop</launchable>
|
|
||||||
<provides>
|
|
||||||
<id>com.maks_it.UScheduler</id>
|
|
||||||
</provides>
|
|
||||||
<replaces>
|
|
||||||
<id>com.maks_it.UScheduler</id>
|
|
||||||
</replaces>
|
|
||||||
<url type="homepage">https://maks-it.com/</url>
|
|
||||||
<url type="bugtracker">https://github.com/MAKS-IT-COM/uscheduler/issues</url>
|
|
||||||
<url type="vcs-browser">https://github.com/MAKS-IT-COM/uscheduler</url>
|
|
||||||
<url type="donation">https://www.buymeacoffee.com/maksitcom</url>
|
|
||||||
<branding>
|
|
||||||
<color type="primary" scheme_preference="light">#33A5CF</color>
|
|
||||||
<color type="primary" scheme_preference="dark">#003D7A</color>
|
|
||||||
</branding>
|
|
||||||
<content_rating type="oars-1.1"/>
|
|
||||||
<requires>
|
|
||||||
<control>keyboard</control>
|
|
||||||
<control>pointing</control>
|
|
||||||
<display_length compare="ge">768</display_length>
|
|
||||||
</requires>
|
|
||||||
<releases>
|
|
||||||
<release version="1.2.0" date="2026-09-21">
|
|
||||||
<url type="details">https://github.com/MAKS-IT-COM/uscheduler/blob/v1.2.0/CHANGELOG.md</url>
|
|
||||||
<description>
|
|
||||||
<p>Linux Flatpak uses X11/XWayland from the GNOME app icon and a lowercase app id.</p>
|
|
||||||
</description>
|
|
||||||
</release>
|
|
||||||
</releases>
|
|
||||||
</component>
|
|
||||||
@ -62,8 +62,7 @@ public class ProcessConfiguration : TaskConfiguration {
|
|||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Root configuration class for the UScheduler service.
|
/// Root configuration class for the UScheduler service.
|
||||||
/// Stored in machine-wide settings.json (ProgramData), or under the install
|
/// Stored in machine-wide settings.json (ProgramData), not next to the exe.
|
||||||
/// folder when a portable layout is used.
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class Configuration {
|
public class Configuration {
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -104,17 +103,12 @@ public class Configuration {
|
|||||||
Processes ??= [];
|
Processes ??= [];
|
||||||
}
|
}
|
||||||
|
|
||||||
public string GetEffectiveScriptsDirectory(string? baseDirectory = null) =>
|
public string GetEffectiveScriptsDirectory() =>
|
||||||
HostPaths.ResolveScriptsDirectory(ScriptsDir, baseDirectory);
|
HostPaths.ResolveScriptsDirectory(ScriptsDir);
|
||||||
|
|
||||||
public string GetEffectiveLogDirectory(string? baseDirectory = null) {
|
public string GetEffectiveLogDirectory(string? baseDirectory = null) {
|
||||||
var start = baseDirectory ?? AppContext.BaseDirectory;
|
var logDir = string.IsNullOrWhiteSpace(LogDir) ? HostPaths.DefaultLogDirectory : LogDir;
|
||||||
if (string.IsNullOrWhiteSpace(LogDir)) {
|
return PathHelper.ResolvePath(logDir, baseDirectory ?? AppContext.BaseDirectory);
|
||||||
var root = HostPaths.FindPortableRoot(start);
|
|
||||||
return root is null ? HostPaths.DefaultLogDirectory : HostPaths.GetPortableLogDirectory(root);
|
|
||||||
}
|
|
||||||
|
|
||||||
return PathHelper.ResolvePath(LogDir, start);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public string ResolveScriptPath(string path) =>
|
public string ResolveScriptPath(string path) =>
|
||||||
|
|||||||
@ -6,10 +6,8 @@ namespace MaksIT.UScheduler.Shared;
|
|||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Loads and saves machine-wide <c>Configuration</c> under ProgramData
|
/// Loads and saves machine-wide <c>Configuration</c> under ProgramData
|
||||||
/// (all-users AppData), or under the portable Data folder when a
|
/// (all-users AppData). Shipped <c>appsettings.json</c> next to the worker is
|
||||||
/// <see cref="HostPaths.PortableMarkerFileName"/> file is present.
|
/// seed-only; host logging stays in that file and is never copied.
|
||||||
/// Shipped <c>appsettings.json</c> next to the worker is seed-only; host
|
|
||||||
/// logging stays in that file and is never copied.
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class ConfigurationFileService {
|
public sealed class ConfigurationFileService {
|
||||||
public const string ProductFolder = HostPaths.ProductFolder;
|
public const string ProductFolder = HostPaths.ProductFolder;
|
||||||
@ -29,7 +27,7 @@ public sealed class ConfigurationFileService {
|
|||||||
|
|
||||||
public ConfigurationFileService(string? configurationPath = null, string? seedPath = null) {
|
public ConfigurationFileService(string? configurationPath = null, string? seedPath = null) {
|
||||||
FilePath = string.IsNullOrWhiteSpace(configurationPath)
|
FilePath = string.IsNullOrWhiteSpace(configurationPath)
|
||||||
? HostPaths.ResolveSharedSettingsFile()
|
? HostPaths.SharedSettingsFile
|
||||||
: configurationPath;
|
: configurationPath;
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(seedPath))
|
if (!string.IsNullOrWhiteSpace(seedPath))
|
||||||
@ -95,7 +93,6 @@ public sealed class ConfigurationFileService {
|
|||||||
return;
|
return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
HostPaths.ApplyPortableDefaults(_current, Path.GetDirectoryName(FilePath));
|
|
||||||
Save(_current);
|
Save(_current);
|
||||||
}
|
}
|
||||||
catch (UnauthorizedAccessException) {
|
catch (UnauthorizedAccessException) {
|
||||||
|
|||||||
@ -4,115 +4,53 @@ using System.Runtime.Versioning;
|
|||||||
|
|
||||||
namespace MaksIT.UScheduler.Shared;
|
namespace MaksIT.UScheduler.Shared;
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Scripts, logs, and shared-settings locations for one install.
|
|
||||||
/// </summary>
|
|
||||||
public readonly record struct HostDataLayout(
|
|
||||||
string Root,
|
|
||||||
string ScriptsDirectory,
|
|
||||||
string LogDirectory,
|
|
||||||
string SharedSettingsFile,
|
|
||||||
bool Portable);
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Creates machine-wide data folders (scripts, logs, shared settings) and
|
/// Creates machine-wide data folders (scripts, logs, shared settings) and
|
||||||
/// grants the Users group modify rights so the UI can run without elevation.
|
/// grants the Users group modify rights so the UI can run without elevation.
|
||||||
/// Call from an elevated <c>--install</c> / <c>--prepare-data</c> process.
|
/// Call from an elevated <c>--install</c> / <c>--prepare-data</c> process.
|
||||||
/// Pass <c>--portable</c> (or drop a <see cref="HostPaths.PortableMarkerFileName"/>
|
|
||||||
/// file) to keep scripts, logs, and settings under the install folder.
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static class HostDataDirectories {
|
public static class HostDataDirectories {
|
||||||
/// <summary>Windows Built-in Users (locale-independent).</summary>
|
/// <summary>Windows Built-in Users (locale-independent).</summary>
|
||||||
private const string BuiltinUsersSid = "*S-1-5-32-545";
|
private const string BuiltinUsersSid = "*S-1-5-32-545";
|
||||||
|
|
||||||
public static HostDataLayout ResolveLayout(string? installDirectory = null, bool portableRequested = false) {
|
public static HostServiceOperationResult Prepare(string? installDirectory = null) {
|
||||||
var installDir = string.IsNullOrWhiteSpace(installDirectory)
|
|
||||||
? AppContext.BaseDirectory
|
|
||||||
: Path.GetFullPath(installDirectory);
|
|
||||||
|
|
||||||
var portableRoot = HostPaths.FindPortableRoot(installDir);
|
|
||||||
var portable = portableRequested || portableRoot is not null;
|
|
||||||
if (!portable) {
|
|
||||||
return new HostDataLayout(
|
|
||||||
HostPaths.DefaultDataRoot,
|
|
||||||
HostPaths.DefaultScriptsDirectory,
|
|
||||||
HostPaths.DefaultLogDirectory,
|
|
||||||
HostPaths.SharedSettingsFile,
|
|
||||||
false);
|
|
||||||
}
|
|
||||||
|
|
||||||
var root = portableRoot ?? installDir;
|
|
||||||
return new HostDataLayout(
|
|
||||||
root,
|
|
||||||
HostPaths.GetPortableScriptsDirectory(root),
|
|
||||||
HostPaths.GetPortableLogDirectory(root),
|
|
||||||
HostPaths.GetPortableSharedSettingsFile(root),
|
|
||||||
true);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static HostServiceOperationResult Prepare(string? installDirectory = null, bool portable = false) {
|
|
||||||
var installDir = string.IsNullOrWhiteSpace(installDirectory)
|
var installDir = string.IsNullOrWhiteSpace(installDirectory)
|
||||||
? AppContext.BaseDirectory
|
? AppContext.BaseDirectory
|
||||||
: installDirectory;
|
: installDirectory;
|
||||||
var messages = new List<string>();
|
var messages = new List<string>();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (portable)
|
Directory.CreateDirectory(HostPaths.DefaultDataRoot);
|
||||||
HostPaths.WritePortableMarker(Path.GetFullPath(installDir));
|
Directory.CreateDirectory(HostPaths.DefaultScriptsDirectory);
|
||||||
|
Directory.CreateDirectory(HostPaths.DefaultLogDirectory);
|
||||||
|
|
||||||
var layout = ResolveLayout(installDir, portable);
|
var settingsDir = Path.GetDirectoryName(HostPaths.SharedSettingsFile);
|
||||||
Directory.CreateDirectory(layout.Root);
|
|
||||||
Directory.CreateDirectory(layout.ScriptsDirectory);
|
|
||||||
Directory.CreateDirectory(layout.LogDirectory);
|
|
||||||
|
|
||||||
var settingsDir = Path.GetDirectoryName(layout.SharedSettingsFile);
|
|
||||||
if (!string.IsNullOrEmpty(settingsDir))
|
if (!string.IsNullOrEmpty(settingsDir))
|
||||||
Directory.CreateDirectory(settingsDir);
|
Directory.CreateDirectory(settingsDir);
|
||||||
|
|
||||||
if (OperatingSystem.IsWindows()) {
|
if (OperatingSystem.IsWindows()) {
|
||||||
if (layout.Portable) {
|
GrantUsersModify(HostPaths.DefaultDataRoot, messages);
|
||||||
GrantUsersModify(layout.ScriptsDirectory, messages);
|
|
||||||
GrantUsersModify(layout.LogDirectory, messages);
|
|
||||||
if (!string.IsNullOrEmpty(settingsDir))
|
if (!string.IsNullOrEmpty(settingsDir))
|
||||||
GrantUsersModify(settingsDir, messages);
|
GrantUsersModify(settingsDir, messages);
|
||||||
}
|
}
|
||||||
else {
|
|
||||||
GrantUsersModify(layout.Root, messages);
|
|
||||||
if (!string.IsNullOrEmpty(settingsDir))
|
|
||||||
GrantUsersModify(settingsDir, messages);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (OperatingSystem.IsLinux() || OperatingSystem.IsMacOS()) {
|
else if (OperatingSystem.IsLinux() || OperatingSystem.IsMacOS()) {
|
||||||
GrantUnixGroupWrite(layout.Root, messages);
|
GrantUnixGroupWrite(HostPaths.DefaultDataRoot, messages);
|
||||||
GrantUnixGroupWrite(layout.ScriptsDirectory, messages);
|
GrantUnixGroupWrite(HostPaths.DefaultScriptsDirectory, messages);
|
||||||
GrantUnixGroupWrite(layout.LogDirectory, messages);
|
GrantUnixGroupWrite(HostPaths.DefaultLogDirectory, messages);
|
||||||
if (!string.IsNullOrEmpty(settingsDir))
|
if (!string.IsNullOrEmpty(settingsDir))
|
||||||
GrantUnixGroupWrite(settingsDir, messages);
|
GrantUnixGroupWrite(settingsDir, messages);
|
||||||
}
|
}
|
||||||
|
|
||||||
var source = HostPaths.FindBundledScriptsDirectory(installDir);
|
var copy = CopySeedScripts(installDir, HostPaths.DefaultScriptsDirectory);
|
||||||
if (source is null || !Directory.Exists(source)) {
|
|
||||||
messages.Add(
|
|
||||||
$"Bundled scripts were not found next to '{installDir}'; left {layout.ScriptsDirectory} unchanged.");
|
|
||||||
}
|
|
||||||
else if (PathsEqual(source, layout.ScriptsDirectory)) {
|
|
||||||
messages.Add($"Scripts directory: {layout.ScriptsDirectory}");
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
var copy = CopySeedScriptsIfMissing(source, layout.ScriptsDirectory);
|
|
||||||
if (copy.CopiedFiles > 0)
|
if (copy.CopiedFiles > 0)
|
||||||
messages.Add($"Copied {copy.CopiedFiles} new seed script file(s) to {layout.ScriptsDirectory}.");
|
messages.Add($"Copied {copy.CopiedFiles} new seed script file(s) to {HostPaths.DefaultScriptsDirectory}.");
|
||||||
if (copy.SkippedItems > 0)
|
if (copy.SkippedItems > 0)
|
||||||
messages.Add($"Left {copy.SkippedItems} existing script folder(s)/file(s) unchanged in {layout.ScriptsDirectory}.");
|
messages.Add($"Left {copy.SkippedItems} existing script folder(s)/file(s) unchanged in {HostPaths.DefaultScriptsDirectory}.");
|
||||||
if (copy.CopiedFiles == 0 && copy.SkippedItems == 0)
|
if (copy.CopiedFiles == 0 && copy.SkippedItems == 0)
|
||||||
messages.Add($"Scripts directory: {layout.ScriptsDirectory}");
|
messages.Add($"Scripts directory: {HostPaths.DefaultScriptsDirectory}");
|
||||||
}
|
|
||||||
|
|
||||||
WritePortableSettingsIfNeeded(layout, installDir);
|
messages.Add($"Logs directory: {HostPaths.DefaultLogDirectory}");
|
||||||
messages.Add($"Logs directory: {layout.LogDirectory}");
|
messages.Add($"Shared settings: {HostPaths.SharedSettingsFile}");
|
||||||
messages.Add($"Shared settings: {layout.SharedSettingsFile}");
|
|
||||||
if (layout.Portable)
|
|
||||||
messages.Add("Install type: portable (everything under the install folder).");
|
|
||||||
return new HostServiceOperationResult(true, string.Join(Environment.NewLine, messages));
|
return new HostServiceOperationResult(true, string.Join(Environment.NewLine, messages));
|
||||||
}
|
}
|
||||||
catch (Exception ex) {
|
catch (Exception ex) {
|
||||||
@ -120,6 +58,14 @@ public static class HostDataDirectories {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static SeedCopyResult CopySeedScripts(string installDirectory, string destination) {
|
||||||
|
var source = HostPaths.FindBundledScriptsDirectory(installDirectory);
|
||||||
|
if (source is null || !Directory.Exists(source) || PathsEqual(source, destination))
|
||||||
|
return default;
|
||||||
|
|
||||||
|
return CopySeedScriptsIfMissing(source, destination);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Copies bundled example scripts only when the destination does not already
|
/// Copies bundled example scripts only when the destination does not already
|
||||||
/// have that file or script folder. Existing folders are left as-is (no merge),
|
/// have that file or script folder. Existing folders are left as-is (no merge),
|
||||||
@ -154,16 +100,6 @@ public static class HostDataDirectories {
|
|||||||
return new SeedCopyResult(copied, skipped);
|
return new SeedCopyResult(copied, skipped);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void WritePortableSettingsIfNeeded(HostDataLayout layout, string installDir) {
|
|
||||||
if (!layout.Portable)
|
|
||||||
return;
|
|
||||||
|
|
||||||
var seed = Path.Combine(installDir, ConfigurationFileService.SeedFileName);
|
|
||||||
_ = new ConfigurationFileService(
|
|
||||||
layout.SharedSettingsFile,
|
|
||||||
File.Exists(seed) ? seed : null);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static int CopyNewDirectory(string source, string destination) {
|
private static int CopyNewDirectory(string source, string destination) {
|
||||||
Directory.CreateDirectory(destination);
|
Directory.CreateDirectory(destination);
|
||||||
var copied = 0;
|
var copied = 0;
|
||||||
@ -208,7 +144,10 @@ public static class HostDataDirectories {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static bool PathsEqual(string left, string right) =>
|
private static bool PathsEqual(string left, string right) =>
|
||||||
HostPaths.PathsEqual(left, right);
|
string.Equals(
|
||||||
|
Path.GetFullPath(left).TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar),
|
||||||
|
Path.GetFullPath(right).TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar),
|
||||||
|
OperatingSystem.IsWindows() ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal);
|
||||||
|
|
||||||
private static (int ExitCode, string Message) RunProcess(string fileName, string arguments) {
|
private static (int ExitCode, string Message) RunProcess(string fileName, string arguments) {
|
||||||
using var process = Process.Start(new ProcessStartInfo {
|
using var process = Process.Start(new ProcessStartInfo {
|
||||||
|
|||||||
@ -4,21 +4,13 @@ using MaksIT.UScheduler.Shared.Helpers;
|
|||||||
namespace MaksIT.UScheduler.Shared;
|
namespace MaksIT.UScheduler.Shared;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Default install and data locations. Standard layout keeps binaries under
|
/// Default install and data locations. Binaries live under Program Files;
|
||||||
/// Program Files and scripts, logs, and machine settings outside Program Files
|
/// scripts, logs, and machine settings are outside Program Files so normal
|
||||||
/// so normal users can read and write them after <c>--prepare-data</c> /
|
/// users can read and write them after <c>--prepare-data</c> / <c>--install</c>.
|
||||||
/// <c>--install</c>. A <see cref="PortableMarkerFileName"/> file next to the
|
|
||||||
/// worker (or in a parent folder) switches to a portable layout: scripts, logs,
|
|
||||||
/// and settings stay under that folder.
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static class HostPaths {
|
public static class HostPaths {
|
||||||
public const string ProductFolder = "UScheduler";
|
public const string ProductFolder = "UScheduler";
|
||||||
public const string Manufacturer = UserSettingsPath.Manufacturer;
|
public const string Manufacturer = UserSettingsPath.Manufacturer;
|
||||||
public const string PortableMarkerFileName = "portable";
|
|
||||||
public const string PortableScriptsFolderName = "Scripts";
|
|
||||||
public const string PortableLogsFolderName = "Logs";
|
|
||||||
public const string PortableDataFolderName = "Data";
|
|
||||||
public const string PortableMarkerContent = "MaksIT.UScheduler portable layout";
|
|
||||||
|
|
||||||
public static string SharedSettingsFile =>
|
public static string SharedSettingsFile =>
|
||||||
UserSettingsPath.GetShared(ProductFolder);
|
UserSettingsPath.GetShared(ProductFolder);
|
||||||
@ -61,111 +53,21 @@ public static class HostPaths {
|
|||||||
public static string DefaultDataRoot =>
|
public static string DefaultDataRoot =>
|
||||||
OperatingSystem.IsWindows() ? @"C:\MaksIT" : "/var/lib/maksit";
|
OperatingSystem.IsWindows() ? @"C:\MaksIT" : "/var/lib/maksit";
|
||||||
|
|
||||||
public static bool IsPortableLayout(string? startDirectory = null) =>
|
|
||||||
FindPortableRoot(startDirectory) is not null;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Directory that contains <see cref="PortableMarkerFileName"/>, walking up
|
/// Scripts folder to use at runtime: configured path if it exists, then
|
||||||
/// from <paramref name="startDirectory"/> (or the current app base).
|
/// <see cref="DefaultScriptsDirectory"/>, then a bundled/dev tree.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static string? FindPortableRoot(string? startDirectory = null) {
|
public static string ResolveScriptsDirectory(string? configured = null) {
|
||||||
var current = startDirectory ?? AppContext.BaseDirectory;
|
|
||||||
if (string.IsNullOrWhiteSpace(current))
|
|
||||||
return null;
|
|
||||||
|
|
||||||
try {
|
|
||||||
current = Path.GetFullPath(current);
|
|
||||||
}
|
|
||||||
catch (Exception ex) when (ex is ArgumentException or NotSupportedException or PathTooLongException) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (File.Exists(current))
|
|
||||||
current = Path.GetDirectoryName(current) ?? current;
|
|
||||||
|
|
||||||
for (var i = 0; i < 10 && !string.IsNullOrEmpty(current); i++) {
|
|
||||||
if (File.Exists(Path.Combine(current, PortableMarkerFileName)))
|
|
||||||
return current;
|
|
||||||
|
|
||||||
current = Directory.GetParent(current)?.FullName;
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static string GetPortableScriptsDirectory(string portableRoot) =>
|
|
||||||
Path.Combine(portableRoot, PortableScriptsFolderName);
|
|
||||||
|
|
||||||
public static string GetPortableLogDirectory(string portableRoot) =>
|
|
||||||
Path.Combine(portableRoot, PortableLogsFolderName);
|
|
||||||
|
|
||||||
public static string GetPortableDataDirectory(string portableRoot) =>
|
|
||||||
Path.Combine(portableRoot, PortableDataFolderName);
|
|
||||||
|
|
||||||
public static string GetPortableSharedSettingsFile(string portableRoot) =>
|
|
||||||
Path.Combine(GetPortableDataDirectory(portableRoot), "settings.json");
|
|
||||||
|
|
||||||
public static string GetPortableUiSettingsFile(string portableRoot) =>
|
|
||||||
Path.Combine(GetPortableDataDirectory(portableRoot), "ui-settings.json");
|
|
||||||
|
|
||||||
public static string ResolveSharedSettingsFile(string? startDirectory = null) {
|
|
||||||
var root = FindPortableRoot(startDirectory);
|
|
||||||
return root is null ? SharedSettingsFile : GetPortableSharedSettingsFile(root);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static string ResolveUiSettingsFile(string? startDirectory = null) {
|
|
||||||
var root = FindPortableRoot(startDirectory);
|
|
||||||
return root is null ? UserSettingsPath.Get(ProductFolder) : GetPortableUiSettingsFile(root);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void WritePortableMarker(string portableRoot) {
|
|
||||||
Directory.CreateDirectory(portableRoot);
|
|
||||||
var path = Path.Combine(portableRoot, PortableMarkerFileName);
|
|
||||||
if (!File.Exists(path))
|
|
||||||
File.WriteAllText(path, PortableMarkerContent + Environment.NewLine);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// When a portable root is present, replace machine-wide default script/log
|
|
||||||
/// paths with folders under that root. Custom paths are left unchanged.
|
|
||||||
/// </summary>
|
|
||||||
public static void ApplyPortableDefaults(Configuration configuration, string? startDirectory = null) {
|
|
||||||
ArgumentNullException.ThrowIfNull(configuration);
|
|
||||||
var root = FindPortableRoot(startDirectory);
|
|
||||||
if (root is null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(configuration.LogDir) || PathsEqual(configuration.LogDir, DefaultLogDirectory))
|
|
||||||
configuration.LogDir = GetPortableLogDirectory(root);
|
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(configuration.ScriptsDir) || PathsEqual(configuration.ScriptsDir, DefaultScriptsDirectory))
|
|
||||||
configuration.ScriptsDir = GetPortableScriptsDirectory(root);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Scripts folder to use at runtime: configured path if it exists, then a
|
|
||||||
/// portable <c>Scripts</c> folder, then <see cref="DefaultScriptsDirectory"/>,
|
|
||||||
/// then a bundled/dev tree.
|
|
||||||
/// </summary>
|
|
||||||
public static string ResolveScriptsDirectory(string? configured = null, string? startDirectory = null) {
|
|
||||||
var start = startDirectory ?? AppContext.BaseDirectory;
|
|
||||||
if (!string.IsNullOrWhiteSpace(configured)) {
|
if (!string.IsNullOrWhiteSpace(configured)) {
|
||||||
var resolved = PathHelper.ResolvePath(configured, start);
|
var resolved = PathHelper.ResolvePath(configured);
|
||||||
if (Directory.Exists(resolved))
|
if (Directory.Exists(resolved))
|
||||||
return resolved;
|
return resolved;
|
||||||
}
|
}
|
||||||
|
|
||||||
var portableRoot = FindPortableRoot(start);
|
|
||||||
if (portableRoot is not null) {
|
|
||||||
var portableScripts = GetPortableScriptsDirectory(portableRoot);
|
|
||||||
if (Directory.Exists(portableScripts))
|
|
||||||
return portableScripts;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Directory.Exists(DefaultScriptsDirectory))
|
if (Directory.Exists(DefaultScriptsDirectory))
|
||||||
return DefaultScriptsDirectory;
|
return DefaultScriptsDirectory;
|
||||||
|
|
||||||
var bundled = FindBundledScriptsDirectory(start);
|
var bundled = FindBundledScriptsDirectory();
|
||||||
return bundled ?? DefaultScriptsDirectory;
|
return bundled ?? DefaultScriptsDirectory;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -173,8 +75,8 @@ public static class HostPaths {
|
|||||||
var current = startDirectory ?? AppContext.BaseDirectory;
|
var current = startDirectory ?? AppContext.BaseDirectory;
|
||||||
for (var i = 0; i < 10 && !string.IsNullOrEmpty(current); i++) {
|
for (var i = 0; i < 10 && !string.IsNullOrEmpty(current); i++) {
|
||||||
foreach (var candidate in new[] {
|
foreach (var candidate in new[] {
|
||||||
Path.Combine(current, PortableScriptsFolderName),
|
Path.Combine(current, "Scripts"),
|
||||||
Path.Combine(current, "src", PortableScriptsFolderName)
|
Path.Combine(current, "src", "Scripts")
|
||||||
}) {
|
}) {
|
||||||
if (LooksLikeScriptsRoot(candidate))
|
if (LooksLikeScriptsRoot(candidate))
|
||||||
return candidate;
|
return candidate;
|
||||||
@ -238,17 +140,7 @@ public static class HostPaths {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
internal static bool PathsEqual(string left, string right) =>
|
|
||||||
string.Equals(
|
|
||||||
Path.GetFullPath(left).TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar),
|
|
||||||
Path.GetFullPath(right).TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar),
|
|
||||||
OperatingSystem.IsWindows() ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal);
|
|
||||||
|
|
||||||
private static bool LooksLikeScriptsRoot(string path) =>
|
private static bool LooksLikeScriptsRoot(string path) =>
|
||||||
Directory.Exists(path)
|
Directory.Exists(Path.Combine(path, "File-Sync"))
|
||||||
&& (Directory.Exists(Path.Combine(path, "File-Sync"))
|
|| File.Exists(Path.Combine(path, "SchedulerTemplate.psm1"));
|
||||||
|| Directory.Exists(Path.Combine(path, "Native-Sync"))
|
|
||||||
|| Directory.Exists(Path.Combine(path, "HyperV-Backup"))
|
|
||||||
|| Directory.Exists(Path.Combine(path, "Windows-Update"))
|
|
||||||
|| File.Exists(Path.Combine(path, "SchedulerTemplate.psm1")));
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -173,139 +173,6 @@ public class HostPathsTests {
|
|||||||
path);
|
path);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void FindPortableRoot_returns_null_without_marker() {
|
|
||||||
var root = Path.Combine(Path.GetTempPath(), $"uscheduler-no-portable-{Guid.NewGuid():N}");
|
|
||||||
Directory.CreateDirectory(root);
|
|
||||||
try {
|
|
||||||
Assert.Null(HostPaths.FindPortableRoot(root));
|
|
||||||
Assert.False(HostPaths.IsPortableLayout(root));
|
|
||||||
}
|
|
||||||
finally {
|
|
||||||
Directory.Delete(root, true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void FindPortableRoot_finds_marker_in_start_directory() {
|
|
||||||
var root = Path.Combine(Path.GetTempPath(), $"uscheduler-portable-{Guid.NewGuid():N}");
|
|
||||||
Directory.CreateDirectory(root);
|
|
||||||
File.WriteAllText(Path.Combine(root, HostPaths.PortableMarkerFileName), HostPaths.PortableMarkerContent);
|
|
||||||
try {
|
|
||||||
Assert.Equal(Path.GetFullPath(root), HostPaths.FindPortableRoot(root));
|
|
||||||
Assert.True(HostPaths.IsPortableLayout(root));
|
|
||||||
Assert.Equal(
|
|
||||||
Path.GetFullPath(Path.Combine(root, "Data", "settings.json")),
|
|
||||||
Path.GetFullPath(HostPaths.ResolveSharedSettingsFile(root)));
|
|
||||||
Assert.Equal(
|
|
||||||
Path.GetFullPath(Path.Combine(root, "Data", "ui-settings.json")),
|
|
||||||
Path.GetFullPath(HostPaths.ResolveUiSettingsFile(root)));
|
|
||||||
}
|
|
||||||
finally {
|
|
||||||
Directory.Delete(root, true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void FindPortableRoot_walks_up_to_parent_marker() {
|
|
||||||
var root = Path.Combine(Path.GetTempPath(), $"uscheduler-portable-parent-{Guid.NewGuid():N}");
|
|
||||||
var worker = Path.Combine(root, "MaksIT.UScheduler");
|
|
||||||
Directory.CreateDirectory(worker);
|
|
||||||
File.WriteAllText(Path.Combine(root, HostPaths.PortableMarkerFileName), HostPaths.PortableMarkerContent);
|
|
||||||
try {
|
|
||||||
Assert.Equal(Path.GetFullPath(root), HostPaths.FindPortableRoot(worker));
|
|
||||||
}
|
|
||||||
finally {
|
|
||||||
Directory.Delete(root, true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void ApplyPortableDefaults_replaces_machine_wide_paths() {
|
|
||||||
var root = Path.Combine(Path.GetTempPath(), $"uscheduler-portable-defaults-{Guid.NewGuid():N}");
|
|
||||||
Directory.CreateDirectory(root);
|
|
||||||
File.WriteAllText(Path.Combine(root, HostPaths.PortableMarkerFileName), HostPaths.PortableMarkerContent);
|
|
||||||
try {
|
|
||||||
var config = new Configuration {
|
|
||||||
LogDir = HostPaths.DefaultLogDirectory,
|
|
||||||
ScriptsDir = HostPaths.DefaultScriptsDirectory
|
|
||||||
};
|
|
||||||
HostPaths.ApplyPortableDefaults(config, root);
|
|
||||||
Assert.Equal(Path.GetFullPath(Path.Combine(root, "Logs")), Path.GetFullPath(config.LogDir));
|
|
||||||
Assert.Equal(Path.GetFullPath(Path.Combine(root, "Scripts")), Path.GetFullPath(config.ScriptsDir));
|
|
||||||
}
|
|
||||||
finally {
|
|
||||||
Directory.Delete(root, true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void Copies_seed_into_portable_data_folder_with_local_paths() {
|
|
||||||
var root = Path.Combine(Path.GetTempPath(), $"uscheduler-portable-seed-{Guid.NewGuid():N}");
|
|
||||||
var data = Path.Combine(root, "Data");
|
|
||||||
Directory.CreateDirectory(data);
|
|
||||||
File.WriteAllText(Path.Combine(root, HostPaths.PortableMarkerFileName), HostPaths.PortableMarkerContent);
|
|
||||||
var seed = Path.Combine(root, "appsettings.json");
|
|
||||||
var user = Path.Combine(data, "settings.json");
|
|
||||||
var defaultLog = HostPaths.DefaultLogDirectory.Replace("\\", "\\\\", StringComparison.Ordinal);
|
|
||||||
var defaultScripts = HostPaths.DefaultScriptsDirectory.Replace("\\", "\\\\", StringComparison.Ordinal);
|
|
||||||
File.WriteAllText(seed, $$"""
|
|
||||||
{
|
|
||||||
"Logging": { "LogLevel": { "Default": "Information" } },
|
|
||||||
"Configuration": {
|
|
||||||
"LogDir": "{{defaultLog}}",
|
|
||||||
"ScriptsDir": "{{defaultScripts}}",
|
|
||||||
"Powershell": [ { "Path": "File-Sync\\file-sync.ps1", "Disabled": true } ]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
""");
|
|
||||||
|
|
||||||
try {
|
|
||||||
var service = new ConfigurationFileService(user, seed);
|
|
||||||
Assert.True(File.Exists(user));
|
|
||||||
Assert.Equal(Path.GetFullPath(Path.Combine(root, "Logs")), Path.GetFullPath(service.Current.LogDir));
|
|
||||||
Assert.Equal(Path.GetFullPath(Path.Combine(root, "Scripts")), Path.GetFullPath(service.Current.ScriptsDir));
|
|
||||||
Assert.Single(service.Current.Powershell);
|
|
||||||
}
|
|
||||||
finally {
|
|
||||||
Directory.Delete(root, true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void FindBundledScriptsDirectory_finds_scripts_next_to_start() {
|
|
||||||
var root = Path.Combine(Path.GetTempPath(), $"uscheduler-bundled-{Guid.NewGuid():N}");
|
|
||||||
var start = Path.Combine(root, "install");
|
|
||||||
var scripts = Path.Combine(start, "Scripts");
|
|
||||||
Directory.CreateDirectory(Path.Combine(scripts, "Native-Sync"));
|
|
||||||
File.WriteAllText(Path.Combine(scripts, "Native-Sync", "native-sync.ps1"), "seed");
|
|
||||||
|
|
||||||
try {
|
|
||||||
var found = HostPaths.FindBundledScriptsDirectory(start);
|
|
||||||
Assert.Equal(Path.GetFullPath(scripts), found is null ? null : Path.GetFullPath(found));
|
|
||||||
}
|
|
||||||
finally {
|
|
||||||
Directory.Delete(root, true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void FindBundledScriptsDirectory_finds_sibling_scripts_folder() {
|
|
||||||
var root = Path.Combine(Path.GetTempPath(), $"uscheduler-sibling-{Guid.NewGuid():N}");
|
|
||||||
var start = Path.Combine(root, "MaksIT.UScheduler");
|
|
||||||
var scripts = Path.Combine(root, "Scripts");
|
|
||||||
Directory.CreateDirectory(start);
|
|
||||||
Directory.CreateDirectory(Path.Combine(scripts, "File-Sync"));
|
|
||||||
|
|
||||||
try {
|
|
||||||
var found = HostPaths.FindBundledScriptsDirectory(start);
|
|
||||||
Assert.Equal(Path.GetFullPath(scripts), found is null ? null : Path.GetFullPath(found));
|
|
||||||
}
|
|
||||||
finally {
|
|
||||||
Directory.Delete(root, true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void ResolveScriptPath_uses_scripts_dir() {
|
public void ResolveScriptPath_uses_scripts_dir() {
|
||||||
var dir = Path.Combine(Path.GetTempPath(), $"uscheduler-scripts-{Guid.NewGuid():N}");
|
var dir = Path.Combine(Path.GetTempPath(), $"uscheduler-scripts-{Guid.NewGuid():N}");
|
||||||
|
|||||||
@ -76,78 +76,4 @@ public class HostDataDirectoriesTests {
|
|||||||
Directory.Delete(root, true);
|
Directory.Delete(root, true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void CopySeedScriptsIfMissing_copies_into_empty_destination() {
|
|
||||||
var root = Path.Combine(Path.GetTempPath(), $"uscheduler-seed-{Guid.NewGuid():N}");
|
|
||||||
var source = Path.Combine(root, "seed");
|
|
||||||
var dest = Path.Combine(root, "scripts");
|
|
||||||
Directory.CreateDirectory(Path.Combine(source, "File-Sync"));
|
|
||||||
Directory.CreateDirectory(dest);
|
|
||||||
File.WriteAllText(Path.Combine(source, "File-Sync", "file-sync.ps1"), "seed");
|
|
||||||
File.WriteAllText(Path.Combine(source, "SchedulerTemplate.psm1"), "module");
|
|
||||||
|
|
||||||
try {
|
|
||||||
var result = HostDataDirectories.CopySeedScriptsIfMissing(source, dest);
|
|
||||||
|
|
||||||
Assert.Equal(2, result.CopiedFiles);
|
|
||||||
Assert.Equal(0, result.SkippedItems);
|
|
||||||
Assert.Equal("seed", File.ReadAllText(Path.Combine(dest, "File-Sync", "file-sync.ps1")));
|
|
||||||
Assert.Equal("module", File.ReadAllText(Path.Combine(dest, "SchedulerTemplate.psm1")));
|
|
||||||
}
|
|
||||||
finally {
|
|
||||||
Directory.Delete(root, true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void ResolveLayout_standard_uses_machine_wide_paths() {
|
|
||||||
var install = Path.Combine(Path.GetTempPath(), $"uscheduler-layout-std-{Guid.NewGuid():N}");
|
|
||||||
Directory.CreateDirectory(install);
|
|
||||||
try {
|
|
||||||
var layout = HostDataDirectories.ResolveLayout(install);
|
|
||||||
Assert.False(layout.Portable);
|
|
||||||
Assert.Equal(HostPaths.DefaultDataRoot, layout.Root);
|
|
||||||
Assert.Equal(HostPaths.DefaultScriptsDirectory, layout.ScriptsDirectory);
|
|
||||||
Assert.Equal(HostPaths.DefaultLogDirectory, layout.LogDirectory);
|
|
||||||
Assert.Equal(HostPaths.SharedSettingsFile, layout.SharedSettingsFile);
|
|
||||||
}
|
|
||||||
finally {
|
|
||||||
Directory.Delete(install, true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void ResolveLayout_portable_requested_keeps_everything_under_install_folder() {
|
|
||||||
var install = Path.Combine(Path.GetTempPath(), $"uscheduler-layout-port-{Guid.NewGuid():N}");
|
|
||||||
Directory.CreateDirectory(install);
|
|
||||||
try {
|
|
||||||
var layout = HostDataDirectories.ResolveLayout(install, portableRequested: true);
|
|
||||||
Assert.True(layout.Portable);
|
|
||||||
Assert.Equal(Path.GetFullPath(install), Path.GetFullPath(layout.Root));
|
|
||||||
Assert.Equal(Path.GetFullPath(Path.Combine(install, "Scripts")), Path.GetFullPath(layout.ScriptsDirectory));
|
|
||||||
Assert.Equal(Path.GetFullPath(Path.Combine(install, "Logs")), Path.GetFullPath(layout.LogDirectory));
|
|
||||||
Assert.Equal(
|
|
||||||
Path.GetFullPath(Path.Combine(install, "Data", "settings.json")),
|
|
||||||
Path.GetFullPath(layout.SharedSettingsFile));
|
|
||||||
}
|
|
||||||
finally {
|
|
||||||
Directory.Delete(install, true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void ResolveLayout_detects_existing_portable_marker() {
|
|
||||||
var install = Path.Combine(Path.GetTempPath(), $"uscheduler-layout-mark-{Guid.NewGuid():N}");
|
|
||||||
Directory.CreateDirectory(install);
|
|
||||||
File.WriteAllText(Path.Combine(install, HostPaths.PortableMarkerFileName), HostPaths.PortableMarkerContent);
|
|
||||||
try {
|
|
||||||
var layout = HostDataDirectories.ResolveLayout(install);
|
|
||||||
Assert.True(layout.Portable);
|
|
||||||
Assert.Equal(Path.GetFullPath(install), Path.GetFullPath(layout.Root));
|
|
||||||
}
|
|
||||||
finally {
|
|
||||||
Directory.Delete(install, true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,7 +4,7 @@
|
|||||||
<OutputType>WinExe</OutputType>
|
<OutputType>WinExe</OutputType>
|
||||||
<TargetFramework>net10.0</TargetFramework>
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
<RootNamespace>MaksIT.UScheduler.UI</RootNamespace>
|
<RootNamespace>MaksIT.UScheduler.UI</RootNamespace>
|
||||||
<Version>1.2.0</Version>
|
<Version>1.1.0</Version>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
<BuiltInComInteropSupport>true</BuiltInComInteropSupport>
|
<BuiltInComInteropSupport>true</BuiltInComInteropSupport>
|
||||||
@ -14,10 +14,10 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Avalonia" Version="12.1.2" />
|
<PackageReference Include="Avalonia" Version="12.1.0" />
|
||||||
<PackageReference Include="Avalonia.Desktop" Version="12.1.2" />
|
<PackageReference Include="Avalonia.Desktop" Version="12.1.0" />
|
||||||
<PackageReference Include="Avalonia.Themes.Simple" Version="12.1.2" />
|
<PackageReference Include="Avalonia.Themes.Simple" Version="12.1.0" />
|
||||||
<PackageReference Include="Avalonia.Controls.DataGrid" Version="12.1.2" />
|
<PackageReference Include="Avalonia.Controls.DataGrid" Version="12.1.0" />
|
||||||
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.4.2" />
|
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.4.2" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
|||||||
@ -9,8 +9,6 @@ internal static class Program {
|
|||||||
public static void Main(string[] args) =>
|
public static void Main(string[] args) =>
|
||||||
BuildAvaloniaApp().StartWithClassicDesktopLifetime(args);
|
BuildAvaloniaApp().StartWithClassicDesktopLifetime(args);
|
||||||
|
|
||||||
// Linux: X11/XWayland. Avalonia 12.1.2 native Wayland still hangs on GNOME's
|
|
||||||
// xdg_toplevel.configure(0, 0) and never maps a window (GNOME app icon).
|
|
||||||
public static AppBuilder BuildAvaloniaApp() =>
|
public static AppBuilder BuildAvaloniaApp() =>
|
||||||
AppBuilder.Configure<App>()
|
AppBuilder.Configure<App>()
|
||||||
.UsePlatformDetect()
|
.UsePlatformDetect()
|
||||||
|
|||||||
@ -5,8 +5,7 @@ using MaksIT.UScheduler.Shared.Helpers;
|
|||||||
namespace MaksIT.UScheduler.UI.Services;
|
namespace MaksIT.UScheduler.UI.Services;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Loads and saves machine-wide service configuration (ProgramData settings.json,
|
/// Loads and saves machine-wide service configuration (ProgramData settings.json).
|
||||||
/// or the portable Data folder).
|
|
||||||
/// Shipped appsettings.json next to the worker is seed-only.
|
/// Shipped appsettings.json next to the worker is seed-only.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class AppSettingsService {
|
public class AppSettingsService {
|
||||||
@ -25,10 +24,10 @@ public class AppSettingsService {
|
|||||||
if (string.IsNullOrEmpty(seed) && !string.IsNullOrEmpty(serviceDirectory))
|
if (string.IsNullOrEmpty(seed) && !string.IsNullOrEmpty(serviceDirectory))
|
||||||
seed = Path.Combine(serviceDirectory, ConfigurationFileService.SeedFileName);
|
seed = Path.Combine(serviceDirectory, ConfigurationFileService.SeedFileName);
|
||||||
|
|
||||||
_files = new ConfigurationFileService(HostPaths.ResolveSharedSettingsFile(serviceDirectory), seed);
|
_files = new ConfigurationFileService(HostPaths.SharedSettingsFile, seed);
|
||||||
var config = _files.Current;
|
var config = _files.Current;
|
||||||
config.EnsureDefaults();
|
config.EnsureDefaults();
|
||||||
ScriptsDirectory = config.GetEffectiveScriptsDirectory(_serviceDirectory);
|
ScriptsDirectory = config.GetEffectiveScriptsDirectory();
|
||||||
return config;
|
return config;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -38,7 +37,7 @@ public class AppSettingsService {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
_files.Save(configuration);
|
_files.Save(configuration);
|
||||||
ScriptsDirectory = configuration.GetEffectiveScriptsDirectory(_serviceDirectory);
|
ScriptsDirectory = configuration.GetEffectiveScriptsDirectory();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
catch {
|
catch {
|
||||||
|
|||||||
@ -7,8 +7,7 @@ using MaksIT.UScheduler.Shared.Helpers;
|
|||||||
namespace MaksIT.UScheduler.UI.Services;
|
namespace MaksIT.UScheduler.UI.Services;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Per-user UI prefs stored in roaming AppData (not next to the exe), or under
|
/// Per-user UI prefs stored in roaming AppData (not next to the exe).
|
||||||
/// the portable Data folder when a portable layout is used.
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class UISettings {
|
public class UISettings {
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -19,19 +18,17 @@ public class UISettings {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Service for loading and saving UI settings under the current user's AppData
|
/// Service for loading and saving UI settings under the current user's AppData.
|
||||||
/// (or the portable Data folder). Machine-wide schedule configuration lives in
|
/// Machine-wide schedule configuration lives in ProgramData via
|
||||||
/// ProgramData (or that same Data folder) via <see cref="ConfigurationFileService"/>.
|
/// <see cref="ConfigurationFileService"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class UISettingsService {
|
public class UISettingsService {
|
||||||
public const string ProductFolder = HostPaths.ProductFolder;
|
public const string ProductFolder = HostPaths.ProductFolder;
|
||||||
|
|
||||||
private readonly string _settingsFilePath;
|
private readonly string _settingsFilePath;
|
||||||
|
|
||||||
public UISettingsService(string? settingsFilePath = null) {
|
public UISettingsService() {
|
||||||
_settingsFilePath = string.IsNullOrWhiteSpace(settingsFilePath)
|
_settingsFilePath = UserSettingsPath.Get(ProductFolder);
|
||||||
? HostPaths.ResolveUiSettingsFile()
|
|
||||||
: settingsFilePath;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public UISettings Load() {
|
public UISettings Load() {
|
||||||
|
|||||||
@ -78,8 +78,7 @@ public partial class MainViewModel : ObservableObject {
|
|||||||
var seedPath = string.IsNullOrEmpty(bin)
|
var seedPath = string.IsNullOrEmpty(bin)
|
||||||
? null
|
? null
|
||||||
: Path.Combine(bin, ConfigurationFileService.SeedFileName);
|
: Path.Combine(bin, ConfigurationFileService.SeedFileName);
|
||||||
var sharedSettingsPath = HostPaths.ResolveSharedSettingsFile(bin);
|
var sharedExists = File.Exists(HostPaths.SharedSettingsFile);
|
||||||
var sharedExists = File.Exists(sharedSettingsPath);
|
|
||||||
var seedExists = !string.IsNullOrEmpty(seedPath) && File.Exists(seedPath);
|
var seedExists = !string.IsNullOrEmpty(seedPath) && File.Exists(seedPath);
|
||||||
|
|
||||||
if (!sharedExists && !seedExists) {
|
if (!sharedExists && !seedExists) {
|
||||||
@ -87,8 +86,8 @@ public partial class MainViewModel : ObservableObject {
|
|||||||
AppSettingsLoadError = "Set Service Bin Path to the worker folder (seed appsettings.json), or register the service to create shared settings.";
|
AppSettingsLoadError = "Set Service Bin Path to the worker folder (seed appsettings.json), or register the service to create shared settings.";
|
||||||
ServiceName = string.Empty;
|
ServiceName = string.Empty;
|
||||||
LogDirectory = string.Empty;
|
LogDirectory = string.Empty;
|
||||||
ScriptsDirectory = HostPaths.ResolveScriptsDirectory(startDirectory: bin);
|
ScriptsDirectory = HostPaths.DefaultScriptsDirectory;
|
||||||
SharedSettingsPath = string.IsNullOrEmpty(bin) ? HostPaths.ResolveSharedSettingsFile() : sharedSettingsPath;
|
SharedSettingsPath = HostPaths.SharedSettingsFile;
|
||||||
ProcessList.Clear();
|
ProcessList.Clear();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -99,8 +98,8 @@ public partial class MainViewModel : ObservableObject {
|
|||||||
_serviceManager = new HostServiceManager(_serviceConfig.ServiceName);
|
_serviceManager = new HostServiceManager(_serviceConfig.ServiceName);
|
||||||
ServiceName = _serviceConfig.ServiceName;
|
ServiceName = _serviceConfig.ServiceName;
|
||||||
LogDirectory = _serviceConfig.GetEffectiveLogDirectory(bin);
|
LogDirectory = _serviceConfig.GetEffectiveLogDirectory(bin);
|
||||||
ScriptsDirectory = _serviceConfig.GetEffectiveScriptsDirectory(bin);
|
ScriptsDirectory = _serviceConfig.GetEffectiveScriptsDirectory();
|
||||||
SharedSettingsPath = _appSettingsService.SettingsPath ?? HostPaths.ResolveSharedSettingsFile(bin);
|
SharedSettingsPath = _appSettingsService.SettingsPath ?? HostPaths.SharedSettingsFile;
|
||||||
ProcessList.Clear();
|
ProcessList.Clear();
|
||||||
foreach (var p in _serviceConfig.Processes)
|
foreach (var p in _serviceConfig.Processes)
|
||||||
ProcessList.Add(p);
|
ProcessList.Add(p);
|
||||||
@ -109,8 +108,8 @@ public partial class MainViewModel : ObservableObject {
|
|||||||
AppSettingsLoadError = "Failed to load shared settings. Register the service once to create writable data folders.";
|
AppSettingsLoadError = "Failed to load shared settings. Register the service once to create writable data folders.";
|
||||||
ServiceName = string.Empty;
|
ServiceName = string.Empty;
|
||||||
LogDirectory = string.Empty;
|
LogDirectory = string.Empty;
|
||||||
ScriptsDirectory = HostPaths.ResolveScriptsDirectory(startDirectory: bin);
|
ScriptsDirectory = HostPaths.DefaultScriptsDirectory;
|
||||||
SharedSettingsPath = HostPaths.ResolveSharedSettingsFile(bin);
|
SharedSettingsPath = HostPaths.SharedSettingsFile;
|
||||||
ProcessList.Clear();
|
ProcessList.Clear();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -133,7 +132,7 @@ public partial class MainViewModel : ObservableObject {
|
|||||||
private string _scriptsDirectory = HostPaths.DefaultScriptsDirectory;
|
private string _scriptsDirectory = HostPaths.DefaultScriptsDirectory;
|
||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
private string _sharedSettingsPath = HostPaths.ResolveSharedSettingsFile();
|
private string _sharedSettingsPath = HostPaths.SharedSettingsFile;
|
||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
[NotifyPropertyChangedFor(nameof(HasAppSettingsLoadError))]
|
[NotifyPropertyChangedFor(nameof(HasAppSettingsLoadError))]
|
||||||
@ -206,7 +205,7 @@ public partial class MainViewModel : ObservableObject {
|
|||||||
if (string.IsNullOrWhiteSpace(ResolvedExecutablePath) || !File.Exists(ResolvedExecutablePath)) {
|
if (string.IsNullOrWhiteSpace(ResolvedExecutablePath) || !File.Exists(ResolvedExecutablePath)) {
|
||||||
await _dialogs.ShowMessageAsync(
|
await _dialogs.ShowMessageAsync(
|
||||||
"Register Service",
|
"Register Service",
|
||||||
"Could not find MaksIT.UScheduler.exe. Set Service Bin Path to the worker install folder.");
|
"Could not find MaksIT.UScheduler.exe. Set Service Bin Path to the Program Files install folder.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -3,7 +3,7 @@
|
|||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net10.0</TargetFramework>
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
<RootNamespace>$(MSBuildProjectName.Replace(" ", "_"))</RootNamespace>
|
<RootNamespace>$(MSBuildProjectName.Replace(" ", "_"))</RootNamespace>
|
||||||
<Version>1.2.0</Version>
|
<Version>1.1.0</Version>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
<UserSecretsId>dotnet-UScheduler-040d8105-9e07-4024-a632-cbe091387b66</UserSecretsId>
|
<UserSecretsId>dotnet-UScheduler-040d8105-9e07-4024-a632-cbe091387b66</UserSecretsId>
|
||||||
@ -41,9 +41,5 @@
|
|||||||
<None Include="..\..\assets\**\*" Link="assets\%(RecursiveDir)%(Filename)%(Extension)">
|
<None Include="..\..\assets\**\*" Link="assets\%(RecursiveDir)%(Filename)%(Extension)">
|
||||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||||
</None>
|
</None>
|
||||||
<Content Include="..\Scripts\**\*" Link="Scripts\%(RecursiveDir)%(Filename)%(Extension)">
|
|
||||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
|
||||||
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
|
|
||||||
</Content>
|
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@ -9,20 +9,18 @@ using MaksIT.UScheduler.BackgroundServices;
|
|||||||
|
|
||||||
var basePath = AppContext.BaseDirectory;
|
var basePath = AppContext.BaseDirectory;
|
||||||
var seedPath = Path.Combine(basePath, ConfigurationFileService.SeedFileName);
|
var seedPath = Path.Combine(basePath, ConfigurationFileService.SeedFileName);
|
||||||
var portableRequested = HasFlag(args, "--portable");
|
var settingsPath = HostPaths.SharedSettingsFile;
|
||||||
|
|
||||||
if (args.Length > 0) {
|
if (args.Length > 0) {
|
||||||
var early = args[0].ToLowerInvariant();
|
var early = args[0].ToLowerInvariant();
|
||||||
if (early is "--install" or "-i" or "--prepare-data") {
|
if (early is "--install" or "-i" or "--prepare-data") {
|
||||||
var prepared = HostDataDirectories.Prepare(basePath, portableRequested);
|
var prepared = HostDataDirectories.Prepare(basePath);
|
||||||
Console.WriteLine(prepared.Message);
|
Console.WriteLine(prepared.Message);
|
||||||
if (!prepared.Success)
|
if (!prepared.Success)
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var settingsPath = HostPaths.ResolveSharedSettingsFile(basePath);
|
|
||||||
|
|
||||||
_ = new ConfigurationFileService(settingsPath, seedPath);
|
_ = new ConfigurationFileService(settingsPath, seedPath);
|
||||||
|
|
||||||
var configurationRoot = new ConfigurationBuilder()
|
var configurationRoot = new ConfigurationBuilder()
|
||||||
@ -65,7 +63,7 @@ if (args.Length > 0) {
|
|||||||
return GetServiceStatus(serviceName);
|
return GetServiceStatus(serviceName);
|
||||||
|
|
||||||
case "--prepare-data":
|
case "--prepare-data":
|
||||||
return PrepareDataDirectories(basePath, portableRequested);
|
return PrepareDataDirectories(basePath);
|
||||||
|
|
||||||
case "--help":
|
case "--help":
|
||||||
case "-h":
|
case "-h":
|
||||||
@ -125,12 +123,11 @@ static void PrintHelp(string name) =>
|
|||||||
--start Start the service
|
--start Start the service
|
||||||
--stop Stop the service
|
--stop Stop the service
|
||||||
--status Query service status
|
--status Query service status
|
||||||
--prepare-data Create scripts, logs, and shared settings (elevated)
|
--prepare-data Create C:\MaksIT\Scripts, C:\MaksIT\Logs, and shared settings (elevated)
|
||||||
--portable With --install / --prepare-data: keep data in the install folder
|
|
||||||
--help, -h Show this help message
|
--help, -h Show this help message
|
||||||
|
|
||||||
Service Name: {name}
|
Service Name: {name}
|
||||||
Config File: {HostPaths.ResolveSharedSettingsFile()}
|
Config File: {HostPaths.SharedSettingsFile}
|
||||||
Seed File: appsettings.json (next to the executable, logging + first-run seed)
|
Seed File: appsettings.json (next to the executable, logging + first-run seed)
|
||||||
|
|
||||||
Note: Install/uninstall/prepare-data typically require administrator / root privileges.
|
Note: Install/uninstall/prepare-data typically require administrator / root privileges.
|
||||||
@ -190,19 +187,14 @@ static int InstallService(string name, string exePath, string description) {
|
|||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
static int PrepareDataDirectories(string installDirectory, bool portable) {
|
static int PrepareDataDirectories(string installDirectory) {
|
||||||
var layout = HostDataDirectories.ResolveLayout(installDirectory, portable);
|
|
||||||
Console.WriteLine($"Install: {installDirectory}");
|
Console.WriteLine($"Install: {installDirectory}");
|
||||||
Console.WriteLine($"Type: {(layout.Portable ? "portable" : "standard")}");
|
Console.WriteLine($"Settings: {HostPaths.SharedSettingsFile}");
|
||||||
Console.WriteLine($"Settings: {layout.SharedSettingsFile}");
|
Console.WriteLine($"Scripts: {HostPaths.DefaultScriptsDirectory}");
|
||||||
Console.WriteLine($"Scripts: {layout.ScriptsDirectory}");
|
Console.WriteLine($"Logs: {HostPaths.DefaultLogDirectory}");
|
||||||
Console.WriteLine($"Logs: {layout.LogDirectory}");
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
static bool HasFlag(string[] args, string name) =>
|
|
||||||
args.Any(a => string.Equals(a, name, StringComparison.OrdinalIgnoreCase));
|
|
||||||
|
|
||||||
static int UninstallService(string name) {
|
static int UninstallService(string name) {
|
||||||
if (OperatingSystem.IsWindows())
|
if (OperatingSystem.IsWindows())
|
||||||
return UninstallWindowsService(name);
|
return UninstallWindowsService(name);
|
||||||
|
|||||||
@ -9,7 +9,7 @@
|
|||||||
Does not publish. Copies src/Scripts into each MaksIT.UScheduler RID output,
|
Does not publish. Copies src/Scripts into each MaksIT.UScheduler RID output,
|
||||||
rewrites worker seed appsettings for the bundled layout, and
|
rewrites worker seed appsettings for the bundled layout, and
|
||||||
writes Windows/Linux launchers. The portable zip is the win-x64 bundle folder
|
writes Windows/Linux launchers. The portable zip is the win-x64 bundle folder
|
||||||
A flat installer payload (worker + UI + Scripts) is staged for WiX.
|
(unique names). A flat installer payload (worker + UI) is staged for WiX.
|
||||||
#>
|
#>
|
||||||
|
|
||||||
if (-not (Get-Command Import-PluginDependency -ErrorAction SilentlyContinue)) {
|
if (-not (Get-Command Import-PluginDependency -ErrorAction SilentlyContinue)) {
|
||||||
@ -245,9 +245,7 @@ DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
|
|||||||
exec "$DIR/MaksIT.UScheduler.UI/MaksIT.UScheduler.UI" "$@"
|
exec "$DIR/MaksIT.UScheduler.UI/MaksIT.UScheduler.UI" "$@"
|
||||||
'@
|
'@
|
||||||
Set-Content -Path $shPath -Value $shBody -Encoding utf8
|
Set-Content -Path $shPath -Value $shBody -Encoding utf8
|
||||||
$portableMarkerPath = Join-Path $bundleDirectory "portable"
|
Write-Log -Level "OK" -Message " Created launchers."
|
||||||
Set-Content -Path $portableMarkerPath -Value "MaksIT.UScheduler portable layout" -Encoding ascii
|
|
||||||
Write-Log -Level "OK" -Message " Created launchers and portable marker."
|
|
||||||
|
|
||||||
$installerPayload = if ($pluginSettings.PSObject.Properties['installerPayloadDir'] -and -not [string]::IsNullOrWhiteSpace([string]$pluginSettings.installerPayloadDir)) {
|
$installerPayload = if ($pluginSettings.PSObject.Properties['installerPayloadDir'] -and -not [string]::IsNullOrWhiteSpace([string]$pluginSettings.installerPayloadDir)) {
|
||||||
Resolve-PluginPath -Path ([string]$pluginSettings.installerPayloadDir) -BasePath $scriptDir
|
Resolve-PluginPath -Path ([string]$pluginSettings.installerPayloadDir) -BasePath $scriptDir
|
||||||
@ -256,7 +254,7 @@ exec "$DIR/MaksIT.UScheduler.UI/MaksIT.UScheduler.UI" "$@"
|
|||||||
Join-Path $sharedSettings.artifactsDirectory "installer-payload"
|
Join-Path $sharedSettings.artifactsDirectory "installer-payload"
|
||||||
}
|
}
|
||||||
|
|
||||||
Write-Log -Level "STEP" -Message "Preparing per-machine installer payload (worker + UI + Scripts)..."
|
Write-Log -Level "STEP" -Message "Preparing per-machine installer payload (worker + UI)..."
|
||||||
if (Test-Path $installerPayload) {
|
if (Test-Path $installerPayload) {
|
||||||
Remove-Item -Path $installerPayload -Recurse -Force
|
Remove-Item -Path $installerPayload -Recurse -Force
|
||||||
}
|
}
|
||||||
@ -264,13 +262,7 @@ exec "$DIR/MaksIT.UScheduler.UI/MaksIT.UScheduler.UI" "$@"
|
|||||||
New-Item -ItemType Directory -Path $installerPayload | Out-Null
|
New-Item -ItemType Directory -Path $installerPayload | Out-Null
|
||||||
Copy-Item -Path (Join-Path ([string]$uiWin.directory) '*') -Destination $installerPayload -Recurse -Force
|
Copy-Item -Path (Join-Path ([string]$uiWin.directory) '*') -Destination $installerPayload -Recurse -Force
|
||||||
Copy-Item -Path (Join-Path ([string]$workerWin.directory) '*') -Destination $installerPayload -Recurse -Force
|
Copy-Item -Path (Join-Path ([string]$workerWin.directory) '*') -Destination $installerPayload -Recurse -Force
|
||||||
$payloadScripts = Join-Path $installerPayload "Scripts"
|
|
||||||
if (Test-Path -LiteralPath $payloadScripts) {
|
|
||||||
Remove-Item -LiteralPath $payloadScripts -Recurse -Force
|
|
||||||
}
|
|
||||||
Copy-Item -Path $scriptsSourcePath -Destination $payloadScripts -Recurse
|
|
||||||
Write-Log -Level "OK" -Message " Installer payload: $installerPayload"
|
Write-Log -Level "OK" -Message " Installer payload: $installerPayload"
|
||||||
Write-Log -Level "OK" -Message " Installer Scripts: $payloadScripts"
|
|
||||||
|
|
||||||
Set-EngineFact -Context $sharedSettings -Namespace 'release' -Name 'archiveInputs' -Value @($bundleDirectory) -Overwrite Replace -LegacyProperty 'releaseArchiveInputs'
|
Set-EngineFact -Context $sharedSettings -Namespace 'release' -Name 'archiveInputs' -Value @($bundleDirectory) -Overwrite Replace -LegacyProperty 'releaseArchiveInputs'
|
||||||
Set-EngineFact -Context $sharedSettings -Namespace 'dotnet' -Name 'publishOutputs' -Value @() -Overwrite Replace
|
Set-EngineFact -Context $sharedSettings -Namespace 'dotnet' -Name 'publishOutputs' -Value @() -Overwrite Replace
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://json-schema.org/draft-07/schema",
|
"$schema": "https://json-schema.org/draft-07/schema",
|
||||||
"title": "MaksIT.UScheduler Release Script Settings",
|
"title": "MaksIT.UScheduler Release Script Settings",
|
||||||
"description": "Community desktop pipeline. GitHub assets are siblings: portable zip (win-x64 worker, UI, Scripts, portable marker), Windows setup exe (Avalonia UI; Standard or Portable layout), Flatpak (Avalonia UI). The zip never contains the installer or Flatpak.",
|
"description": "Community desktop pipeline. GitHub assets are siblings: portable zip (win-x64 worker, UI, Scripts), Windows setup exe (Avalonia UI), Flatpak (Avalonia UI). The zip never contains the installer or Flatpak.",
|
||||||
"plugins": [
|
"plugins": [
|
||||||
{
|
{
|
||||||
"name": "DotNetReleaseVersion",
|
"name": "DotNetReleaseVersion",
|
||||||
@ -80,36 +80,21 @@
|
|||||||
"installScope": "perMachine",
|
"installScope": "perMachine",
|
||||||
"installFolderName": "UScheduler",
|
"installFolderName": "UScheduler",
|
||||||
"iconPath": "..\\..\\..\\src\\MaksIT.UScheduler.UI\\Assets\\icon.ico",
|
"iconPath": "..\\..\\..\\src\\MaksIT.UScheduler.UI\\Assets\\icon.ico",
|
||||||
"exeNamePattern": "maksit-uscheduler-{version}.exe",
|
"exeNamePattern": "maksit-uscheduler-{version}.exe"
|
||||||
"postInstallExecutableName": "MaksIT.UScheduler.exe",
|
|
||||||
"postInstallArguments": "--prepare-data"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "FlatpakPack",
|
"name": "FlatpakPack",
|
||||||
"stageLabel": "build",
|
"stageLabel": "build",
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"appId": "com.maks_it.uscheduler",
|
"appId": "com.maks_it.UScheduler",
|
||||||
"appName": "MaksIT UScheduler",
|
"appName": "MaksIT UScheduler",
|
||||||
"command": "maksit-uscheduler",
|
"command": "maksit-uscheduler",
|
||||||
"executableName": "MaksIT.UScheduler.UI",
|
"executableName": "MaksIT.UScheduler.UI",
|
||||||
"runtimeIdentifier": "linux-x64",
|
"runtimeIdentifier": "linux-x64",
|
||||||
"publishDir": "..\\..\\..\\releases\\MaksIT.UScheduler.UI\\linux-x64",
|
"publishDir": "..\\..\\..\\releases\\MaksIT.UScheduler.UI\\linux-x64",
|
||||||
"summary": "Schedule PowerShell scripts and processes",
|
"summary": "Schedule PowerShell scripts and processes",
|
||||||
"description": "Schedule and run PowerShell scripts and console processes from an Avalonia desktop UI.",
|
"projectLicense": "MIT",
|
||||||
"projectLicense": "Apache-2.0",
|
|
||||||
"categories": "Utility;System;",
|
|
||||||
"iconPath": "..\\..\\..\\src\\MaksIT.UScheduler.UI\\Assets\\icon.png",
|
"iconPath": "..\\..\\..\\src\\MaksIT.UScheduler.UI\\Assets\\icon.png",
|
||||||
"metainfoPath": "..\\..\\..\\data\\com.maks_it.uscheduler.metainfo.xml",
|
|
||||||
"desktopPath": "..\\..\\..\\data\\com.maks_it.uscheduler.desktop",
|
|
||||||
"finishArgs": [
|
|
||||||
"--share=ipc",
|
|
||||||
"--share=network",
|
|
||||||
"--socket=wayland",
|
|
||||||
"--socket=x11",
|
|
||||||
"--socket=pulseaudio",
|
|
||||||
"--device=dri",
|
|
||||||
"--filesystem=home"
|
|
||||||
],
|
|
||||||
"bundleNamePattern": "maksit-uscheduler-{version}.flatpak",
|
"bundleNamePattern": "maksit-uscheduler-{version}.flatpak",
|
||||||
"buildBundle": true,
|
"buildBundle": true,
|
||||||
"attachSourceZip": false,
|
"attachSourceZip": false,
|
||||||
|
|||||||
@ -496,10 +496,9 @@ function Get-DefaultFlatpakFinishArgs {
|
|||||||
return @(
|
return @(
|
||||||
'--share=ipc',
|
'--share=ipc',
|
||||||
'--share=network',
|
'--share=network',
|
||||||
'--socket=x11',
|
'--socket=fallback-x11',
|
||||||
'--socket=wayland',
|
'--socket=wayland',
|
||||||
'--device=dri',
|
'--device=dri',
|
||||||
'--socket=pulseaudio',
|
|
||||||
'--filesystem=home'
|
'--filesystem=home'
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -525,118 +524,12 @@ function New-FlatpakDesktopEntry {
|
|||||||
"Name=$AppName",
|
"Name=$AppName",
|
||||||
"Exec=$Command",
|
"Exec=$Command",
|
||||||
"Icon=$AppId",
|
"Icon=$AppId",
|
||||||
"StartupWMClass=$AppId",
|
|
||||||
'StartupNotify=true',
|
|
||||||
'Terminal=false',
|
'Terminal=false',
|
||||||
"Categories=$Categories"
|
"Categories=$Categories"
|
||||||
)
|
)
|
||||||
return (($lines -join "`n") + "`n")
|
return (($lines -join "`n") + "`n")
|
||||||
}
|
}
|
||||||
|
|
||||||
function Copy-FlatpakHicolorIcons {
|
|
||||||
param(
|
|
||||||
[Parameter(Mandatory = $true)]
|
|
||||||
[string]$AppId,
|
|
||||||
|
|
||||||
[Parameter(Mandatory = $true)]
|
|
||||||
[string]$HicolorRoot,
|
|
||||||
|
|
||||||
[Parameter(Mandatory = $false)]
|
|
||||||
[string]$IconPath,
|
|
||||||
|
|
||||||
[Parameter(Mandatory = $false)]
|
|
||||||
[string]$SvgIconPath
|
|
||||||
)
|
|
||||||
|
|
||||||
$pngSource = $null
|
|
||||||
$svgSource = $null
|
|
||||||
|
|
||||||
if (-not [string]::IsNullOrWhiteSpace($IconPath) -and (Test-Path -LiteralPath $IconPath -PathType Leaf)) {
|
|
||||||
$ext = [System.IO.Path]::GetExtension($IconPath)
|
|
||||||
if ($ext -ieq '.png') {
|
|
||||||
$pngSource = $IconPath
|
|
||||||
}
|
|
||||||
elseif ($ext -ieq '.svg') {
|
|
||||||
$svgSource = $IconPath
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (-not [string]::IsNullOrWhiteSpace($SvgIconPath) -and (Test-Path -LiteralPath $SvgIconPath -PathType Leaf)) {
|
|
||||||
$svgSource = $SvgIconPath
|
|
||||||
}
|
|
||||||
|
|
||||||
if ([string]::IsNullOrWhiteSpace($svgSource) -and -not [string]::IsNullOrWhiteSpace($pngSource)) {
|
|
||||||
$siblingSvg = [System.IO.Path]::ChangeExtension($pngSource, '.svg')
|
|
||||||
if (Test-Path -LiteralPath $siblingSvg -PathType Leaf) {
|
|
||||||
$svgSource = $siblingSvg
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if ([string]::IsNullOrWhiteSpace($pngSource) -and -not [string]::IsNullOrWhiteSpace($svgSource)) {
|
|
||||||
$siblingPng = [System.IO.Path]::ChangeExtension($svgSource, '.png')
|
|
||||||
if (Test-Path -LiteralPath $siblingPng -PathType Leaf) {
|
|
||||||
$pngSource = $siblingPng
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$installed = $false
|
|
||||||
|
|
||||||
if (-not [string]::IsNullOrWhiteSpace($pngSource)) {
|
|
||||||
foreach ($size in @('48x48', '64x64', '128x128', '256x256')) {
|
|
||||||
$pngDir = Join-Path $HicolorRoot "$size\apps"
|
|
||||||
New-Item -ItemType Directory -Path $pngDir -Force | Out-Null
|
|
||||||
Copy-Item -LiteralPath $pngSource -Destination (Join-Path $pngDir "$AppId.png") -Force
|
|
||||||
}
|
|
||||||
|
|
||||||
$installed = $true
|
|
||||||
}
|
|
||||||
|
|
||||||
if (-not [string]::IsNullOrWhiteSpace($svgSource)) {
|
|
||||||
$svgDir = Join-Path $HicolorRoot 'scalable\apps'
|
|
||||||
New-Item -ItemType Directory -Path $svgDir -Force | Out-Null
|
|
||||||
Copy-Item -LiteralPath $svgSource -Destination (Join-Path $svgDir "$AppId.svg") -Force
|
|
||||||
$installed = $true
|
|
||||||
}
|
|
||||||
|
|
||||||
return $installed
|
|
||||||
}
|
|
||||||
|
|
||||||
function ConvertTo-FlatpakMetainfoDescriptionXml {
|
|
||||||
param(
|
|
||||||
[Parameter(Mandatory = $true)]
|
|
||||||
[string]$AppName,
|
|
||||||
|
|
||||||
[Parameter(Mandatory = $true)]
|
|
||||||
[AllowEmptyString()]
|
|
||||||
[string]$Summary,
|
|
||||||
|
|
||||||
[Parameter(Mandatory = $false)]
|
|
||||||
[AllowEmptyString()]
|
|
||||||
[string]$Description
|
|
||||||
)
|
|
||||||
|
|
||||||
$text = $Description
|
|
||||||
if ([string]::IsNullOrWhiteSpace($text)) {
|
|
||||||
$text = "$AppName is a desktop application. $Summary".Trim()
|
|
||||||
}
|
|
||||||
|
|
||||||
$paragraphs = [System.Collections.Generic.List[string]]::new()
|
|
||||||
foreach ($chunk in @($text -split '(\r?\n){2,}')) {
|
|
||||||
$trimmed = $chunk.Trim()
|
|
||||||
if ([string]::IsNullOrWhiteSpace($trimmed)) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
$paragraphs.Add(' <p>' + [System.Security.SecurityElement]::Escape($trimmed) + '</p>')
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($paragraphs.Count -eq 0) {
|
|
||||||
$paragraphs.Add(' <p>' + [System.Security.SecurityElement]::Escape("$AppName is a desktop application.") + '</p>')
|
|
||||||
}
|
|
||||||
|
|
||||||
return ($paragraphs -join "`n")
|
|
||||||
}
|
|
||||||
|
|
||||||
function New-FlatpakMetainfoXml {
|
function New-FlatpakMetainfoXml {
|
||||||
param(
|
param(
|
||||||
[Parameter(Mandatory = $true)]
|
[Parameter(Mandatory = $true)]
|
||||||
@ -648,124 +541,26 @@ function New-FlatpakMetainfoXml {
|
|||||||
[Parameter(Mandatory = $true)]
|
[Parameter(Mandatory = $true)]
|
||||||
[string]$Summary,
|
[string]$Summary,
|
||||||
|
|
||||||
[Parameter(Mandatory = $false)]
|
|
||||||
[AllowEmptyString()]
|
|
||||||
[string]$Description = '',
|
|
||||||
|
|
||||||
[Parameter(Mandatory = $false)]
|
[Parameter(Mandatory = $false)]
|
||||||
[string]$ProjectLicense = 'MIT',
|
[string]$ProjectLicense = 'MIT',
|
||||||
|
|
||||||
[Parameter(Mandatory = $false)]
|
[Parameter(Mandatory = $false)]
|
||||||
[string]$MetadataLicense = 'CC0-1.0',
|
[string]$MetadataLicense = 'CC0-1.0'
|
||||||
|
|
||||||
[Parameter(Mandatory = $false)]
|
|
||||||
[string]$Version = ''
|
|
||||||
)
|
)
|
||||||
|
|
||||||
$escapedName = [System.Security.SecurityElement]::Escape($AppName)
|
$escapedName = [System.Security.SecurityElement]::Escape($AppName)
|
||||||
$escapedSummary = [System.Security.SecurityElement]::Escape($Summary)
|
$escapedSummary = [System.Security.SecurityElement]::Escape($Summary)
|
||||||
$descriptionXml = ConvertTo-FlatpakMetainfoDescriptionXml -AppName $AppName -Summary $Summary -Description $Description
|
return @"
|
||||||
$xml = @"
|
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<component type="desktop-application">
|
<component type="desktop-application">
|
||||||
<id>$AppId</id>
|
<id>$AppId</id>
|
||||||
<name>$escapedName</name>
|
<name>$escapedName</name>
|
||||||
<summary>$escapedSummary</summary>
|
<summary>$escapedSummary</summary>
|
||||||
<description>
|
|
||||||
$descriptionXml
|
|
||||||
</description>
|
|
||||||
<launchable type="desktop-id">$AppId.desktop</launchable>
|
<launchable type="desktop-id">$AppId.desktop</launchable>
|
||||||
<metadata_license>$MetadataLicense</metadata_license>
|
<metadata_license>$MetadataLicense</metadata_license>
|
||||||
<project_license>$ProjectLicense</project_license>
|
<project_license>$ProjectLicense</project_license>
|
||||||
</component>
|
</component>
|
||||||
"@
|
"@
|
||||||
if ([string]::IsNullOrWhiteSpace($Version)) {
|
|
||||||
return $xml
|
|
||||||
}
|
|
||||||
|
|
||||||
return Set-FlatpakMetainfoRelease -XmlText $xml -Version $Version
|
|
||||||
}
|
|
||||||
|
|
||||||
function Set-FlatpakMetainfoRelease {
|
|
||||||
param(
|
|
||||||
[Parameter(Mandatory = $true)]
|
|
||||||
[string]$XmlText,
|
|
||||||
|
|
||||||
[Parameter(Mandatory = $true)]
|
|
||||||
[string]$Version,
|
|
||||||
|
|
||||||
[Parameter(Mandatory = $false)]
|
|
||||||
[string]$Date = ''
|
|
||||||
)
|
|
||||||
|
|
||||||
if ([string]::IsNullOrWhiteSpace($Version)) {
|
|
||||||
throw "Set-FlatpakMetainfoRelease requires Version."
|
|
||||||
}
|
|
||||||
|
|
||||||
if ([string]::IsNullOrWhiteSpace($Date)) {
|
|
||||||
$Date = [datetime]::UtcNow.ToString('yyyy-MM-dd')
|
|
||||||
}
|
|
||||||
|
|
||||||
$doc = New-Object System.Xml.XmlDocument
|
|
||||||
$doc.PreserveWhitespace = $false
|
|
||||||
$doc.LoadXml($XmlText)
|
|
||||||
$component = $doc.SelectSingleNode('/component')
|
|
||||||
if ($null -eq $component) {
|
|
||||||
throw "Flatpak metainfo has no <component> root."
|
|
||||||
}
|
|
||||||
|
|
||||||
$releases = $component.SelectSingleNode('releases')
|
|
||||||
if ($null -eq $releases) {
|
|
||||||
$releases = $doc.CreateElement('releases')
|
|
||||||
$null = $component.AppendChild($releases)
|
|
||||||
}
|
|
||||||
|
|
||||||
$match = $null
|
|
||||||
foreach ($node in @($releases.SelectNodes('release'))) {
|
|
||||||
if ([string]$node.GetAttribute('version') -eq $Version) {
|
|
||||||
$match = $node
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$first = $releases.SelectSingleNode('release')
|
|
||||||
if ($null -ne $match) {
|
|
||||||
$match.SetAttribute('date', $Date)
|
|
||||||
if (-not [object]::ReferenceEquals($match, $first)) {
|
|
||||||
$null = $releases.InsertBefore($match, $first)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
$release = $doc.CreateElement('release')
|
|
||||||
$release.SetAttribute('version', $Version)
|
|
||||||
$release.SetAttribute('date', $Date)
|
|
||||||
if ($null -eq $first) {
|
|
||||||
$null = $releases.AppendChild($release)
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
$null = $releases.InsertBefore($release, $first)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$settings = New-Object System.Xml.XmlWriterSettings
|
|
||||||
$settings.Indent = $true
|
|
||||||
$settings.OmitXmlDeclaration = $false
|
|
||||||
$settings.Encoding = [System.Text.UTF8Encoding]::new($false)
|
|
||||||
$stream = New-Object System.IO.MemoryStream
|
|
||||||
try {
|
|
||||||
$writer = [System.Xml.XmlWriter]::Create($stream, $settings)
|
|
||||||
try {
|
|
||||||
$doc.Save($writer)
|
|
||||||
}
|
|
||||||
finally {
|
|
||||||
$writer.Dispose()
|
|
||||||
}
|
|
||||||
|
|
||||||
return [System.Text.Encoding]::UTF8.GetString($stream.ToArray())
|
|
||||||
}
|
|
||||||
finally {
|
|
||||||
$stream.Dispose()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function New-FlatpakManifestObject {
|
function New-FlatpakManifestObject {
|
||||||
@ -802,8 +597,8 @@ function New-FlatpakManifestObject {
|
|||||||
if ($null -eq $BuildCommands -or $BuildCommands.Count -eq 0) {
|
if ($null -eq $BuildCommands -or $BuildCommands.Count -eq 0) {
|
||||||
$BuildCommands = @(
|
$BuildCommands = @(
|
||||||
'mkdir -p /app/lib /app/share',
|
'mkdir -p /app/lib /app/share',
|
||||||
'cp -a lib/. /app/lib/',
|
'cp -a lib /app/lib',
|
||||||
'cp -a share/. /app/share/',
|
'cp -a share /app/share',
|
||||||
"install -Dm755 bin/$Command /app/bin/$Command",
|
"install -Dm755 bin/$Command /app/bin/$Command",
|
||||||
"chmod +x /app/lib/$ModuleName/$Command"
|
"chmod +x /app/lib/$ModuleName/$Command"
|
||||||
)
|
)
|
||||||
@ -841,15 +636,7 @@ function New-FlatpakLaunchScript {
|
|||||||
[string]$ExecutableFileName
|
[string]$ExecutableFileName
|
||||||
)
|
)
|
||||||
|
|
||||||
$lib = "/app/lib/$ModuleName"
|
return "#!/bin/sh`nexec /app/lib/$ModuleName/$ExecutableFileName `"`$@`"`n"
|
||||||
return @"
|
|
||||||
#!/bin/sh
|
|
||||||
LIB='$lib'
|
|
||||||
if [ -f "`$LIB/flatpak-native.env" ]; then
|
|
||||||
. "`$LIB/flatpak-native.env"
|
|
||||||
fi
|
|
||||||
exec `$LIB/$ExecutableFileName "`$`@"
|
|
||||||
"@
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function Assert-FlatpakAppId {
|
function Assert-FlatpakAppId {
|
||||||
@ -1233,9 +1020,7 @@ Export-ModuleMember -Function `
|
|||||||
New-WixBundleXml, `
|
New-WixBundleXml, `
|
||||||
Get-DefaultFlatpakFinishArgs, `
|
Get-DefaultFlatpakFinishArgs, `
|
||||||
New-FlatpakDesktopEntry, `
|
New-FlatpakDesktopEntry, `
|
||||||
Copy-FlatpakHicolorIcons, `
|
|
||||||
New-FlatpakMetainfoXml, `
|
New-FlatpakMetainfoXml, `
|
||||||
Set-FlatpakMetainfoRelease, `
|
|
||||||
New-FlatpakManifestObject, `
|
New-FlatpakManifestObject, `
|
||||||
New-FlatpakLaunchScript, `
|
New-FlatpakLaunchScript, `
|
||||||
Assert-FlatpakAppId, `
|
Assert-FlatpakAppId, `
|
||||||
|
|||||||
@ -13,8 +13,6 @@
|
|||||||
(default Debian) on the Linux filesystem, then copies the bundle back.
|
(default Debian) on the Linux filesystem, then copies the bundle back.
|
||||||
Docker Desktop WSL distros are never used. whenBuilderMissing skip|fail
|
Docker Desktop WSL distros are never used. whenBuilderMissing skip|fail
|
||||||
applies when neither native nor WSL builder is available.
|
applies when neither native nor WSL builder is available.
|
||||||
AppStream <release version> and the bundle file name use the same shared
|
|
||||||
release version as zip/MSI (DotNetReleaseVersion: csproj or Directory.Build.props).
|
|
||||||
#>
|
#>
|
||||||
|
|
||||||
if (-not (Get-Command Import-PluginDependency -ErrorAction SilentlyContinue)) {
|
if (-not (Get-Command Import-PluginDependency -ErrorAction SilentlyContinue)) {
|
||||||
@ -36,37 +34,6 @@ function Get-FlatpakWhenBuilderMissing {
|
|||||||
return $value
|
return $value
|
||||||
}
|
}
|
||||||
|
|
||||||
function Resolve-FlatpakPackOptionalFile {
|
|
||||||
param(
|
|
||||||
[Parameter(Mandatory = $true)]
|
|
||||||
[string]$ScriptDir,
|
|
||||||
|
|
||||||
[Parameter(Mandatory = $false)]
|
|
||||||
[AllowEmptyString()]
|
|
||||||
[string]$Setting,
|
|
||||||
|
|
||||||
[Parameter(Mandatory = $true)]
|
|
||||||
[string]$Name
|
|
||||||
)
|
|
||||||
|
|
||||||
if ([string]::IsNullOrWhiteSpace($Setting)) {
|
|
||||||
return $null
|
|
||||||
}
|
|
||||||
|
|
||||||
$path = if ([System.IO.Path]::IsPathRooted($Setting)) {
|
|
||||||
$Setting
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
[System.IO.Path]::GetFullPath((Join-Path $ScriptDir $Setting))
|
|
||||||
}
|
|
||||||
|
|
||||||
if (-not (Test-Path -LiteralPath $path -PathType Leaf)) {
|
|
||||||
throw "FlatpakPack $Name not found: $path"
|
|
||||||
}
|
|
||||||
|
|
||||||
return $path
|
|
||||||
}
|
|
||||||
|
|
||||||
function Test-FlatpakBuilderMissingException {
|
function Test-FlatpakBuilderMissingException {
|
||||||
param(
|
param(
|
||||||
[Parameter(Mandatory = $true)]
|
[Parameter(Mandatory = $true)]
|
||||||
@ -221,9 +188,6 @@ function Invoke-Plugin {
|
|||||||
$scriptDir = $sharedSettings.scriptDir
|
$scriptDir = $sharedSettings.scriptDir
|
||||||
$version = [string]$sharedSettings.version
|
$version = [string]$sharedSettings.version
|
||||||
$artifactsDirectory = $sharedSettings.artifactsDirectory
|
$artifactsDirectory = $sharedSettings.artifactsDirectory
|
||||||
if ([string]::IsNullOrWhiteSpace($version)) {
|
|
||||||
throw "FlatpakPack requires a release version in the shared context (same as zip/MSI: DotNetReleaseVersion from csproj / Directory.Build.props)."
|
|
||||||
}
|
|
||||||
|
|
||||||
$appId = [string](Get-PluginPropertyValue -PluginSettings $pluginSettings -Name 'appId')
|
$appId = [string](Get-PluginPropertyValue -PluginSettings $pluginSettings -Name 'appId')
|
||||||
$appName = [string](Get-PluginPropertyValue -PluginSettings $pluginSettings -Name 'appName')
|
$appName = [string](Get-PluginPropertyValue -PluginSettings $pluginSettings -Name 'appName')
|
||||||
@ -242,13 +206,9 @@ function Invoke-Plugin {
|
|||||||
$runtimeIdentifier = [string](Get-PluginPropertyValue -PluginSettings $pluginSettings -Name 'runtimeIdentifier' -Default 'linux-x64')
|
$runtimeIdentifier = [string](Get-PluginPropertyValue -PluginSettings $pluginSettings -Name 'runtimeIdentifier' -Default 'linux-x64')
|
||||||
$publishDirSetting = [string](Get-PluginPropertyValue -PluginSettings $pluginSettings -Name 'publishDir')
|
$publishDirSetting = [string](Get-PluginPropertyValue -PluginSettings $pluginSettings -Name 'publishDir')
|
||||||
$iconSetting = [string](Get-PluginPropertyValue -PluginSettings $pluginSettings -Name 'iconPath')
|
$iconSetting = [string](Get-PluginPropertyValue -PluginSettings $pluginSettings -Name 'iconPath')
|
||||||
$svgIconSetting = [string](Get-PluginPropertyValue -PluginSettings $pluginSettings -Name 'svgIconPath')
|
|
||||||
$summary = [string](Get-PluginPropertyValue -PluginSettings $pluginSettings -Name 'summary' -Default $appName)
|
$summary = [string](Get-PluginPropertyValue -PluginSettings $pluginSettings -Name 'summary' -Default $appName)
|
||||||
$description = [string](Get-PluginPropertyValue -PluginSettings $pluginSettings -Name 'description' -Default '')
|
|
||||||
$projectLicense = [string](Get-PluginPropertyValue -PluginSettings $pluginSettings -Name 'projectLicense' -Default 'MIT')
|
$projectLicense = [string](Get-PluginPropertyValue -PluginSettings $pluginSettings -Name 'projectLicense' -Default 'MIT')
|
||||||
$categories = [string](Get-PluginPropertyValue -PluginSettings $pluginSettings -Name 'categories' -Default 'Utility;')
|
$categories = [string](Get-PluginPropertyValue -PluginSettings $pluginSettings -Name 'categories' -Default 'Utility;')
|
||||||
$metainfoSetting = [string](Get-PluginPropertyValue -PluginSettings $pluginSettings -Name 'metainfoPath' -Default '')
|
|
||||||
$desktopFileSetting = [string](Get-PluginPropertyValue -PluginSettings $pluginSettings -Name 'desktopPath' -Default '')
|
|
||||||
$runtime = [string](Get-PluginPropertyValue -PluginSettings $pluginSettings -Name 'runtime' -Default 'org.freedesktop.Platform')
|
$runtime = [string](Get-PluginPropertyValue -PluginSettings $pluginSettings -Name 'runtime' -Default 'org.freedesktop.Platform')
|
||||||
$runtimeVersion = [string](Get-PluginPropertyValue -PluginSettings $pluginSettings -Name 'runtimeVersion' -Default '24.08')
|
$runtimeVersion = [string](Get-PluginPropertyValue -PluginSettings $pluginSettings -Name 'runtimeVersion' -Default '24.08')
|
||||||
$sdk = [string](Get-PluginPropertyValue -PluginSettings $pluginSettings -Name 'sdk' -Default 'org.freedesktop.Sdk')
|
$sdk = [string](Get-PluginPropertyValue -PluginSettings $pluginSettings -Name 'sdk' -Default 'org.freedesktop.Sdk')
|
||||||
@ -304,8 +264,8 @@ function Invoke-Plugin {
|
|||||||
$binDir = Join-Path $filesRoot 'bin'
|
$binDir = Join-Path $filesRoot 'bin'
|
||||||
$shareAppDir = Join-Path $filesRoot 'share\applications'
|
$shareAppDir = Join-Path $filesRoot 'share\applications'
|
||||||
$shareMetaDir = Join-Path $filesRoot 'share\metainfo'
|
$shareMetaDir = Join-Path $filesRoot 'share\metainfo'
|
||||||
$hicolorRoot = Join-Path $filesRoot 'share\icons\hicolor'
|
$shareIconDir = Join-Path $filesRoot 'share\icons\hicolor\256x256\apps'
|
||||||
New-Item -ItemType Directory -Path $libDir, $binDir, $shareAppDir, $shareMetaDir, $hicolorRoot -Force | Out-Null
|
New-Item -ItemType Directory -Path $libDir, $binDir, $shareAppDir, $shareMetaDir, $shareIconDir -Force | Out-Null
|
||||||
|
|
||||||
Write-Log -Level "STEP" -Message "Staging Flatpak layout from $publishDirectory"
|
Write-Log -Level "STEP" -Message "Staging Flatpak layout from $publishDirectory"
|
||||||
Copy-Item -Path (Join-Path $publishDirectory '*') -Destination $libDir -Recurse -Force
|
Copy-Item -Path (Join-Path $publishDirectory '*') -Destination $libDir -Recurse -Force
|
||||||
@ -314,56 +274,20 @@ function Invoke-Plugin {
|
|||||||
$launchPath = Join-Path $binDir $command
|
$launchPath = Join-Path $binDir $command
|
||||||
[System.IO.File]::WriteAllText($launchPath, $launchScript.Replace("`r`n", "`n"), [System.Text.UTF8Encoding]::new($false))
|
[System.IO.File]::WriteAllText($launchPath, $launchScript.Replace("`r`n", "`n"), [System.Text.UTF8Encoding]::new($false))
|
||||||
|
|
||||||
$desktopDest = Join-Path $shareAppDir "$appId.desktop"
|
$desktopPath = Join-Path $shareAppDir "$appId.desktop"
|
||||||
$upstreamDesktop = Resolve-FlatpakPackOptionalFile -ScriptDir $scriptDir -Setting $desktopFileSetting -Name 'desktopPath'
|
|
||||||
if ($null -ne $upstreamDesktop) {
|
|
||||||
$desktopText = [System.IO.File]::ReadAllText($upstreamDesktop) -replace "`r`n", "`n" -replace "`r", "`n"
|
|
||||||
if (-not $desktopText.EndsWith("`n")) {
|
|
||||||
$desktopText += "`n"
|
|
||||||
}
|
|
||||||
|
|
||||||
[System.IO.File]::WriteAllText($desktopDest, $desktopText, [System.Text.UTF8Encoding]::new($false))
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
[System.IO.File]::WriteAllText(
|
[System.IO.File]::WriteAllText(
|
||||||
$desktopDest,
|
$desktopPath,
|
||||||
(New-FlatpakDesktopEntry -AppId $appId -AppName $appName -Command $command -Categories $categories).Replace("`r`n", "`n"),
|
(New-FlatpakDesktopEntry -AppId $appId -AppName $appName -Command $command -Categories $categories).Replace("`r`n", "`n"),
|
||||||
[System.Text.UTF8Encoding]::new($false)
|
[System.Text.UTF8Encoding]::new($false)
|
||||||
)
|
)
|
||||||
}
|
|
||||||
|
|
||||||
$metaDest = Join-Path $shareMetaDir "$appId.metainfo.xml"
|
$metaPath = Join-Path $shareMetaDir "$appId.metainfo.xml"
|
||||||
$upstreamMetainfo = Resolve-FlatpakPackOptionalFile -ScriptDir $scriptDir -Setting $metainfoSetting -Name 'metainfoPath'
|
|
||||||
if ($null -ne $upstreamMetainfo) {
|
|
||||||
$metaText = [System.IO.File]::ReadAllText($upstreamMetainfo) -replace "`r`n", "`n" -replace "`r", "`n"
|
|
||||||
if (-not $metaText.EndsWith("`n")) {
|
|
||||||
$metaText += "`n"
|
|
||||||
}
|
|
||||||
|
|
||||||
$metaText = $metaText.Replace('{version}', $version)
|
|
||||||
$metaText = Set-FlatpakMetainfoRelease -XmlText $metaText -Version $version
|
|
||||||
if (-not $metaText.EndsWith("`n")) {
|
|
||||||
$metaText += "`n"
|
|
||||||
}
|
|
||||||
|
|
||||||
[System.IO.File]::WriteAllText($metaDest, $metaText, [System.Text.UTF8Encoding]::new($false))
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
[System.IO.File]::WriteAllText(
|
[System.IO.File]::WriteAllText(
|
||||||
$metaDest,
|
$metaPath,
|
||||||
(New-FlatpakMetainfoXml -AppId $appId -AppName $appName -Summary $summary -Description $description -ProjectLicense $projectLicense -Version $version).Replace("`r`n", "`n"),
|
(New-FlatpakMetainfoXml -AppId $appId -AppName $appName -Summary $summary -ProjectLicense $projectLicense).Replace("`r`n", "`n"),
|
||||||
[System.Text.UTF8Encoding]::new($false)
|
[System.Text.UTF8Encoding]::new($false)
|
||||||
)
|
)
|
||||||
}
|
|
||||||
|
|
||||||
$stagedMeta = [System.IO.File]::ReadAllText($metaDest)
|
|
||||||
if ($stagedMeta -notmatch [regex]::Escape("version=`"$version`"")) {
|
|
||||||
throw "FlatpakPack failed to stamp AppStream release version '$version' into $metaDest."
|
|
||||||
}
|
|
||||||
|
|
||||||
Write-Log -Level "OK" -Message " AppStream release version: $version"
|
|
||||||
|
|
||||||
$iconPath = $null
|
|
||||||
if (-not [string]::IsNullOrWhiteSpace($iconSetting)) {
|
if (-not [string]::IsNullOrWhiteSpace($iconSetting)) {
|
||||||
$iconPath = if ([System.IO.Path]::IsPathRooted($iconSetting)) {
|
$iconPath = if ([System.IO.Path]::IsPathRooted($iconSetting)) {
|
||||||
$iconSetting
|
$iconSetting
|
||||||
@ -375,39 +299,21 @@ function Invoke-Plugin {
|
|||||||
if (-not (Test-Path -LiteralPath $iconPath -PathType Leaf)) {
|
if (-not (Test-Path -LiteralPath $iconPath -PathType Leaf)) {
|
||||||
throw "FlatpakPack iconPath not found: $iconPath"
|
throw "FlatpakPack iconPath not found: $iconPath"
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
$svgIconPath = $null
|
if ([System.IO.Path]::GetExtension($iconPath) -ine '.png') {
|
||||||
if (-not [string]::IsNullOrWhiteSpace($svgIconSetting)) {
|
Write-Log -Level "WARN" -Message " Flatpak icon should be PNG; skipping copy of $iconPath"
|
||||||
$svgIconPath = if ([System.IO.Path]::IsPathRooted($svgIconSetting)) {
|
|
||||||
$svgIconSetting
|
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
[System.IO.Path]::GetFullPath((Join-Path $scriptDir $svgIconSetting))
|
Copy-Item -LiteralPath $iconPath -Destination (Join-Path $shareIconDir "$appId.png") -Force
|
||||||
}
|
}
|
||||||
|
|
||||||
if (-not (Test-Path -LiteralPath $svgIconPath -PathType Leaf)) {
|
|
||||||
throw "FlatpakPack svgIconPath not found: $svgIconPath"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$iconInstalled = Copy-FlatpakHicolorIcons `
|
|
||||||
-AppId $appId `
|
|
||||||
-HicolorRoot $hicolorRoot `
|
|
||||||
-IconPath $iconPath `
|
|
||||||
-SvgIconPath $svgIconPath
|
|
||||||
|
|
||||||
if (-not $iconInstalled -and -not [string]::IsNullOrWhiteSpace($iconPath)) {
|
|
||||||
Write-Log -Level "WARN" -Message " Flatpak icon should be PNG or SVG; skipping copy of $iconPath"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$buildCommands = @(
|
$buildCommands = @(
|
||||||
'mkdir -p /app/lib /app/share',
|
'mkdir -p /app/lib /app/share',
|
||||||
"cp -a lib/$moduleName /app/lib/$moduleName",
|
"cp -a lib/$moduleName /app/lib/$moduleName",
|
||||||
'cp -a share/. /app/share/',
|
'cp -a share /app/share',
|
||||||
"install -Dm755 bin/$command /app/bin/$command",
|
"install -Dm755 bin/$command /app/bin/$command",
|
||||||
"chmod +x /app/lib/$moduleName/$executableFileName",
|
"chmod +x /app/lib/$moduleName/$executableFileName"
|
||||||
'gtk-update-icon-cache -f /app/share/icons/hicolor >/dev/null 2>&1 || true'
|
|
||||||
)
|
)
|
||||||
|
|
||||||
$manifest = New-FlatpakManifestObject `
|
$manifest = New-FlatpakManifestObject `
|
||||||
|
|||||||
@ -26,25 +26,17 @@
|
|||||||
<Label X="185" Y="11" Width="-11" Height="32" FontId="1" DisablePrefix="yes">#(loc.Title)</Label>
|
<Label X="185" Y="11" Width="-11" Height="32" FontId="1" DisablePrefix="yes">#(loc.Title)</Label>
|
||||||
<ImageControl X="11" Y="11" Width="165" Height="400" ImageFile="logoside.png"/>
|
<ImageControl X="11" Y="11" Width="165" Height="400" ImageFile="logoside.png"/>
|
||||||
<Label X="185" Y="50" Width="-11" Height="32" FontId="2" DisablePrefix="yes">#(loc.InstallHeader)</Label>
|
<Label X="185" Y="50" Width="-11" Height="32" FontId="2" DisablePrefix="yes">#(loc.InstallHeader)</Label>
|
||||||
<Label X="185" Y="91" Width="-11" Height="24" FontId="3" DisablePrefix="yes">
|
<Label X="185" Y="91" Width="-11" Height="40" FontId="3" DisablePrefix="yes">
|
||||||
<Text Condition="WixStdBASuppressOptionsUI">#(loc.InstallMessage)</Text>
|
<Text Condition="WixStdBASuppressOptionsUI">#(loc.InstallMessage)</Text>
|
||||||
<Text Condition="NOT WixStdBASuppressOptionsUI">#(loc.InstallMessageOptions)</Text>
|
<Text Condition="NOT WixStdBASuppressOptionsUI">#(loc.InstallMessageOptions)</Text>
|
||||||
</Label>
|
</Label>
|
||||||
<RadioButtons Name="InstallLayout">
|
<Label X="185" Y="138" Width="-11" Height="17" FontId="3">#(loc.OptionsLocationLabel)</Label>
|
||||||
<RadioButton X="185" Y="116" Height="18" Width="-11" Value="standard" TabStop="yes" FontId="3">Standard — Program Files, data in C:\MaksIT</RadioButton>
|
<Editbox Name="InstallFolder" X="185" Y="160" Width="-91" Height="21" TabStop="yes" FontId="3" FileSystemAutoComplete="yes" />
|
||||||
<RadioButton X="185" Y="134" Height="18" Width="-11" Value="portable" TabStop="yes" FontId="3">Portable — everything in one folder</RadioButton>
|
<Button Name="InstallFolderBrowseButton" X="-11" Y="159" Width="75" Height="23" TabStop="yes" FontId="3">
|
||||||
</RadioButtons>
|
|
||||||
<Label X="185" Y="156" Width="-11" Height="17" FontId="3">
|
|
||||||
<Text Condition="InstallLayout = "portable"">Folder for program, scripts, logs, and settings</Text>
|
|
||||||
<Text Condition="InstallLayout <> "portable"">#(loc.OptionsLocationLabel)</Text>
|
|
||||||
</Label>
|
|
||||||
<Editbox Name="InstallFolder" X="185" Y="174" Width="-91" Height="21" TabStop="yes" FontId="3" FileSystemAutoComplete="yes" />
|
|
||||||
<Button Name="InstallFolderBrowseButton" X="-11" Y="173" Width="75" Height="23" TabStop="yes" FontId="3">
|
|
||||||
<Text>#(loc.OptionsBrowseButton)</Text>
|
<Text>#(loc.OptionsBrowseButton)</Text>
|
||||||
<BrowseDirectoryAction VariableName="InstallFolder" />
|
<BrowseDirectoryAction VariableName="InstallFolder" />
|
||||||
</Button>
|
</Button>
|
||||||
<Checkbox Name="InstallDesktopShortcut" X="185" Y="200" Width="-11" Height="17" TabStop="yes" FontId="3">Place a shortcut on the desktop</Checkbox>
|
<Checkbox Name="InstallDesktopShortcut" X="185" Y="192" Width="-11" Height="17" TabStop="yes" FontId="3">Place a shortcut on the desktop</Checkbox>
|
||||||
<Label X="185" Y="220" Width="-11" Height="32" FontId="3" DisablePrefix="yes" VisibleCondition="InstallLayout = "portable"">Scripts, logs, and settings stay in this folder (Scripts, Logs, and Data subfolders). Pick a writable location if you do not want Program Files.</Label>
|
|
||||||
<Hypertext Name="EulaHyperlink" X="185" Y="-111" Width="-11" Height="17" TabStop="yes" FontId="3" HideWhenDisabled="yes">#(loc.InstallLicenseLinkText)</Hypertext>
|
<Hypertext Name="EulaHyperlink" X="185" Y="-111" Width="-11" Height="17" TabStop="yes" FontId="3" HideWhenDisabled="yes">#(loc.InstallLicenseLinkText)</Hypertext>
|
||||||
<Label Name="InstallVersion" X="185" Y="-81" Width="-11" Height="17" FontId="3" DisablePrefix="yes" VisibleCondition="WixStdBAShowVersion">#(loc.InstallVersion)</Label>
|
<Label Name="InstallVersion" X="185" Y="-81" Width="-11" Height="17" FontId="3" DisablePrefix="yes" VisibleCondition="WixStdBAShowVersion">#(loc.InstallVersion)</Label>
|
||||||
<Checkbox Name="EulaAcceptCheckbox" X="185" Y="-51" Width="-11" Height="17" TabStop="yes" FontId="3" HideWhenDisabled="yes">#(loc.InstallAcceptCheckbox)</Checkbox>
|
<Checkbox Name="EulaAcceptCheckbox" X="185" Y="-51" Width="-11" Height="17" TabStop="yes" FontId="3" HideWhenDisabled="yes">#(loc.InstallAcceptCheckbox)</Checkbox>
|
||||||
|
|||||||
@ -1,139 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<!-- Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information. -->
|
|
||||||
|
|
||||||
|
|
||||||
<Theme xmlns="http://wixtoolset.org/schemas/v4/thmutil">
|
|
||||||
<Font Id="0" Height="-12" Weight="500" Foreground="windowtext" Background="window">Segoe UI</Font>
|
|
||||||
<Font Id="1" Height="-24" Weight="500" Foreground="windowtext">Segoe UI</Font>
|
|
||||||
<Font Id="2" Height="-22" Weight="500" Foreground="graytext">Segoe UI</Font>
|
|
||||||
<Font Id="3" Height="-12" Weight="500" Foreground="windowtext" Background="window">Segoe UI</Font>
|
|
||||||
|
|
||||||
<Window Width="600" Height="450" HexStyle="100a0000" FontId="0" Caption="#(loc.Caption)">
|
|
||||||
<Page Name="Help">
|
|
||||||
<Label X="80" Y="11" Width="-11" Height="32" FontId="1" DisablePrefix="yes">#(loc.Title)</Label>
|
|
||||||
<ImageControl X="11" Y="11" Width="64" Height="64" ImageFile="logo.png"/>
|
|
||||||
<Label X="11" Y="80" Width="-11" Height="32" FontId="2" DisablePrefix="yes">#(loc.HelpHeader)</Label>
|
|
||||||
<Label X="11" Y="121" Width="-11" Height="-35" FontId="3" DisablePrefix="yes">#(loc.HelpText)</Label>
|
|
||||||
<Button Name="HelpCloseButton" X="-11" Y="-11" Width="75" Height="23" TabStop="yes" FontId="0">
|
|
||||||
<Text>#(loc.HelpCloseButton)</Text>
|
|
||||||
<CloseWindowAction />
|
|
||||||
</Button>
|
|
||||||
</Page>
|
|
||||||
<Page Name="Loading">
|
|
||||||
<Label X="185" Y="50" Width="-11" Height="30" FontId="2" DisablePrefix="yes" Visible="no" Name="CheckingForUpdatesLabel" />
|
|
||||||
</Page>
|
|
||||||
<Page Name="Install">
|
|
||||||
<Label X="185" Y="11" Width="-11" Height="32" FontId="1" DisablePrefix="yes">#(loc.Title)</Label>
|
|
||||||
<ImageControl X="11" Y="11" Width="165" Height="400" ImageFile="logoside.png"/>
|
|
||||||
<Label X="185" Y="50" Width="-11" Height="32" FontId="2" DisablePrefix="yes">#(loc.InstallHeader)</Label>
|
|
||||||
<Label X="185" Y="91" Width="-11" Height="40" FontId="3" DisablePrefix="yes">
|
|
||||||
<Text Condition="WixStdBASuppressOptionsUI">#(loc.InstallMessage)</Text>
|
|
||||||
<Text Condition="NOT WixStdBASuppressOptionsUI">#(loc.InstallMessageOptions)</Text>
|
|
||||||
</Label>
|
|
||||||
<Label X="185" Y="138" Width="-11" Height="17" FontId="3">#(loc.OptionsLocationLabel)</Label>
|
|
||||||
<Editbox Name="InstallFolder" X="185" Y="160" Width="-91" Height="21" TabStop="yes" FontId="3" FileSystemAutoComplete="yes" />
|
|
||||||
<Button Name="InstallFolderBrowseButton" X="-11" Y="159" Width="75" Height="23" TabStop="yes" FontId="3">
|
|
||||||
<Text>#(loc.OptionsBrowseButton)</Text>
|
|
||||||
<BrowseDirectoryAction VariableName="InstallFolder" />
|
|
||||||
</Button>
|
|
||||||
<Checkbox Name="InstallDesktopShortcut" X="185" Y="192" Width="-11" Height="17" TabStop="yes" FontId="3">Place a shortcut on the desktop</Checkbox>
|
|
||||||
<Hypertext Name="EulaHyperlink" X="185" Y="-111" Width="-11" Height="17" TabStop="yes" FontId="3" HideWhenDisabled="yes">#(loc.InstallLicenseLinkText)</Hypertext>
|
|
||||||
<Label Name="InstallVersion" X="185" Y="-81" Width="-11" Height="17" FontId="3" DisablePrefix="yes" VisibleCondition="WixStdBAShowVersion">#(loc.InstallVersion)</Label>
|
|
||||||
<Checkbox Name="EulaAcceptCheckbox" X="185" Y="-51" Width="-11" Height="17" TabStop="yes" FontId="3" HideWhenDisabled="yes">#(loc.InstallAcceptCheckbox)</Checkbox>
|
|
||||||
<Button Name="InstallUpdateButton" X="11" Y="-11" Width="200" Height="23" TabStop="yes" FontId="0" EnableCondition="WixStdBAUpdateAvailable" HideWhenDisabled="yes">#(loc.UpdateButton)</Button>
|
|
||||||
<Button Name="OptionsButton" X="-171" Y="-11" Width="75" Height="23" TabStop="yes" FontId="0" VisibleCondition="NOT WixStdBASuppressOptionsUI">
|
|
||||||
<Text>#(loc.InstallOptionsButton)</Text>
|
|
||||||
<ChangePageAction Page="Options" />
|
|
||||||
</Button>
|
|
||||||
<Button Name="InstallButton" X="-91" Y="-11" Width="75" Height="23" TabStop="yes" FontId="0" ElevationShieldCondition='(WixBundleAuthoredScope = 1 AND NOT WixBundleElevated) OR ((WixBundleAuthoredScope = 2 OR WixBundleAuthoredScope = 3) AND WixStdBAScope = "PerMachine")'>#(loc.InstallInstallButton)</Button>
|
|
||||||
<Button Name="InstallCancelButton" X="-11" Y="-11" Width="75" Height="23" TabStop="yes" FontId="0">
|
|
||||||
<Text>#(loc.InstallCancelButton)</Text>
|
|
||||||
<CloseWindowAction />
|
|
||||||
</Button>
|
|
||||||
</Page>
|
|
||||||
<Page Name="Options">
|
|
||||||
<Label X="80" Y="11" Width="-11" Height="32" FontId="1" DisablePrefix="yes">#(loc.Title)</Label>
|
|
||||||
<ImageControl X="11" Y="11" Width="64" Height="64" ImageFile="logo.png"/>
|
|
||||||
<Label X="11" Y="80" Width="-11" Height="30" FontId="2" DisablePrefix="yes">#(loc.OptionsHeader)</Label>
|
|
||||||
<RadioButtons Name="WixStdBAScope">
|
|
||||||
<RadioButton X="11" Y="180" Height="20" Width="-11" Value="PerUser" EnableCondition="WixBundleAuthoredScope = 2 OR WixBundleAuthoredScope = 3" HideWhenDisabled="yes" FontId="3">#(loc.OptionsPerUserScopeText)</RadioButton>
|
|
||||||
<RadioButton X="11" Y="200" Height="20" Width="-11" Value="PerMachine" EnableCondition="WixBundleAuthoredScope = 2 OR WixBundleAuthoredScope = 3" HideWhenDisabled="yes" FontId="3">#(loc.OptionsPerMachineScopeText)</RadioButton>
|
|
||||||
</RadioButtons>
|
|
||||||
<Button Name="OptionsOkButton" X="-91" Y="-11" Width="75" Height="23" TabStop="yes" FontId="0">
|
|
||||||
<Text>#(loc.OptionsOkButton)</Text>
|
|
||||||
<ChangePageAction Page="Install" />
|
|
||||||
</Button>
|
|
||||||
<Button Name="OptionsCancelButton" X="-11" Y="-11" Width="75" Height="23" TabStop="yes" FontId="0">
|
|
||||||
<Text>#(loc.OptionsCancelButton)</Text>
|
|
||||||
<ChangePageAction Page="Install" Cancel="yes" />
|
|
||||||
</Button>
|
|
||||||
</Page>
|
|
||||||
<Page Name="Progress">
|
|
||||||
<Label X="80" Y="11" Width="-11" Height="32" FontId="1" DisablePrefix="yes">#(loc.Title)</Label>
|
|
||||||
<ImageControl X="11" Y="11" Width="64" Height="64" ImageFile="logo.png"/>
|
|
||||||
<Label X="11" Y="80" Width="-11" Height="30" FontId="2" DisablePrefix="yes">#(loc.ProgressHeader)</Label>
|
|
||||||
<Label X="11" Y="141" Width="70" Height="17" FontId="3" DisablePrefix="yes">#(loc.ProgressLabel)</Label>
|
|
||||||
<Label Name="OverallProgressPackageText" X="85" Y="141" Width="-11" Height="17" FontId="3" DisablePrefix="yes">#(loc.OverallProgressPackageText)</Label>
|
|
||||||
<Progressbar Name="OverallCalculatedProgressbar" X="11" Y="163" Width="-11" Height="20" />
|
|
||||||
<Button Name="ProgressCancelButton" X="-11" Y="-11" Width="75" Height="23" TabStop="yes" FontId="0">#(loc.ProgressCancelButton)</Button>
|
|
||||||
</Page>
|
|
||||||
<Page Name="Modify">
|
|
||||||
<Label X="185" Y="11" Width="-11" Height="32" FontId="1" DisablePrefix="yes">#(loc.Title)</Label>
|
|
||||||
<ImageControl X="11" Y="11" Width="165" Height="400" ImageFile="logoside.png"/>
|
|
||||||
<Label X="185" Y="50" Width="-11" Height="30" FontId="2" DisablePrefix="yes">#(loc.ModifyHeader)</Label>
|
|
||||||
<Button Name="ModifyUpdateButton" X="11" Y="-11" Width="200" Height="23" TabStop="yes" FontId="0" EnableCondition="WixStdBAUpdateAvailable" HideWhenDisabled="yes">#(loc.UpdateButton)</Button>
|
|
||||||
<Button Name="RepairButton" X="-171" Y="-11" Width="75" Height="23" TabStop="yes" FontId="0" HideWhenDisabled="yes" ElevationShieldCondition='(WixBundleAuthoredScope = 1 AND NOT WixBundleElevated) OR ((WixBundleAuthoredScope = 2 OR WixBundleAuthoredScope = 3) AND WixStdBAScope = "PerMachine")'>#(loc.ModifyRepairButton)</Button>
|
|
||||||
<Button Name="UninstallButton" X="-91" Y="-11" Width="75" Height="23" TabStop="yes" FontId="0" ElevationShieldCondition='(WixBundleAuthoredScope = 1 AND NOT WixBundleElevated) OR ((WixBundleAuthoredScope = 2 OR WixBundleAuthoredScope = 3) AND WixStdBAScope = "PerMachine")'>#(loc.ModifyUninstallButton)</Button>
|
|
||||||
<Button Name="ModifyCancelButton" X="-11" Y="-11" Width="75" Height="23" TabStop="yes" FontId="0">
|
|
||||||
<Text>#(loc.ModifyCancelButton)</Text>
|
|
||||||
<CloseWindowAction />
|
|
||||||
</Button>
|
|
||||||
</Page>
|
|
||||||
<Page Name="Success">
|
|
||||||
<Label X="185" Y="11" Width="-11" Height="32" FontId="1" DisablePrefix="yes">#(loc.Title)</Label>
|
|
||||||
<ImageControl X="11" Y="11" Width="165" Height="400" ImageFile="logoside.png"/>
|
|
||||||
<Label X="185" Y="50" Width="-11" Height="30" FontId="2" DisablePrefix="yes">
|
|
||||||
<Text>#(loc.SuccessHeader)</Text>
|
|
||||||
<Text Condition="WixBundleAction = 2">#(loc.SuccessLayoutHeader)</Text>
|
|
||||||
<Text Condition="WixBundleAction = 3">#(loc.SuccessUnsafeUninstallHeader)</Text>
|
|
||||||
<Text Condition="WixBundleAction = 4">#(loc.SuccessUninstallHeader)</Text>
|
|
||||||
<Text Condition="WixBundleAction = 5">#(loc.SuccessCacheHeader)</Text>
|
|
||||||
<Text Condition="WixBundleAction = 6">#(loc.SuccessInstallHeader)</Text>
|
|
||||||
<Text Condition="WixBundleAction = 7">#(loc.SuccessModifyHeader)</Text>
|
|
||||||
<Text Condition="WixBundleAction = 8">#(loc.SuccessRepairHeader)</Text>
|
|
||||||
</Label>
|
|
||||||
<Button Name="LaunchButton" X="-91" Y="-11" Width="75" Height="23" TabStop="yes" FontId="0" HideWhenDisabled="yes">#(loc.SuccessLaunchButton)</Button>
|
|
||||||
<Label X="185" Y="-51" Width="400" Height="34" FontId="3" DisablePrefix="yes" VisibleCondition="WixStdBARestartRequired">
|
|
||||||
<Text>#(loc.SuccessRestartText)</Text>
|
|
||||||
<Text Condition="WixBundleAction = 3">#(loc.SuccessUninstallRestartText)</Text>
|
|
||||||
</Label>
|
|
||||||
<Button Name="SuccessRestartButton" X="-91" Y="-11" Width="75" Height="23" TabStop="yes" FontId="0" HideWhenDisabled="yes">#(loc.SuccessRestartButton)</Button>
|
|
||||||
<Button Name="SuccessCloseButton" X="-11" Y="-11" Width="75" Height="23" TabStop="yes" FontId="0">
|
|
||||||
<Text>#(loc.SuccessCloseButton)</Text>
|
|
||||||
<CloseWindowAction />
|
|
||||||
</Button>
|
|
||||||
</Page>
|
|
||||||
<Page Name="Failure">
|
|
||||||
<Label X="185" Y="11" Width="-11" Height="32" FontId="1" DisablePrefix="yes">#(loc.Title)</Label>
|
|
||||||
<ImageControl X="11" Y="11" Width="165" Height="400" ImageFile="logoside.png"/>
|
|
||||||
<Label X="185" Y="50" Width="-11" Height="30" FontId="2" DisablePrefix="yes">
|
|
||||||
<Text>#(loc.FailureHeader)</Text>
|
|
||||||
<Text Condition="WixBundleAction = 2">#(loc.FailureLayoutHeader)</Text>
|
|
||||||
<Text Condition="WixBundleAction = 3">#(loc.FailureUnsafeUninstallHeader)</Text>
|
|
||||||
<Text Condition="WixBundleAction = 4">#(loc.FailureUninstallHeader)</Text>
|
|
||||||
<Text Condition="WixBundleAction = 5">#(loc.FailureCacheHeader)</Text>
|
|
||||||
<Text Condition="WixBundleAction = 6">#(loc.FailureInstallHeader)</Text>
|
|
||||||
<Text Condition="WixBundleAction = 7">#(loc.FailureModifyHeader)</Text>
|
|
||||||
<Text Condition="WixBundleAction = 8">#(loc.FailureRepairHeader)</Text>
|
|
||||||
</Label>
|
|
||||||
<Hypertext Name="FailureLogFileLink" X="185" Y="121" Width="-11" Height="68" FontId="3" TabStop="yes" HideWhenDisabled="yes">#(loc.FailureHyperlinkLogText)</Hypertext>
|
|
||||||
<Hypertext Name="FailureMessageText" X="185" Y="-115" Width="-11" Height="80" FontId="3" TabStop="yes" HideWhenDisabled="yes" />
|
|
||||||
<Label X="185" Y="-57" Width="-11" Height="80" FontId="3" DisablePrefix="yes" VisibleCondition="WixStdBARestartRequired">#(loc.FailureRestartText)</Label>
|
|
||||||
<Button Name="FailureRestartButton" X="-91" Y="-11" Width="75" Height="23" TabStop="yes" FontId="0" HideWhenDisabled="yes">#(loc.FailureRestartButton)</Button>
|
|
||||||
<Button Name="FailureCloseButton" X="-11" Y="-11" Width="75" Height="23" TabStop="yes" FontId="0">
|
|
||||||
<Text>#(loc.FailureCloseButton)</Text>
|
|
||||||
<CloseWindowAction />
|
|
||||||
</Button>
|
|
||||||
</Page>
|
|
||||||
</Window>
|
|
||||||
</Theme>
|
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 2.1 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 23 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 21 KiB |
@ -1,27 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" role="img" aria-label="MAKS.IT">
|
|
||||||
<defs>
|
|
||||||
<linearGradient id="hi" x1="0" y1="0" x2="1" y2="1">
|
|
||||||
<stop offset="0" stop-color="#33A5CF"/>
|
|
||||||
<stop offset="1" stop-color="#006199"/>
|
|
||||||
</linearGradient>
|
|
||||||
<linearGradient id="mid" x1="0.2" y1="0" x2="0.9" y2="1">
|
|
||||||
<stop offset="0" stop-color="#0795C5"/>
|
|
||||||
<stop offset="1" stop-color="#003D7A"/>
|
|
||||||
</linearGradient>
|
|
||||||
<linearGradient id="deep" x1="0" y1="0" x2="0" y2="1">
|
|
||||||
<stop offset="0" stop-color="#00427E"/>
|
|
||||||
<stop offset="1" stop-color="#002A6A"/>
|
|
||||||
</linearGradient>
|
|
||||||
<linearGradient id="navy" x1="1" y1="0" x2="0" y2="1">
|
|
||||||
<stop offset="0" stop-color="#00306F"/>
|
|
||||||
<stop offset="1" stop-color="#001428"/>
|
|
||||||
</linearGradient>
|
|
||||||
</defs>
|
|
||||||
<!-- Origami M: sharp facets, light from upper-left. No wordmark. -->
|
|
||||||
<polygon fill="url(#hi)" points="28,40 78,40 62,216 12,216"/>
|
|
||||||
<polygon fill="url(#navy)" points="78,40 118,128 102,216 62,216"/>
|
|
||||||
<polygon fill="url(#mid)" points="118,128 158,40 174,216 102,216"/>
|
|
||||||
<polygon fill="url(#hi)" points="158,40 228,40 244,216 174,216"/>
|
|
||||||
<polygon fill="url(#deep)" points="28,40 78,40 70,128 20,128"/>
|
|
||||||
<polygon fill="url(#navy)" points="158,40 228,40 220,128 166,128"/>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 1.3 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 16 KiB |
Loading…
Reference in New Issue
Block a user