mirror of
https://github.com/MAKS-IT-COM/uscheduler.git
synced 2026-09-29 20:48: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]
|
||||
|
||||
## [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
|
||||
|
||||
### Added
|
||||
|
||||
@ -173,8 +173,4 @@ Follow [Keep a Changelog](https://keepachangelog.com/) format:
|
||||
- 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
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
MIT License
|
||||
|
||||
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,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
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.
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
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**.
|
||||
@ -57,7 +57,7 @@ See [CONTRIBUTING.md](CONTRIBUTING.md) for development setup, commit format, and
|
||||
|
||||
## 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
|
||||
- [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
|
||||
|
||||
The Windows setup exe lets you choose **Standard** or **Portable**.
|
||||
|
||||
**Standard** (default):
|
||||
|
||||
| Location | Purpose | Who can write |
|
||||
|----------|---------|----------------|
|
||||
| `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) |
|
||||
| `%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 |
|
||||
|
||||
**Portable** (everything in one folder):
|
||||
|
||||
| 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.
|
||||
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.
|
||||
|
||||
### Using CLI Commands
|
||||
|
||||
@ -139,9 +123,6 @@ MaksIT.UScheduler --help
|
||||
|
||||
# Create data folders and ACLs without installing the service
|
||||
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`.
|
||||
@ -153,8 +134,7 @@ On Windows the file is `MaksIT.UScheduler.exe`.
|
||||
| `--start` | | Start the service |
|
||||
| `--stop` | | Stop the service |
|
||||
| `--status` | | Query service status |
|
||||
| `--prepare-data` | | Create scripts, logs, and shared settings (elevated) |
|
||||
| `--portable` | | With `--install` / `--prepare-data`: keep data in the install folder |
|
||||
| `--prepare-data` | | Create `C:\MaksIT\Scripts`, `C:\MaksIT\Logs`, and shared settings (elevated) |
|
||||
| `--help` | `-h` | Show help message |
|
||||
|
||||
> **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
|
||||
|
||||
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.
|
||||
|
||||
#### 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/).
|
||||
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.
|
||||
|
||||

|
||||
|
||||
> **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
|
||||
|
||||
@ -230,12 +186,12 @@ The Settings view is your starting point for configuring UScheduler.
|
||||
|
||||
| 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) |
|
||||
| **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) |
|
||||
| **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
|
||||
|
||||
@ -306,9 +262,9 @@ Features:
|
||||
|
||||
### 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
|
||||
{
|
||||
@ -482,7 +438,7 @@ Invoke-ScheduledExecution -Config $Config -Automated:$Automated -CurrentDateTime
|
||||
1. Copy template
|
||||
2. Modify `$Config`
|
||||
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.
|
||||
|
||||
@ -559,6 +515,6 @@ Email: maksym.sadovnychyy@gmail.com
|
||||
|
||||
## 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>
|
||||
/// Root configuration class for the UScheduler service.
|
||||
/// Stored in machine-wide settings.json (ProgramData), or under the install
|
||||
/// folder when a portable layout is used.
|
||||
/// Stored in machine-wide settings.json (ProgramData), not next to the exe.
|
||||
/// </summary>
|
||||
public class Configuration {
|
||||
/// <summary>
|
||||
@ -104,17 +103,12 @@ public class Configuration {
|
||||
Processes ??= [];
|
||||
}
|
||||
|
||||
public string GetEffectiveScriptsDirectory(string? baseDirectory = null) =>
|
||||
HostPaths.ResolveScriptsDirectory(ScriptsDir, baseDirectory);
|
||||
public string GetEffectiveScriptsDirectory() =>
|
||||
HostPaths.ResolveScriptsDirectory(ScriptsDir);
|
||||
|
||||
public string GetEffectiveLogDirectory(string? baseDirectory = null) {
|
||||
var start = baseDirectory ?? AppContext.BaseDirectory;
|
||||
if (string.IsNullOrWhiteSpace(LogDir)) {
|
||||
var root = HostPaths.FindPortableRoot(start);
|
||||
return root is null ? HostPaths.DefaultLogDirectory : HostPaths.GetPortableLogDirectory(root);
|
||||
}
|
||||
|
||||
return PathHelper.ResolvePath(LogDir, start);
|
||||
var logDir = string.IsNullOrWhiteSpace(LogDir) ? HostPaths.DefaultLogDirectory : LogDir;
|
||||
return PathHelper.ResolvePath(logDir, baseDirectory ?? AppContext.BaseDirectory);
|
||||
}
|
||||
|
||||
public string ResolveScriptPath(string path) =>
|
||||
|
||||
@ -6,10 +6,8 @@ namespace MaksIT.UScheduler.Shared;
|
||||
|
||||
/// <summary>
|
||||
/// Loads and saves machine-wide <c>Configuration</c> under ProgramData
|
||||
/// (all-users AppData), or under the portable Data folder when a
|
||||
/// <see cref="HostPaths.PortableMarkerFileName"/> file is present.
|
||||
/// Shipped <c>appsettings.json</c> next to the worker is seed-only; host
|
||||
/// logging stays in that file and is never copied.
|
||||
/// (all-users AppData). Shipped <c>appsettings.json</c> next to the worker is
|
||||
/// seed-only; host logging stays in that file and is never copied.
|
||||
/// </summary>
|
||||
public sealed class ConfigurationFileService {
|
||||
public const string ProductFolder = HostPaths.ProductFolder;
|
||||
@ -29,7 +27,7 @@ public sealed class ConfigurationFileService {
|
||||
|
||||
public ConfigurationFileService(string? configurationPath = null, string? seedPath = null) {
|
||||
FilePath = string.IsNullOrWhiteSpace(configurationPath)
|
||||
? HostPaths.ResolveSharedSettingsFile()
|
||||
? HostPaths.SharedSettingsFile
|
||||
: configurationPath;
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(seedPath))
|
||||
@ -95,7 +93,6 @@ public sealed class ConfigurationFileService {
|
||||
return;
|
||||
|
||||
try {
|
||||
HostPaths.ApplyPortableDefaults(_current, Path.GetDirectoryName(FilePath));
|
||||
Save(_current);
|
||||
}
|
||||
catch (UnauthorizedAccessException) {
|
||||
|
||||
@ -4,115 +4,53 @@ using System.Runtime.Versioning;
|
||||
|
||||
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>
|
||||
/// Creates machine-wide data folders (scripts, logs, shared settings) and
|
||||
/// 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.
|
||||
/// Pass <c>--portable</c> (or drop a <see cref="HostPaths.PortableMarkerFileName"/>
|
||||
/// file) to keep scripts, logs, and settings under the install folder.
|
||||
/// </summary>
|
||||
public static class HostDataDirectories {
|
||||
/// <summary>Windows Built-in Users (locale-independent).</summary>
|
||||
private const string BuiltinUsersSid = "*S-1-5-32-545";
|
||||
|
||||
public static HostDataLayout ResolveLayout(string? installDirectory = null, bool portableRequested = false) {
|
||||
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) {
|
||||
public static HostServiceOperationResult Prepare(string? installDirectory = null) {
|
||||
var installDir = string.IsNullOrWhiteSpace(installDirectory)
|
||||
? AppContext.BaseDirectory
|
||||
: installDirectory;
|
||||
var messages = new List<string>();
|
||||
|
||||
try {
|
||||
if (portable)
|
||||
HostPaths.WritePortableMarker(Path.GetFullPath(installDir));
|
||||
Directory.CreateDirectory(HostPaths.DefaultDataRoot);
|
||||
Directory.CreateDirectory(HostPaths.DefaultScriptsDirectory);
|
||||
Directory.CreateDirectory(HostPaths.DefaultLogDirectory);
|
||||
|
||||
var layout = ResolveLayout(installDir, portable);
|
||||
Directory.CreateDirectory(layout.Root);
|
||||
Directory.CreateDirectory(layout.ScriptsDirectory);
|
||||
Directory.CreateDirectory(layout.LogDirectory);
|
||||
|
||||
var settingsDir = Path.GetDirectoryName(layout.SharedSettingsFile);
|
||||
var settingsDir = Path.GetDirectoryName(HostPaths.SharedSettingsFile);
|
||||
if (!string.IsNullOrEmpty(settingsDir))
|
||||
Directory.CreateDirectory(settingsDir);
|
||||
|
||||
if (OperatingSystem.IsWindows()) {
|
||||
if (layout.Portable) {
|
||||
GrantUsersModify(layout.ScriptsDirectory, messages);
|
||||
GrantUsersModify(layout.LogDirectory, messages);
|
||||
if (!string.IsNullOrEmpty(settingsDir))
|
||||
GrantUsersModify(settingsDir, messages);
|
||||
}
|
||||
else {
|
||||
GrantUsersModify(layout.Root, messages);
|
||||
if (!string.IsNullOrEmpty(settingsDir))
|
||||
GrantUsersModify(settingsDir, messages);
|
||||
}
|
||||
GrantUsersModify(HostPaths.DefaultDataRoot, messages);
|
||||
if (!string.IsNullOrEmpty(settingsDir))
|
||||
GrantUsersModify(settingsDir, messages);
|
||||
}
|
||||
else if (OperatingSystem.IsLinux() || OperatingSystem.IsMacOS()) {
|
||||
GrantUnixGroupWrite(layout.Root, messages);
|
||||
GrantUnixGroupWrite(layout.ScriptsDirectory, messages);
|
||||
GrantUnixGroupWrite(layout.LogDirectory, messages);
|
||||
GrantUnixGroupWrite(HostPaths.DefaultDataRoot, messages);
|
||||
GrantUnixGroupWrite(HostPaths.DefaultScriptsDirectory, messages);
|
||||
GrantUnixGroupWrite(HostPaths.DefaultLogDirectory, messages);
|
||||
if (!string.IsNullOrEmpty(settingsDir))
|
||||
GrantUnixGroupWrite(settingsDir, messages);
|
||||
}
|
||||
|
||||
var source = HostPaths.FindBundledScriptsDirectory(installDir);
|
||||
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)
|
||||
messages.Add($"Copied {copy.CopiedFiles} new seed script file(s) to {layout.ScriptsDirectory}.");
|
||||
if (copy.SkippedItems > 0)
|
||||
messages.Add($"Left {copy.SkippedItems} existing script folder(s)/file(s) unchanged in {layout.ScriptsDirectory}.");
|
||||
if (copy.CopiedFiles == 0 && copy.SkippedItems == 0)
|
||||
messages.Add($"Scripts directory: {layout.ScriptsDirectory}");
|
||||
}
|
||||
var copy = CopySeedScripts(installDir, HostPaths.DefaultScriptsDirectory);
|
||||
if (copy.CopiedFiles > 0)
|
||||
messages.Add($"Copied {copy.CopiedFiles} new seed script file(s) to {HostPaths.DefaultScriptsDirectory}.");
|
||||
if (copy.SkippedItems > 0)
|
||||
messages.Add($"Left {copy.SkippedItems} existing script folder(s)/file(s) unchanged in {HostPaths.DefaultScriptsDirectory}.");
|
||||
if (copy.CopiedFiles == 0 && copy.SkippedItems == 0)
|
||||
messages.Add($"Scripts directory: {HostPaths.DefaultScriptsDirectory}");
|
||||
|
||||
WritePortableSettingsIfNeeded(layout, installDir);
|
||||
messages.Add($"Logs directory: {layout.LogDirectory}");
|
||||
messages.Add($"Shared settings: {layout.SharedSettingsFile}");
|
||||
if (layout.Portable)
|
||||
messages.Add("Install type: portable (everything under the install folder).");
|
||||
messages.Add($"Logs directory: {HostPaths.DefaultLogDirectory}");
|
||||
messages.Add($"Shared settings: {HostPaths.SharedSettingsFile}");
|
||||
return new HostServiceOperationResult(true, string.Join(Environment.NewLine, messages));
|
||||
}
|
||||
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>
|
||||
/// 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),
|
||||
@ -154,16 +100,6 @@ public static class HostDataDirectories {
|
||||
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) {
|
||||
Directory.CreateDirectory(destination);
|
||||
var copied = 0;
|
||||
@ -208,7 +144,10 @@ public static class HostDataDirectories {
|
||||
}
|
||||
|
||||
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) {
|
||||
using var process = Process.Start(new ProcessStartInfo {
|
||||
|
||||
@ -4,21 +4,13 @@ using MaksIT.UScheduler.Shared.Helpers;
|
||||
namespace MaksIT.UScheduler.Shared;
|
||||
|
||||
/// <summary>
|
||||
/// Default install and data locations. Standard layout keeps binaries under
|
||||
/// Program Files and scripts, logs, and machine settings outside Program Files
|
||||
/// so normal users can read and write them after <c>--prepare-data</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.
|
||||
/// Default install and data locations. Binaries live under Program Files;
|
||||
/// scripts, logs, and machine settings are outside Program Files so normal
|
||||
/// users can read and write them after <c>--prepare-data</c> / <c>--install</c>.
|
||||
/// </summary>
|
||||
public static class HostPaths {
|
||||
public const string ProductFolder = "UScheduler";
|
||||
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 =>
|
||||
UserSettingsPath.GetShared(ProductFolder);
|
||||
@ -61,111 +53,21 @@ public static class HostPaths {
|
||||
public static string DefaultDataRoot =>
|
||||
OperatingSystem.IsWindows() ? @"C:\MaksIT" : "/var/lib/maksit";
|
||||
|
||||
public static bool IsPortableLayout(string? startDirectory = null) =>
|
||||
FindPortableRoot(startDirectory) is not null;
|
||||
|
||||
/// <summary>
|
||||
/// Directory that contains <see cref="PortableMarkerFileName"/>, walking up
|
||||
/// from <paramref name="startDirectory"/> (or the current app base).
|
||||
/// Scripts folder to use at runtime: configured path if it exists, then
|
||||
/// <see cref="DefaultScriptsDirectory"/>, then a bundled/dev tree.
|
||||
/// </summary>
|
||||
public static string? FindPortableRoot(string? startDirectory = 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;
|
||||
public static string ResolveScriptsDirectory(string? configured = null) {
|
||||
if (!string.IsNullOrWhiteSpace(configured)) {
|
||||
var resolved = PathHelper.ResolvePath(configured, start);
|
||||
var resolved = PathHelper.ResolvePath(configured);
|
||||
if (Directory.Exists(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))
|
||||
return DefaultScriptsDirectory;
|
||||
|
||||
var bundled = FindBundledScriptsDirectory(start);
|
||||
var bundled = FindBundledScriptsDirectory();
|
||||
return bundled ?? DefaultScriptsDirectory;
|
||||
}
|
||||
|
||||
@ -173,8 +75,8 @@ public static class HostPaths {
|
||||
var current = startDirectory ?? AppContext.BaseDirectory;
|
||||
for (var i = 0; i < 10 && !string.IsNullOrEmpty(current); i++) {
|
||||
foreach (var candidate in new[] {
|
||||
Path.Combine(current, PortableScriptsFolderName),
|
||||
Path.Combine(current, "src", PortableScriptsFolderName)
|
||||
Path.Combine(current, "Scripts"),
|
||||
Path.Combine(current, "src", "Scripts")
|
||||
}) {
|
||||
if (LooksLikeScriptsRoot(candidate))
|
||||
return candidate;
|
||||
@ -238,17 +140,7 @@ public static class HostPaths {
|
||||
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) =>
|
||||
Directory.Exists(path)
|
||||
&& (Directory.Exists(Path.Combine(path, "File-Sync"))
|
||||
|| 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")));
|
||||
Directory.Exists(Path.Combine(path, "File-Sync"))
|
||||
|| File.Exists(Path.Combine(path, "SchedulerTemplate.psm1"));
|
||||
}
|
||||
|
||||
@ -173,139 +173,6 @@ public class HostPathsTests {
|
||||
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]
|
||||
public void ResolveScriptPath_uses_scripts_dir() {
|
||||
var dir = Path.Combine(Path.GetTempPath(), $"uscheduler-scripts-{Guid.NewGuid():N}");
|
||||
|
||||
@ -76,78 +76,4 @@ public class HostDataDirectoriesTests {
|
||||
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>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<RootNamespace>MaksIT.UScheduler.UI</RootNamespace>
|
||||
<Version>1.2.0</Version>
|
||||
<Version>1.1.0</Version>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<BuiltInComInteropSupport>true</BuiltInComInteropSupport>
|
||||
@ -14,10 +14,10 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Avalonia" Version="12.1.2" />
|
||||
<PackageReference Include="Avalonia.Desktop" Version="12.1.2" />
|
||||
<PackageReference Include="Avalonia.Themes.Simple" Version="12.1.2" />
|
||||
<PackageReference Include="Avalonia.Controls.DataGrid" Version="12.1.2" />
|
||||
<PackageReference Include="Avalonia" Version="12.1.0" />
|
||||
<PackageReference Include="Avalonia.Desktop" Version="12.1.0" />
|
||||
<PackageReference Include="Avalonia.Themes.Simple" Version="12.1.0" />
|
||||
<PackageReference Include="Avalonia.Controls.DataGrid" Version="12.1.0" />
|
||||
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.4.2" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
@ -9,8 +9,6 @@ internal static class Program {
|
||||
public static void Main(string[] 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() =>
|
||||
AppBuilder.Configure<App>()
|
||||
.UsePlatformDetect()
|
||||
|
||||
@ -5,8 +5,7 @@ using MaksIT.UScheduler.Shared.Helpers;
|
||||
namespace MaksIT.UScheduler.UI.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Loads and saves machine-wide service configuration (ProgramData settings.json,
|
||||
/// or the portable Data folder).
|
||||
/// Loads and saves machine-wide service configuration (ProgramData settings.json).
|
||||
/// Shipped appsettings.json next to the worker is seed-only.
|
||||
/// </summary>
|
||||
public class AppSettingsService {
|
||||
@ -25,10 +24,10 @@ public class AppSettingsService {
|
||||
if (string.IsNullOrEmpty(seed) && !string.IsNullOrEmpty(serviceDirectory))
|
||||
seed = Path.Combine(serviceDirectory, ConfigurationFileService.SeedFileName);
|
||||
|
||||
_files = new ConfigurationFileService(HostPaths.ResolveSharedSettingsFile(serviceDirectory), seed);
|
||||
_files = new ConfigurationFileService(HostPaths.SharedSettingsFile, seed);
|
||||
var config = _files.Current;
|
||||
config.EnsureDefaults();
|
||||
ScriptsDirectory = config.GetEffectiveScriptsDirectory(_serviceDirectory);
|
||||
ScriptsDirectory = config.GetEffectiveScriptsDirectory();
|
||||
return config;
|
||||
}
|
||||
|
||||
@ -38,7 +37,7 @@ public class AppSettingsService {
|
||||
|
||||
try {
|
||||
_files.Save(configuration);
|
||||
ScriptsDirectory = configuration.GetEffectiveScriptsDirectory(_serviceDirectory);
|
||||
ScriptsDirectory = configuration.GetEffectiveScriptsDirectory();
|
||||
return true;
|
||||
}
|
||||
catch {
|
||||
|
||||
@ -7,8 +7,7 @@ using MaksIT.UScheduler.Shared.Helpers;
|
||||
namespace MaksIT.UScheduler.UI.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Per-user UI prefs stored in roaming AppData (not next to the exe), or under
|
||||
/// the portable Data folder when a portable layout is used.
|
||||
/// Per-user UI prefs stored in roaming AppData (not next to the exe).
|
||||
/// </summary>
|
||||
public class UISettings {
|
||||
/// <summary>
|
||||
@ -19,19 +18,17 @@ public class UISettings {
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Service for loading and saving UI settings under the current user's AppData
|
||||
/// (or the portable Data folder). Machine-wide schedule configuration lives in
|
||||
/// ProgramData (or that same Data folder) via <see cref="ConfigurationFileService"/>.
|
||||
/// Service for loading and saving UI settings under the current user's AppData.
|
||||
/// Machine-wide schedule configuration lives in ProgramData via
|
||||
/// <see cref="ConfigurationFileService"/>.
|
||||
/// </summary>
|
||||
public class UISettingsService {
|
||||
public const string ProductFolder = HostPaths.ProductFolder;
|
||||
|
||||
private readonly string _settingsFilePath;
|
||||
|
||||
public UISettingsService(string? settingsFilePath = null) {
|
||||
_settingsFilePath = string.IsNullOrWhiteSpace(settingsFilePath)
|
||||
? HostPaths.ResolveUiSettingsFile()
|
||||
: settingsFilePath;
|
||||
public UISettingsService() {
|
||||
_settingsFilePath = UserSettingsPath.Get(ProductFolder);
|
||||
}
|
||||
|
||||
public UISettings Load() {
|
||||
|
||||
@ -78,8 +78,7 @@ public partial class MainViewModel : ObservableObject {
|
||||
var seedPath = string.IsNullOrEmpty(bin)
|
||||
? null
|
||||
: Path.Combine(bin, ConfigurationFileService.SeedFileName);
|
||||
var sharedSettingsPath = HostPaths.ResolveSharedSettingsFile(bin);
|
||||
var sharedExists = File.Exists(sharedSettingsPath);
|
||||
var sharedExists = File.Exists(HostPaths.SharedSettingsFile);
|
||||
var seedExists = !string.IsNullOrEmpty(seedPath) && File.Exists(seedPath);
|
||||
|
||||
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.";
|
||||
ServiceName = string.Empty;
|
||||
LogDirectory = string.Empty;
|
||||
ScriptsDirectory = HostPaths.ResolveScriptsDirectory(startDirectory: bin);
|
||||
SharedSettingsPath = string.IsNullOrEmpty(bin) ? HostPaths.ResolveSharedSettingsFile() : sharedSettingsPath;
|
||||
ScriptsDirectory = HostPaths.DefaultScriptsDirectory;
|
||||
SharedSettingsPath = HostPaths.SharedSettingsFile;
|
||||
ProcessList.Clear();
|
||||
return;
|
||||
}
|
||||
@ -99,8 +98,8 @@ public partial class MainViewModel : ObservableObject {
|
||||
_serviceManager = new HostServiceManager(_serviceConfig.ServiceName);
|
||||
ServiceName = _serviceConfig.ServiceName;
|
||||
LogDirectory = _serviceConfig.GetEffectiveLogDirectory(bin);
|
||||
ScriptsDirectory = _serviceConfig.GetEffectiveScriptsDirectory(bin);
|
||||
SharedSettingsPath = _appSettingsService.SettingsPath ?? HostPaths.ResolveSharedSettingsFile(bin);
|
||||
ScriptsDirectory = _serviceConfig.GetEffectiveScriptsDirectory();
|
||||
SharedSettingsPath = _appSettingsService.SettingsPath ?? HostPaths.SharedSettingsFile;
|
||||
ProcessList.Clear();
|
||||
foreach (var p in _serviceConfig.Processes)
|
||||
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.";
|
||||
ServiceName = string.Empty;
|
||||
LogDirectory = string.Empty;
|
||||
ScriptsDirectory = HostPaths.ResolveScriptsDirectory(startDirectory: bin);
|
||||
SharedSettingsPath = HostPaths.ResolveSharedSettingsFile(bin);
|
||||
ScriptsDirectory = HostPaths.DefaultScriptsDirectory;
|
||||
SharedSettingsPath = HostPaths.SharedSettingsFile;
|
||||
ProcessList.Clear();
|
||||
}
|
||||
}
|
||||
@ -133,7 +132,7 @@ public partial class MainViewModel : ObservableObject {
|
||||
private string _scriptsDirectory = HostPaths.DefaultScriptsDirectory;
|
||||
|
||||
[ObservableProperty]
|
||||
private string _sharedSettingsPath = HostPaths.ResolveSharedSettingsFile();
|
||||
private string _sharedSettingsPath = HostPaths.SharedSettingsFile;
|
||||
|
||||
[ObservableProperty]
|
||||
[NotifyPropertyChangedFor(nameof(HasAppSettingsLoadError))]
|
||||
@ -206,7 +205,7 @@ public partial class MainViewModel : ObservableObject {
|
||||
if (string.IsNullOrWhiteSpace(ResolvedExecutablePath) || !File.Exists(ResolvedExecutablePath)) {
|
||||
await _dialogs.ShowMessageAsync(
|
||||
"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;
|
||||
}
|
||||
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<RootNamespace>$(MSBuildProjectName.Replace(" ", "_"))</RootNamespace>
|
||||
<Version>1.2.0</Version>
|
||||
<Version>1.1.0</Version>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<UserSecretsId>dotnet-UScheduler-040d8105-9e07-4024-a632-cbe091387b66</UserSecretsId>
|
||||
@ -41,9 +41,5 @@
|
||||
<None Include="..\..\assets\**\*" Link="assets\%(RecursiveDir)%(Filename)%(Extension)">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
<Content Include="..\Scripts\**\*" Link="Scripts\%(RecursiveDir)%(Filename)%(Extension)">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
|
||||
</Content>
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
@ -9,20 +9,18 @@ using MaksIT.UScheduler.BackgroundServices;
|
||||
|
||||
var basePath = AppContext.BaseDirectory;
|
||||
var seedPath = Path.Combine(basePath, ConfigurationFileService.SeedFileName);
|
||||
var portableRequested = HasFlag(args, "--portable");
|
||||
var settingsPath = HostPaths.SharedSettingsFile;
|
||||
|
||||
if (args.Length > 0) {
|
||||
var early = args[0].ToLowerInvariant();
|
||||
if (early is "--install" or "-i" or "--prepare-data") {
|
||||
var prepared = HostDataDirectories.Prepare(basePath, portableRequested);
|
||||
var prepared = HostDataDirectories.Prepare(basePath);
|
||||
Console.WriteLine(prepared.Message);
|
||||
if (!prepared.Success)
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
var settingsPath = HostPaths.ResolveSharedSettingsFile(basePath);
|
||||
|
||||
_ = new ConfigurationFileService(settingsPath, seedPath);
|
||||
|
||||
var configurationRoot = new ConfigurationBuilder()
|
||||
@ -65,7 +63,7 @@ if (args.Length > 0) {
|
||||
return GetServiceStatus(serviceName);
|
||||
|
||||
case "--prepare-data":
|
||||
return PrepareDataDirectories(basePath, portableRequested);
|
||||
return PrepareDataDirectories(basePath);
|
||||
|
||||
case "--help":
|
||||
case "-h":
|
||||
@ -125,12 +123,11 @@ static void PrintHelp(string name) =>
|
||||
--start Start the service
|
||||
--stop Stop the service
|
||||
--status Query service status
|
||||
--prepare-data Create scripts, logs, and shared settings (elevated)
|
||||
--portable With --install / --prepare-data: keep data in the install folder
|
||||
--prepare-data Create C:\MaksIT\Scripts, C:\MaksIT\Logs, and shared settings (elevated)
|
||||
--help, -h Show this help message
|
||||
|
||||
Service Name: {name}
|
||||
Config File: {HostPaths.ResolveSharedSettingsFile()}
|
||||
Config File: {HostPaths.SharedSettingsFile}
|
||||
Seed File: appsettings.json (next to the executable, logging + first-run seed)
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
static int PrepareDataDirectories(string installDirectory, bool portable) {
|
||||
var layout = HostDataDirectories.ResolveLayout(installDirectory, portable);
|
||||
static int PrepareDataDirectories(string installDirectory) {
|
||||
Console.WriteLine($"Install: {installDirectory}");
|
||||
Console.WriteLine($"Type: {(layout.Portable ? "portable" : "standard")}");
|
||||
Console.WriteLine($"Settings: {layout.SharedSettingsFile}");
|
||||
Console.WriteLine($"Scripts: {layout.ScriptsDirectory}");
|
||||
Console.WriteLine($"Logs: {layout.LogDirectory}");
|
||||
Console.WriteLine($"Settings: {HostPaths.SharedSettingsFile}");
|
||||
Console.WriteLine($"Scripts: {HostPaths.DefaultScriptsDirectory}");
|
||||
Console.WriteLine($"Logs: {HostPaths.DefaultLogDirectory}");
|
||||
return 0;
|
||||
}
|
||||
|
||||
static bool HasFlag(string[] args, string name) =>
|
||||
args.Any(a => string.Equals(a, name, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
static int UninstallService(string name) {
|
||||
if (OperatingSystem.IsWindows())
|
||||
return UninstallWindowsService(name);
|
||||
|
||||
@ -9,7 +9,7 @@
|
||||
Does not publish. Copies src/Scripts into each MaksIT.UScheduler RID output,
|
||||
rewrites worker seed appsettings for the bundled layout, and
|
||||
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)) {
|
||||
@ -245,9 +245,7 @@ DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
|
||||
exec "$DIR/MaksIT.UScheduler.UI/MaksIT.UScheduler.UI" "$@"
|
||||
'@
|
||||
Set-Content -Path $shPath -Value $shBody -Encoding utf8
|
||||
$portableMarkerPath = Join-Path $bundleDirectory "portable"
|
||||
Set-Content -Path $portableMarkerPath -Value "MaksIT.UScheduler portable layout" -Encoding ascii
|
||||
Write-Log -Level "OK" -Message " Created launchers and portable marker."
|
||||
Write-Log -Level "OK" -Message " Created launchers."
|
||||
|
||||
$installerPayload = if ($pluginSettings.PSObject.Properties['installerPayloadDir'] -and -not [string]::IsNullOrWhiteSpace([string]$pluginSettings.installerPayloadDir)) {
|
||||
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"
|
||||
}
|
||||
|
||||
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) {
|
||||
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
|
||||
Copy-Item -Path (Join-Path ([string]$uiWin.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 Scripts: $payloadScripts"
|
||||
|
||||
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
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft-07/schema",
|
||||
"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": [
|
||||
{
|
||||
"name": "DotNetReleaseVersion",
|
||||
@ -80,36 +80,21 @@
|
||||
"installScope": "perMachine",
|
||||
"installFolderName": "UScheduler",
|
||||
"iconPath": "..\\..\\..\\src\\MaksIT.UScheduler.UI\\Assets\\icon.ico",
|
||||
"exeNamePattern": "maksit-uscheduler-{version}.exe",
|
||||
"postInstallExecutableName": "MaksIT.UScheduler.exe",
|
||||
"postInstallArguments": "--prepare-data"
|
||||
"exeNamePattern": "maksit-uscheduler-{version}.exe"
|
||||
},
|
||||
{
|
||||
"name": "FlatpakPack",
|
||||
"stageLabel": "build",
|
||||
"enabled": true,
|
||||
"appId": "com.maks_it.uscheduler",
|
||||
"appId": "com.maks_it.UScheduler",
|
||||
"appName": "MaksIT UScheduler",
|
||||
"command": "maksit-uscheduler",
|
||||
"executableName": "MaksIT.UScheduler.UI",
|
||||
"runtimeIdentifier": "linux-x64",
|
||||
"publishDir": "..\\..\\..\\releases\\MaksIT.UScheduler.UI\\linux-x64",
|
||||
"summary": "Schedule PowerShell scripts and processes",
|
||||
"description": "Schedule and run PowerShell scripts and console processes from an Avalonia desktop UI.",
|
||||
"projectLicense": "Apache-2.0",
|
||||
"categories": "Utility;System;",
|
||||
"projectLicense": "MIT",
|
||||
"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",
|
||||
"buildBundle": true,
|
||||
"attachSourceZip": false,
|
||||
|
||||
@ -496,10 +496,9 @@ function Get-DefaultFlatpakFinishArgs {
|
||||
return @(
|
||||
'--share=ipc',
|
||||
'--share=network',
|
||||
'--socket=x11',
|
||||
'--socket=fallback-x11',
|
||||
'--socket=wayland',
|
||||
'--device=dri',
|
||||
'--socket=pulseaudio',
|
||||
'--filesystem=home'
|
||||
)
|
||||
}
|
||||
@ -525,118 +524,12 @@ function New-FlatpakDesktopEntry {
|
||||
"Name=$AppName",
|
||||
"Exec=$Command",
|
||||
"Icon=$AppId",
|
||||
"StartupWMClass=$AppId",
|
||||
'StartupNotify=true',
|
||||
'Terminal=false',
|
||||
"Categories=$Categories"
|
||||
)
|
||||
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 {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
@ -648,124 +541,26 @@ function New-FlatpakMetainfoXml {
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$Summary,
|
||||
|
||||
[Parameter(Mandatory = $false)]
|
||||
[AllowEmptyString()]
|
||||
[string]$Description = '',
|
||||
|
||||
[Parameter(Mandatory = $false)]
|
||||
[string]$ProjectLicense = 'MIT',
|
||||
|
||||
[Parameter(Mandatory = $false)]
|
||||
[string]$MetadataLicense = 'CC0-1.0',
|
||||
|
||||
[Parameter(Mandatory = $false)]
|
||||
[string]$Version = ''
|
||||
[string]$MetadataLicense = 'CC0-1.0'
|
||||
)
|
||||
|
||||
$escapedName = [System.Security.SecurityElement]::Escape($AppName)
|
||||
$escapedSummary = [System.Security.SecurityElement]::Escape($Summary)
|
||||
$descriptionXml = ConvertTo-FlatpakMetainfoDescriptionXml -AppName $AppName -Summary $Summary -Description $Description
|
||||
$xml = @"
|
||||
return @"
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<component type="desktop-application">
|
||||
<id>$AppId</id>
|
||||
<name>$escapedName</name>
|
||||
<summary>$escapedSummary</summary>
|
||||
<description>
|
||||
$descriptionXml
|
||||
</description>
|
||||
<launchable type="desktop-id">$AppId.desktop</launchable>
|
||||
<metadata_license>$MetadataLicense</metadata_license>
|
||||
<project_license>$ProjectLicense</project_license>
|
||||
</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 {
|
||||
@ -802,8 +597,8 @@ function New-FlatpakManifestObject {
|
||||
if ($null -eq $BuildCommands -or $BuildCommands.Count -eq 0) {
|
||||
$BuildCommands = @(
|
||||
'mkdir -p /app/lib /app/share',
|
||||
'cp -a lib/. /app/lib/',
|
||||
'cp -a share/. /app/share/',
|
||||
'cp -a lib /app/lib',
|
||||
'cp -a share /app/share',
|
||||
"install -Dm755 bin/$Command /app/bin/$Command",
|
||||
"chmod +x /app/lib/$ModuleName/$Command"
|
||||
)
|
||||
@ -841,15 +636,7 @@ function New-FlatpakLaunchScript {
|
||||
[string]$ExecutableFileName
|
||||
)
|
||||
|
||||
$lib = "/app/lib/$ModuleName"
|
||||
return @"
|
||||
#!/bin/sh
|
||||
LIB='$lib'
|
||||
if [ -f "`$LIB/flatpak-native.env" ]; then
|
||||
. "`$LIB/flatpak-native.env"
|
||||
fi
|
||||
exec `$LIB/$ExecutableFileName "`$`@"
|
||||
"@
|
||||
return "#!/bin/sh`nexec /app/lib/$ModuleName/$ExecutableFileName `"`$@`"`n"
|
||||
}
|
||||
|
||||
function Assert-FlatpakAppId {
|
||||
@ -1233,9 +1020,7 @@ Export-ModuleMember -Function `
|
||||
New-WixBundleXml, `
|
||||
Get-DefaultFlatpakFinishArgs, `
|
||||
New-FlatpakDesktopEntry, `
|
||||
Copy-FlatpakHicolorIcons, `
|
||||
New-FlatpakMetainfoXml, `
|
||||
Set-FlatpakMetainfoRelease, `
|
||||
New-FlatpakManifestObject, `
|
||||
New-FlatpakLaunchScript, `
|
||||
Assert-FlatpakAppId, `
|
||||
|
||||
@ -13,8 +13,6 @@
|
||||
(default Debian) on the Linux filesystem, then copies the bundle back.
|
||||
Docker Desktop WSL distros are never used. whenBuilderMissing skip|fail
|
||||
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)) {
|
||||
@ -36,37 +34,6 @@ function Get-FlatpakWhenBuilderMissing {
|
||||
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 {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
@ -221,9 +188,6 @@ function Invoke-Plugin {
|
||||
$scriptDir = $sharedSettings.scriptDir
|
||||
$version = [string]$sharedSettings.version
|
||||
$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')
|
||||
$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')
|
||||
$publishDirSetting = [string](Get-PluginPropertyValue -PluginSettings $pluginSettings -Name 'publishDir')
|
||||
$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)
|
||||
$description = [string](Get-PluginPropertyValue -PluginSettings $pluginSettings -Name 'description' -Default '')
|
||||
$projectLicense = [string](Get-PluginPropertyValue -PluginSettings $pluginSettings -Name 'projectLicense' -Default 'MIT')
|
||||
$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')
|
||||
$runtimeVersion = [string](Get-PluginPropertyValue -PluginSettings $pluginSettings -Name 'runtimeVersion' -Default '24.08')
|
||||
$sdk = [string](Get-PluginPropertyValue -PluginSettings $pluginSettings -Name 'sdk' -Default 'org.freedesktop.Sdk')
|
||||
@ -304,8 +264,8 @@ function Invoke-Plugin {
|
||||
$binDir = Join-Path $filesRoot 'bin'
|
||||
$shareAppDir = Join-Path $filesRoot 'share\applications'
|
||||
$shareMetaDir = Join-Path $filesRoot 'share\metainfo'
|
||||
$hicolorRoot = Join-Path $filesRoot 'share\icons\hicolor'
|
||||
New-Item -ItemType Directory -Path $libDir, $binDir, $shareAppDir, $shareMetaDir, $hicolorRoot -Force | Out-Null
|
||||
$shareIconDir = Join-Path $filesRoot 'share\icons\hicolor\256x256\apps'
|
||||
New-Item -ItemType Directory -Path $libDir, $binDir, $shareAppDir, $shareMetaDir, $shareIconDir -Force | Out-Null
|
||||
|
||||
Write-Log -Level "STEP" -Message "Staging Flatpak layout from $publishDirectory"
|
||||
Copy-Item -Path (Join-Path $publishDirectory '*') -Destination $libDir -Recurse -Force
|
||||
@ -314,56 +274,20 @@ function Invoke-Plugin {
|
||||
$launchPath = Join-Path $binDir $command
|
||||
[System.IO.File]::WriteAllText($launchPath, $launchScript.Replace("`r`n", "`n"), [System.Text.UTF8Encoding]::new($false))
|
||||
|
||||
$desktopDest = 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"
|
||||
}
|
||||
$desktopPath = Join-Path $shareAppDir "$appId.desktop"
|
||||
[System.IO.File]::WriteAllText(
|
||||
$desktopPath,
|
||||
(New-FlatpakDesktopEntry -AppId $appId -AppName $appName -Command $command -Categories $categories).Replace("`r`n", "`n"),
|
||||
[System.Text.UTF8Encoding]::new($false)
|
||||
)
|
||||
|
||||
[System.IO.File]::WriteAllText($desktopDest, $desktopText, [System.Text.UTF8Encoding]::new($false))
|
||||
}
|
||||
else {
|
||||
[System.IO.File]::WriteAllText(
|
||||
$desktopDest,
|
||||
(New-FlatpakDesktopEntry -AppId $appId -AppName $appName -Command $command -Categories $categories).Replace("`r`n", "`n"),
|
||||
[System.Text.UTF8Encoding]::new($false)
|
||||
)
|
||||
}
|
||||
$metaPath = Join-Path $shareMetaDir "$appId.metainfo.xml"
|
||||
[System.IO.File]::WriteAllText(
|
||||
$metaPath,
|
||||
(New-FlatpakMetainfoXml -AppId $appId -AppName $appName -Summary $summary -ProjectLicense $projectLicense).Replace("`r`n", "`n"),
|
||||
[System.Text.UTF8Encoding]::new($false)
|
||||
)
|
||||
|
||||
$metaDest = 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(
|
||||
$metaDest,
|
||||
(New-FlatpakMetainfoXml -AppId $appId -AppName $appName -Summary $summary -Description $description -ProjectLicense $projectLicense -Version $version).Replace("`r`n", "`n"),
|
||||
[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)) {
|
||||
$iconPath = if ([System.IO.Path]::IsPathRooted($iconSetting)) {
|
||||
$iconSetting
|
||||
@ -375,39 +299,21 @@ function Invoke-Plugin {
|
||||
if (-not (Test-Path -LiteralPath $iconPath -PathType Leaf)) {
|
||||
throw "FlatpakPack iconPath not found: $iconPath"
|
||||
}
|
||||
}
|
||||
|
||||
$svgIconPath = $null
|
||||
if (-not [string]::IsNullOrWhiteSpace($svgIconSetting)) {
|
||||
$svgIconPath = if ([System.IO.Path]::IsPathRooted($svgIconSetting)) {
|
||||
$svgIconSetting
|
||||
if ([System.IO.Path]::GetExtension($iconPath) -ine '.png') {
|
||||
Write-Log -Level "WARN" -Message " Flatpak icon should be PNG; skipping copy of $iconPath"
|
||||
}
|
||||
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 = @(
|
||||
'mkdir -p /app/lib /app/share',
|
||||
"cp -a lib/$moduleName /app/lib/$moduleName",
|
||||
'cp -a share/. /app/share/',
|
||||
'cp -a share /app/share',
|
||||
"install -Dm755 bin/$command /app/bin/$command",
|
||||
"chmod +x /app/lib/$moduleName/$executableFileName",
|
||||
'gtk-update-icon-cache -f /app/share/icons/hicolor >/dev/null 2>&1 || true'
|
||||
"chmod +x /app/lib/$moduleName/$executableFileName"
|
||||
)
|
||||
|
||||
$manifest = New-FlatpakManifestObject `
|
||||
|
||||
@ -26,25 +26,17 @@
|
||||
<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="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="NOT WixStdBASuppressOptionsUI">#(loc.InstallMessageOptions)</Text>
|
||||
</Label>
|
||||
<RadioButtons Name="InstallLayout">
|
||||
<RadioButton X="185" Y="116" Height="18" Width="-11" Value="standard" TabStop="yes" FontId="3">Standard — Program Files, data in C:\MaksIT</RadioButton>
|
||||
<RadioButton X="185" Y="134" Height="18" Width="-11" Value="portable" TabStop="yes" FontId="3">Portable — everything in one folder</RadioButton>
|
||||
</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">
|
||||
<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="200" 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>
|
||||
<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>
|
||||
|
||||
@ -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